Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 2364 2365 2366 2367 2368 2369 2370 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387 2388 2389 2390 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403 2404 2405 | 2x 2x 2x 2x 2x 60x 60x 60x 2x 82x 82x 76x 74x 72x 69x 65x 82x 2x 17x 17x 2x 3x 3x 3x 3x 3x 3x 3x 2x 83x 83x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2435x 2435x 2292x 2139x 1996x 1852x 1708x 1564x 2435x 2x 66x 66x 2x 1024x 1024x 2x 1223x 1223x 2x 2090x 2090x 2x 225x 225x 2x 2x 1173x 1173x 2x 778x 778x 2x 350x 350x 350x 350x 350x 2x 2x 2x 63x 63x 63x 2x 2x 2x 8x 8x 8x 8x 8x 8x 2x 93x 93x 2x 3x 3x 2x 370x 370x 2x 7x 7x 2x 3410x 3410x 3410x 2x 2x 4x 1x 1x 3x 3x 3x 3x 4x 4x 4x 1023x 1023x 1023x 849x 755x 750x 748x 748x 748x 748x 724x 295x 295x 2x 1023x 1023x 1023x 1023x 1023x 1023x 1023x 1023x 1023x 1023x 1023x 1023x 1023x 1023x 2x 22x 22x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 419x 419x 419x 419x 419x 419x 419x 419x 419x 419x 419x 419x 419x 419x 419x 419x 419x 419x 419x 419x 419x 419x 419x 419x 419x 419x 419x 419x 419x 419x 2x 1430x 1430x 130x 130x 33x 97x 130x 2x 28x 28x 26x 26x 26x 28x 28x 28x 28x 28x 78x 28x 28x 28x 28x 28x 28x 28x 28x 28x 28x 6x 20x 28x 28x 28x 28x 28x 28x 6x 20x 28x 28x 28x 28x 28x 28x 28x 28x 28x 6x 20x 28x 28x 28x 28x 28x 28x 28x 2x 2x 83x 1x 1x 1x 2x 1x 1x 26x 26x 26x 26x 26x 9x 9x 26x 8x 8x 26x 26x 6x 6x 6x 6x 2x 155x 155x 155x 155x 155x 155x 155x 155x 155x 155x 155x 155x 155x 2x 107x 107x 2x 30x 30x 27x 27x 27x 27x 30x 30x 30x 10x 17x 30x 30x 12x 15x 1x 14x 30x 30x 30x 30x 12x 15x 30x 30x 3x 24x 30x 30x 12x 15x 30x 30x 30x 2x 2x 2x 79x 79x 79x 2x 2x 60x 60x 60x | /**
* What an annotation *is*, before anything decides how to draw it.
*
* Everything in this module is deliberately free of DOM, canvas and worker
* concerns: the same descriptions cross the worker boundary, get persisted to
* session storage, and drive the renderers. Spec §8.
*
* Two shapes carry every text format the viewer accepts. {@link FeatureSet} is
* one interval per row — a BED line, a narrowPeak, a GTF gene with its exons —
* and {@link PairSet} is two intervals per row, which is a bedpe and nothing
* else. Both are typed arrays with a parallel `string[]` for the human-readable
* columns, because a 41,000-line GTF as an array of objects is four megabytes of
* pointer-chasing to draw a picture that has at most a few hundred glyphs in it.
*/
// ---------------------------------------------------------------------------
// What was loaded
// ---------------------------------------------------------------------------
/**
* The four things a loaded file can be.
*
* `sequence` is the odd one: a FASTA is not an annotation *of* a genome, it is
* the genome, and what a lane shows for it depends entirely on the zoom — GC
* content where a pixel is a hundred bases, the residues themselves where a base
* is a pixel. Like a bigWig it carries no payload, because the sequence stays in
* the worker and each view asks for the window it is drawing.
*
* `gtf` and `segments` are both {@link FeatureSet}s and differ only in what the
* reader means by them: a gene model is drawn as exons on a spine and there may
* be only one, a segment file is drawn as boxes and there may be any number.
* The distinction is the user's, made by which drop area the file landed in, and
* it is kept because "the annotation" and "a peak set" are not the same thing on
* screen even when the file format is.
*/
import { DEFAULT_DIVERGING_COLORMAP } from '../render/colormaps'
import { DEFAULT_GENETIC_CODE, isGeneticCode } from './codons'
import type { TrackOp } from './derived'
import type { FeatureStyle } from './fields'
import { modName } from './bam/basemods'
import type { DetectedFormat } from './worker/protocol'
/**
* `metadata` and `classes` are the two the file itself supplies (§8.12). A
* column of `/genomes`, `/contigs` or `/bins` is drawn against the map's own
* cells rather than against base pairs, which is what lets one be drawn on a
* genome × genome overview where nothing else can be — and the two kinds are
* two *pictures* of one idea, because the ink a column deserves depends on
* what is in it. A completeness fraction is a profile with an axis; an MGE call
* is a run of colour with a legend, and drawn as a profile it would be a
* barplot of the order the categories happen to be numbered in.
*/
export type AnnotationKind =
| 'gtf'
| 'track'
| 'segments'
// Called variants (§8.14). A `FeatureSet` like `segments` and `gtf` are, and
// its own kind for the reason those two are each other's: they differ in what
// the reader *means* by a row, and that is what decides the controls, the
// card behind a click, and the colours the lane is drawn in. A peak has a
// score; a variant has a REF, an ALT and a thing that happened.
| 'variants'
// A chart per feature (§8.17). A `FeatureSet` for the same reason `variants`
// is one, and its own kind for a sharper version of the same argument: what a
// reader means by a row here is not an interval that *has* a value but an
// interval that has a **vector**, and there is no way to draw one as a box.
// The glyph, the legend, the card behind a click and the height the lane
// starts at all follow from that, and every one of them is a question no
// other feature kind is asked.
| 'barchart'
// A chromosome's ideogram, from a cytoBand (§8.22). A `FeatureSet` of bands,
// and its own kind because a band is not a box on a row: it is a stretch of
// *one* shape, the chromosome, whose outline pinches at the centromere and
// narrows at a stalk, coloured by the stain the file names. The packing, the
// strand bands, the colour picker and the arrowheads every other interval
// lane has are questions a karyotype is never asked.
| 'ideogram'
// RNA-seq splice junctions (§8.23): a `FeatureSet` of introns with a read
// count on each, drawn as a sashimi plot's arcs from one exon's end to the
// next's start. Its own kind for the glyph, and because it has an axis — two
// samples' arcs are comparable only against one ceiling (§8.17's argument).
| 'junctions'
| 'pairs'
| 'sequence'
| 'metadata'
| 'classes'
// A BAM's depth (§8.13): one value per pixel, an axis, a ceiling — a track in
// every sense except that its values are counted from reads rather than read
// out of a file.
| 'alignments'
// The pile those reads make, which is a *second row* over the same file.
//
// Two kinds rather than two rungs of one, because they are two pictures with
// nothing in common: one is a curve against an axis and is drawn at every
// zoom on both surfaces, the other is a stack of rectangles that exists only
// in the 1D browser and only once a read is wider than a pixel. Sharing a
// lane made every question about it conditional — how tall is it, what does
// the ceiling drag do, which band did the click land in — and a reader who
// wanted the depth without the pile, or the pile without the depth, could ask
// for neither.
| 'reads'
// A 3D model of the chromatin fibre (§25): one bead per interval, at a fixed
// spacing, with a point in space on every row and — where the file is phased
// — two beads at every locus, one per homologue. A `FeatureSet` for the
// reason `barchart` is one, and the argument is the same one sharpened: a
// bead is an interval that *has a place*, and beside a map that interval is
// still a box on a lane, still searchable, still what a metaplot piles on.
// What it has besides is three coordinates, and three coordinates are one
// field ({@link FeatureSet.model}). Its own kind because the picture that
// reads them is not a lane at all but a cell of the deck (`panels/StructurePanel.ts`),
// and every control that picture needs is a question no other kind is asked.
| 'structure'
/**
* Which card an entry belongs to; see §8.1.
*
* `synthetic` is the interesting one (§18.5). An analysis that produces one
* value per bin has produced a track, and a track belongs on the map rather than
* in the drawer it was computed in. Its rows are `kind: 'track'` and are
* indistinguishable from a loaded bigWig from the renderer down — and on
* screen too: they are rows of the Tracks card, in the one list with the loaded
* ones, and the card they were grouped under is gone. The field outlived it,
* and says what only a synthetic row has: a `*` after its name, what made it,
* its exports and its framing, and Recompute where it came back detached.
*
* There was a fourth, `viewpoint`, for the virtual 4C lane that shift-drag used
* to create: a reading of the picture rather than a file, belonging to no card,
* remembered nowhere. The 4C *task* answers the same question from the file, so
* the lane and its card are gone and the gesture is a 1D zoom (§18.9).
*/
export type AnnotationCard = 'linear' | 'overlay' | 'synthetic' | 'metadata' | 'structure'
/**
* Where the bytes come from. The same two-case split as the reader's
* `SourceSpec` (§6.1), and for the same reason: a dropped `File` is handed
* around by reference and never uploaded (§11), a URL is fetched in ranges.
*
* `index` is the sidecar — a `.tbi`, a `.bai` or a `.csi` — and it lives *here*
* rather than being handed to the loader alongside the source, because a
* sidecar is not a fact about one load: it is part of how these bytes are
* reached. Every road that carries a source forward therefore carries the index
* with it, which is the difference between a BAM that survives a resolution
* change and one that comes back refusing to open (§8.13). It is never
* persisted — a `File` cannot be — so a restored row asks for the pair again.
*/
export type AnnotationSource =
| { kind: 'local'; file: File; name: string; index?: File }
| { kind: 'remote'; url: string; name: string; index?: string }
| ComputedSource
| MetadataSource
/**
* A column of the open file's own tables (§8.12).
*
* The one source with no bytes of its own to name, and the only one that comes
* back from a reload **whole**: a dropped file cannot be re-read without a
* gesture and a computed profile has to be worked out again, but a metadata
* column is in the file that is open, so restoring the row is re-reading two
* strings. There is therefore no `detached` state on this road and no
* Recompute button — the row simply draws.
*
* `table` and `column` rather than a joined string, because they are two
* questions with two answers: which of the file's tables, and which of its
* columns. A file that has since lost the column leaves the row saying so
* (`ui/MetadataCard.ts`), which is the one thing a joined string could not do
* without being taken apart again.
*/
export interface MetadataSource {
kind: 'metadata'
/** `contigs/mge_class` — what the row writes when it names its origin. */
name: string
table: 'genomes' | 'contigs' | 'bins'
column: string
}
/**
* A lane an analysis produced rather than a file (§18.5).
*
* It carries a **recipe** and not the numbers. A per-bin vector over a
* metagenome is megabytes and session storage is not for that, so what survives
* a reload is the task and everything it would need to be run again — and the
* row comes back `detached` with a **Recompute** button, which is precisely the
* state a dropped local file returns in and for precisely the same reason
* (§8.6). An exported session therefore hands a colleague the analysis rather
* than a number they have to take on trust.
*
* `recipe` is deliberately an opaque JSON record rather than the analysis
* layer's own `Params` and `AnalysisFraming`. Those types describe an analysis
* and this module describes an annotation; importing them here would make the
* annotation layer depend on the analysis layer, when the true dependency runs
* the other way. The analysis side is the one that knows the shape and is the
* only thing that reads it back.
*/
export interface ComputedSource {
kind: 'computed'
name: string
/** Task id, so the row can say what made it. */
task: string
/** Everything needed to run it again: parameters, framing, task version. */
recipe: Record<string, unknown>
/** `chrIV:1–1,531,933 at 1 kb, balanced` — the row's second line. */
framing: string
}
/**
* Which edge of the map a lane is drawn on.
*
* All four, and they are peers. `bottom` and `right` follow the map's columns
* and rows respectively; `top` is `bottom`'s mirror and `left` is `right`'s, and
* everything from the layout to the value axis is written in terms of *along*
* and *across* so that adding the mirrors added no branch anybody has to keep in
* step (`render/lane.ts`).
*/
export type LaneEdge = 'top' | 'right' | 'bottom' | 'left'
/**
* The edges in the order a stack is laid out and drawn.
*
* Clockwise from the top, which is also the order they are listed in every
* control that offers them, so a reader who has seen one list has seen them all.
*/
export const LANE_EDGES: readonly LaneEdge[] = ['top', 'right', 'bottom', 'left']
/**
* Whether this kind is drawn as a value profile against an axis.
*
* The question every lane asks before it resolves a pair of limits, writes a
* range under a name, or answers a readout with a number — and it is asked in a
* dozen places, which is exactly why it is a function rather than a list of
* kinds spelled out at each of them. A FASTA is one of these beside a map,
* because at a bin per pixel a sequence can only be its GC content.
*/
export function drawsAProfile(kind: AnnotationKind): boolean {
return (
kind === 'track' || kind === 'sequence' || kind === 'metadata' || kind === 'alignments'
)
}
/**
* Whether the reader may move this kind's value axis.
*
* Everything {@link drawsAProfile} covers, **and a barChart**, which draws no
* profile at all and has an axis anyway.
*
* The barChart is the one worth saying out loud. §8.17 gave it a fixed pair of
* limits for a reason that has nothing to do with how it is drawn: both readings
* it had were relative, so two files' lanes side by side were each scaled to
* their own tallest bar and the quieter file drew the taller bars. Having given
* it an axis, every way of moving an axis has to reach it — a ceiling drag, a
* ⌘-scroll, the autoscale button — or the reader is left with a number they can
* only type, over a lane that answers a drag with a pan.
*
* A **sequence** is in the list now, and it used to be the exception: GC
* content is a fraction, `[0, 1]` is the whole of it, and pinning the axis kept
* the same genome looking the same in two views. The pin was only ever half
* kept, which is why it goes. The lane records its limit boxes as click targets
* like every other lane's, the editor stores whatever is typed in them, and the
* settings card offers the row `limits [0] [1] auto` — so a reader who narrowed
* a yeast GC lane to the 0.3–0.5 its values actually live in had no way back
* from the lane, while the lane alone withheld the autoscale mark that is the
* way back. Two surfaces disagreeing about one axis (§8.8). `[0, 1]` survives
* where it belongs, as {@link defaultStyle}'s default, which is what makes two
* views of one genome match until somebody deliberately changes one.
*/
export function hasAMovableAxis(kind: AnnotationKind): boolean {
return (
kind === 'track' ||
kind === 'sequence' ||
kind === 'metadata' ||
kind === 'alignments' ||
kind === 'barchart' ||
// A junction lane is the barChart's case again (§8.23): its arcs are drawn
// against a ceiling, and two samples' lanes are comparable only when that
// ceiling is one number both share — so it has the limits row, and every
// gesture that moves an axis has to reach it.
kind === 'junctions'
)
}
/**
* Whether a lane of this kind packs its features into rows that can be
* expanded, squished or collapsed (`FeatureStyle.display`, §8.16).
*
* The interval lanes whose glyph is a box on a row: a gene model, a segments
* file, a call set. Not a barChart, whose rows are its value axis (§8.17); not
* a karyotype or a junction lane, each one shape along one baseline — the
* chromosome, the arcs — rather than a packing (§8.22, §8.23); and not a
* structure, whose lane is a bead per bin.
*/
export function hasFeatureRows(kind: AnnotationKind): boolean {
return kind === 'gtf' || kind === 'segments' || kind === 'variants'
}
/**
* What a lane of this entry is *called on screen* — `sequence · GC`.
*
* A FASTA drawn as a value track is drawn as its GC content, and an unlabelled
* line between 0 and 1 under a genome's name is a line nobody can name. That
* argument was made in three places and answered in three spellings: the lane
* labels wrote `sequence · GC`, the map's readout `sequence GC`, and the
* browser's tooltip and status line the bare `sequence` — three names for one
* lane in one glance (§8.8). One helper, so the picture, the card's key and both
* readouts say the same words.
*
* The **Tracks card's** row keeps the bare label, and that is not an oversight:
* the card lists the *files* that are loaded, and the file is a sequence. The
* `· GC` belongs to the picture drawn from it.
*/
export function laneLabelOf(
entry: { kind: AnnotationKind; label: string; style?: AnnotationStyle },
/** The modification an alignment lane's percentage is of, once the worker has said. */
modCode?: string,
): string {
if (entry.kind === 'sequence') return `${entry.label} · GC`
// A BAM's depth drawn as a fraction modified is a different quantity on the
// same row, and a 0–100 axis under the file's bare name reads as depth.
if (entry.kind === 'alignments' && entry.style?.reads?.modFraction === true) {
const code = modCode ?? entry.style.reads.modCode
return `${entry.label} · % ${code === '' ? 'modified' : modName(code)}`
}
return entry.label
}
/**
* Whether a lane of this kind is filled from the annotations worker's `sample`.
*
* The question `BrowserView.fill` and `AnnotationLanes.buildEdge` ask before
* they go and fetch anything, and it used to be spelled `kind !== 'track' &&
* kind !== 'sequence'` at both. That is the most dangerous shape a kind test
* takes here, because a kind missing from it is not drawn *badly* — it is not
* drawn at all, and the lane is simply empty with no error anywhere.
*
* `metadata` is deliberately not in this list even though it draws a profile
* and has a movable axis: its values are columns of the open file's own tables
* and come from `cellsFor`, not from the worker (§8.12). Two roads to a
* profile, and this names the one that goes over the wire.
*
* `reads` is not in it either, and for a third reason: it goes over the wire,
* but by its own request. A `sample` answers one number per pixel and a pile is
* not a number per pixel, so the road is `reads` — see `ReadCache`.
*/
export function drawnFromSamples(kind: AnnotationKind): boolean {
return kind === 'track' || kind === 'sequence' || kind === 'alignments'
}
/**
* Every {@link AnnotationKind}, once.
*
* A union cannot be enumerated at run time, so somewhere a list has to be
* written out — and it was written out three times: in the store's stored-row
* validator, in the kind tests, and very nearly in the analysis form. Three
* copies of a list whose whole job is to be complete is three chances for a new
* kind to be missing from one of them, which is the silent failure
* `annotation-kinds.test.ts` exists to catch.
*
* The `satisfies` is what makes this a *check* rather than a fourth copy: a
* kind added to the union and not to this array fails to compile here, and one
* here that is not in the union fails too.
*/
export const ANNOTATION_KINDS = [
'gtf',
'track',
'segments',
'variants',
'barchart',
'ideogram',
'junctions',
'pairs',
'sequence',
'metadata',
'classes',
'alignments',
'reads',
'structure',
] as const satisfies readonly AnnotationKind[]
/**
* Whether this kind is a set of intervals a task can aggregate over.
*
* A gene model, a peak set, a variant set and a barChart are one shape and four
* meanings (§8.1, §8.14, §8.17): they are all `FeatureSet`s, and which one a
* file is depends on what the reader means by a row rather than on anything a
* task cares about. Every task that takes "a BED or a GTF" takes all four — a
* barChart included, because its intervals are intervals whatever is drawn
* inside them, and a metaplot over the genes a barChart names is a reading
* nobody should have to convert a file to ask for.
*
* A function rather than a list spelled at each site, for the reason
* {@link hasTrackControls} is one and in the same words: four literals is not a
* rule, it is a rule spelled four times. It was spelled four times here — the
* form picker's `kindsFor`, and the availability gates of overlap, metaplot and
* APA — and adding `variants` to the first without the other three produced
* exactly the failure that shape produces: a task whose parameter would accept
* a VCF, sitting behind a gate that would not open for one, telling the reader
* to *load a BED or a GTF* while a variant set was on screen.
*/
export function isIntervalSet(kind: AnnotationKind): boolean {
// A structure's beads are intervals too (§25): what a bead carries besides its
// place on the genome does not stop a metaplot piling on the place.
//
// …and so are a karyotype's bands and a sample's introns (§8.22, §8.23). A
// band's score is its stain's darkness, so *Signal by group* split by score
// is a track read over G-light against G-dark bands; an intron's ends are the
// splice sites a metaplot centres on.
return (
kind === 'gtf' ||
kind === 'segments' ||
kind === 'variants' ||
kind === 'barchart' ||
kind === 'ideogram' ||
kind === 'junctions' ||
kind === 'structure'
)
}
/**
* Whether the locus box may search this kind's features **by name** — and so
* whether it can be the starred set the search reads.
*
* Every interval set but two. A karyotype's bands are searched too, but by
* their ISCN names and alongside the gene model rather than instead of it
* (`annotations/search.ts`): a band set made the one search source would turn
* `GAL4` into a search of `p11` and `q12`. A junction has no name a reader
* looks for — regtools' `JUNC00000001` is a row number — and a sample's
* junctions are not where anybody looks a gene up (§8.22, §8.23).
*/
export function searchesByName(kind: AnnotationKind): boolean {
return kind !== 'ideogram' && kind !== 'junctions'
}
/**
* Whether this kind is a 3D model of the fibre, and so carries a
* {@link StructureStyle} and a `model` on its set (§25).
*
* One member today, and a predicate rather than a literal for the reason every
* other one here is: the question is asked by the card, the store's clamp, the
* deck and the picker, and a fifth site spelling `kind === 'structure'` is the
* shape a second structure kind — an ensemble, say — would be missing from.
*/
export function isStructure(kind: AnnotationKind): boolean {
return kind === 'structure'
}
/**
* Whether this kind reads a BAM, and so carries a {@link ReadStyle}.
*
* The two rows an imported BAM makes: the depth and the pile. They are one
* file, one open handle in the worker and one set of filters — a minimum
* mapping quality applied to the pile and not to the depth under it would be
* two pictures of two different files under one name — so everything that is
* about *which reads count* is asked of both through this.
*/
export function readsAlignments(kind: AnnotationKind): boolean {
return kind === 'alignments' || kind === 'reads'
}
/**
* Whether a lane of this kind gets the *whole* set of a track's controls:
* typed limits, autoscale-group membership, the mode and flip switches, and the
* ceiling drag.
*
* A third predicate rather than a fourth spelling of `kind === 'track'`, and it
* is here because those four behaviours were gated by that literal at four
* scattered call sites — `store.scaleGroups`, two arms of `AnnotationCard`, and
* `BrowserView.scalableLane`. Four literals is not a rule, it is a rule spelled
* four times, and the way it fails is instructive: setting
* {@link hasAMovableAxis} reaches none of them, so a new kind gets a ceiling
* drag that works beside the map and silently does nothing in the browser.
*
* Deliberately narrower than {@link hasAMovableAxis}. A metadata column may be
* rescaled and may not join a group: it is a column of the open file rather
* than a measurement, and putting one on a shared axis with a bigWig would be
* asserting they are in the same units.
*
* **The ceiling drag is no longer one of these**, and the warning above is why:
* the two sets coincided only for as long as nothing else had an axis. A
* barChart has one now, so every gesture that *moves* an axis asks
* {@link hasAMovableAxis} on both surfaces, and what is left here is the set of
* controls a lane gets a **row** for in its settings card.
*/
export function hasTrackControls(kind: AnnotationKind): boolean {
return kind === 'track' || kind === 'alignments'
}
/** Whether this kind is one of the file's own columns (§8.12). */
export function isMetadata(kind: AnnotationKind): boolean {
return kind === 'metadata' || kind === 'classes'
}
/** A half-open genomic interval on one contig, as every query here takes one. */
export interface Region {
contig: string
start: number
end: number
}
// ---------------------------------------------------------------------------
// Parsed text formats
// ---------------------------------------------------------------------------
/**
* What joins the attributes of one feature into its `detail` string.
*
* A shared constant because the string is written by two parsers and read back
* apart by the dialog that lists a feature's attributes one to a line. Spelled
* out at each of those three sites, the day one of them gained a space or lost
* one would be the day the dialog quietly stopped splitting — and it would still
* look like a working dialog, showing one long line where there were four.
*/
export const DETAIL_SEP = ' · '
/**
* One interval per row, sorted by `(contig, start)`.
*
* The sort is what makes a range query a binary search rather than a scan, and
* it is done once at parse time — in the worker, off the main thread — because
* the alternative is doing it on every frame of a pan.
*
* `exonAt`/`exonStart`/`exonEnd` are the sub-structure a gene model or a BED12
* row carries, flattened: feature `i` owns `exonStart[exonAt[i] .. exonAt[i+1]]`.
* Left as three empty arrays for a format that has none, so the drawing code
* asks one question ("does this feature have exons") rather than branching on
* which parser produced it.
*/
export interface FeatureSet {
/** Contig names, in the order this file first mentions them. */
contigs: string[]
/** Number of features. Every typed array below is this long, except the exons. */
length: number
/** Index into {@link contigs}, per feature. */
contig: Int32Array
/** 0-based, half-open — the BED convention, which GTF is converted into. */
start: Int32Array
end: Int32Array
/** `+1`, `-1`, or `0` where the format carries no strand. */
strand: Int8Array
/** `NaN` where the format carries no score. */
score: Float32Array
/** What to write on the feature; `''` when there is nothing to write. */
name: string[]
/**
* The **accession** behind the name, where the format carries one: a GTF or
* GFF gene id.
*
* Its own array rather than the first field of {@link detail}, because it is
* asked three questions the other attributes are not. It is what a reader
* types into the search box — half of genomics addresses its genes by
* systematic id and the other half by symbol, and often the same reader in one
* afternoon. It is what they copy out of the feature card, which is why the
* card writes `gene ID:` beside it rather than leaving it as one unlabelled
* part of a joined string. And it is what tells the two apart when a file gives
* a gene both: `name` is the symbol and this is the accession.
*
* `''` for a format with no such notion (a BED name is the only name there is)
* and for a gene the file names but does not identify. **`name === id` means
* the file gave no symbol** — the display name has fallen back to the
* accession — and that is the one convention worth stating here, because it is
* what stops the feature card printing the same string on two lines.
*/
id: string[]
/**
* The attributes worth showing, joined with {@link DETAIL_SEP}.
*
* One string per feature rather than an array per feature, because there are
* millions of them and a million small arrays is a million allocations for
* text that is read one row at a time. Anything that wants the parts back
* splits on the separator — which is why the separator is a constant both
* sides import rather than a punctuation mark each side spells out.
*/
detail: string[]
/** `exonAt[i] .. exonAt[i + 1]` indexes the two arrays below. Length `n + 1`. */
exonAt: Int32Array
exonStart: Int32Array
exonEnd: Int32Array
/** Coding span (GTF CDS, BED `thickStart`), or `-1` for a feature with none. */
cdsStart: Int32Array
cdsEnd: Int32Array
/** `contigAt[c] .. contigAt[c + 1]` is contig `c`'s run. Length `contigs.length + 1`. */
contigAt: Int32Array
/**
* The longest feature on each contig.
*
* The rows are sorted by *start*, so a feature that begins before the window
* can still end inside it, and a binary search for `start >= lo` would miss
* exactly the long genes a reader is most likely to be looking at. Backing the
* search off by this much is what makes the query correct, and keeping it per
* contig is what stops one megabase-long feature from making every query on
* every other contig scan from its start.
*/
maxSpan: Int32Array
/**
* The categories a barChart draws, or absent on every other format (§8.17).
*
* Optional on the set rather than a `FeatureSet` of its own, for §8.14's
* reason restated: `payload.kind === 'features'` is tested at about two dozen
* sites across the browser, the map lanes, the search box, the export and the
* analysis inputs, and a second feature shape does not *break* any of them —
* it makes every one of them skip the lane, silently, to be found one call
* site at a time. A barChart row is an interval with a name, a score and a
* strand like every other row here; what it has besides is a vector, and a
* vector is one field.
*/
bars?: BarChartBars
/**
* The point in space each bead sits at, or absent on every other format (§25).
*
* Optional on the set rather than a `FeatureSet` of its own, for exactly the
* reason {@link bars} is: a second feature shape breaks none of the two dozen
* sites that test `payload.kind === 'features'` and makes every one of them
* skip the lane, silently. A bead is an interval with a name, a score and a
* strand like every other row here; what it has besides is a place, and a
* place is one field.
*/
model?: StructureModel
}
/**
* Where every bead of a structure is, laid out against the set's own sort (§25).
*
* Three parallel typed arrays rather than one interleaved `xyz`, because the
* consumers read one axis at a time — a projection multiplies a column, a
* centroid sums one — and because a renderer's instance buffer wants exactly
* this shape. `hap` is an index into {@link haplotypes} so a phased file's two
* beads at one locus can be told apart without a string compare per bead.
*
* Coordinates are **the file's own**, in whatever units the solver wrote; the
* scene normalises for the camera and keeps the factor so a distance can be
* printed in the units the file had (`structure/scene.ts`). Nothing here
* claims a nanometre.
*/
export interface StructureModel {
x: Float32Array
y: Float32Array
z: Float32Array
/** Index into {@link haplotypes}, per bead. */
hap: Int8Array
/**
* The category names the file used — `shared` alone for an unphased one,
* `maternal` and `paternal` for a phased one, or whatever `g3dtools` was
* given (`cell-1`, `t0`). In first-seen order.
*/
haplotypes: string[]
/**
* The spacing between consecutive beads, in base pairs: the file's
* resolution, inferred from its sorted starts and printed on the row. It is
* the width every bead's interval has, and it is independent of whatever bin
* size the map beside it happens to be at.
*/
width: number
}
/**
* One value per category per feature, and what the categories are (§8.17).
*
* The values are **one flat array with a stride**, not an array per feature, for
* the reason {@link FeatureSet} is a struct of arrays at all: a GTEx barChart is
* twenty thousand genes over fifty-four tissues, and a million small arrays is a
* million allocations to draw a picture with at most a few hundred glyphs in it.
* Feature `i`'s value for category `c` is `values[i * labels.length + c]`.
*
* `NaN` is *no value*, and it is drawn as no bar rather than as a zero bar. A
* row whose `expCount` disagrees with the file's is the case that produces one
* — the format puts the count on every row and the categories are named once
* for the file, so a row that disagrees is a row this cannot place — and it
* keeps its interval, because the interval is not the part that was wrong.
*/
export interface BarChartBars {
/** One label per category, in column order. Never empty. */
labels: string[]
/**
* `true` when the labels are the file's own.
*
* A barChart carries its categories' *values* and not their *names*: the
* names live in the `track` line of a custom track, and in the trackDb stanza
* of a hub — which a bigBarChart dropped on its own does not come with. So
* they may be positions, `1` to `N`, and this is what says so. Nothing is
* inferred from it but the honesty of the legend and of the one colouring
* that reads a category name (`top`, in the parser) — a ramp over the order
* the categories happen to be numbered in is §8.12's own counter-example.
*/
named: boolean
/**
* A colour per category, from `barChartColors`, or empty where the file names
* none — in which case `CATEGORY_PALETTE` supplies them by position.
*/
colours: string[]
/** `values[i * labels.length + c]`; `NaN` where the row carried none. */
values: Float32Array
/**
* The height the tallest bar is drawn at: `maxLimit` where the file sets one,
* otherwise the largest value in the file.
*
* Fitted to the **file** and never to the window, which is §8.16's rule for a
* numeric colouring and holds here for a stronger reason: a bar is read
* against its neighbours *and* against the same gene two windows away, and a
* ceiling that moved on every pan would make one tissue's expression double
* by scrolling. A track's autoscale is safe because its axis is redrawn
* beside it; a glyph carries no axis.
*/
max: number
/** `barChartUnit` — what the numbers are (`TPM`, `RPKM`), or `''`. */
unit: string
/** `barChartLabel` — what the categories are (`Tissues`), or `''`. */
label: string
/** `barChartMetric` — how they were summarised (`Median`), or `''`. */
metric: string
/**
* How many rows kept their interval and lost their chart.
*
* Counted at parse time and carried, rather than scanned for later, because
* it is a fact about the *read*: afterwards a row that carried no vector and
* a row whose vector was all `NaN` are the same array. It is reported under
* the lane's label for §8.7's reason — a file that says what it dropped is
* usable, one that does not is a lie about the genome.
*/
unplaced: number
}
/**
* Two intervals per row — a bedpe, and the only thing that is one.
*
* Not indexed, and deliberately: a loop call set is thousands of rows, not
* millions, and the drawing pass culls by the visible window in one linear scan
* that costs less than the binary searches it would replace. If a caller ever
* arrives with a million pairs, this is the place to add the index, not the
* place to have pre-emptively added it.
*/
export interface PairSet {
contigs: string[]
length: number
contig1: Int32Array
start1: Int32Array
end1: Int32Array
contig2: Int32Array
start2: Int32Array
end2: Int32Array
/** `NaN` where the file carries no score column. */
score: Float32Array
name: string[]
detail: string[]
}
/** A parsed text annotation, as it crosses the worker boundary. */
export type AnnotationPayload =
| { kind: 'features'; data: FeatureSet }
| { kind: 'pairs'; data: PairSet }
// ---------------------------------------------------------------------------
// bigWig samples
// ---------------------------------------------------------------------------
/**
* One track's values over one requested region, at whatever resolution the
* file's own zoom ladder supplied.
*
* `min`/`max` are present only for a summary level; at base resolution the
* value *is* the extremum and duplicating it would be two more megabytes to
* carry the same numbers. A drawing that wants a min/max band asks whether they
* are there.
*/
export interface TrackSamples {
/** The region this covers, echoed back so a late reply can be discarded. */
contig: string
start: number
end: number
/** Interval bounds, 0-based half-open, ascending and non-overlapping. */
starts: Int32Array
ends: Int32Array
values: Float32Array
min: Float32Array | null
max: Float32Array | null
/**
* The share of {@link values} that disagrees with a reference, or `null`.
*
* Only an alignment lane ever carries this, and only where a FASTA covering
* the window is open in the same session and the bins are narrow enough for
* "there is a variant here" to name a place (§8.13). Same units and same
* intervals as `values`, so the ratio of the two is the non-reference
* fraction.
*
* `null` rather than zeros wherever there was nothing to compare against:
* "these reads agree with the reference" and "no reference was loaded" are
* different claims, and an array of zeros would draw them alike.
*/
variant: Float32Array | null
/**
* The introns the window's reads skipped, with how many reads skipped each —
* a sashimi plot's arcs, counted from the reads behind the depth (§8.23).
*
* Only an alignment lane carries this, and it is counted on the depth's own
* pass under the depth's own filter, so an arc never stands on reads the
* curve under it did not count. Absent on every other kind; empty arrays for
* a window of reads with no `N` in any of them, which is a DNA library.
*/
junctions?: SampleJunctions
/** `true` when these came from a zoom level rather than from base resolution. */
summary: boolean
}
/**
* Introns as four parallel arrays, sorted by start: the shape a window's
* spliced reads are tallied into (§8.23). `strands` is `+1`, `-1` or `0` where
* the aligner wrote no `XS`.
*/
export interface SampleJunctions {
starts: Int32Array
ends: Int32Array
counts: Float32Array
strands: Int8Array
}
// ---------------------------------------------------------------------------
// FASTA residues
// ---------------------------------------------------------------------------
/**
* The bases of one window, as they crossed the worker boundary.
*
* One ASCII byte per residue, exactly as the file wrote them — case included,
* since a soft-masked genome says something by writing its repeats in lower
* case. Never a whole sequence: this is only ever asked for at a zoom where
* single bases are drawable, which is a few thousand of them.
*/
export interface BaseRegion {
contig: string
/** 0-based, half-open, as every window here is. */
start: number
end: number
bases: Uint8Array
}
/** Everything one `sample` request answers, plus what it cost to answer it. */
export interface TrackSampleSet {
/** Bases per pixel the samples were chosen for; the cache key. */
basesPerPixel: number
regions: TrackSamples[]
/** Value extent across every region returned, ignoring NaN. `null` if empty. */
extent: { min: number; max: number } | null
/**
* Why there is nothing here, when there is nothing here.
*
* Every other kind answers this question by not being asked it: a bigWig has
* zoom levels all the way out, a FASTA is held whole, a computed vector spans
* whatever it was computed over. An alignment file has none of those, so a
* window can be *legitimately unanswerable* — too many bytes to read at this
* width — and an empty lane and a lane over a region with no reads in it
* would otherwise look identical (§8.13).
*
* A sentence for the reader, not a code: it goes under the lane's name, and
* the doctrine at §10 is that a refusal names the actual state and the
* concrete fix. Absent means the answer is simply the answer.
*/
note?: string
/**
* An alignment lane drawn as a fraction modified (§8.13): the modification
* the values are the percentage of — the one asked for, or the one the
* window held most modified calls of — and every code the window's reads carried, so
* the lane can name what it draws and the settings card can offer the rest.
*/
mods?: { code: string; codes: string[] }
}
// ---------------------------------------------------------------------------
// Presentation
// ---------------------------------------------------------------------------
/** Which edge (or edges) an annotation is drawn on. Any number may be true. */
export type Placement = Record<LaneEdge, boolean>
/** Nothing drawn on any edge — the base every placement is built from. */
export function emptyPlacement(): Placement {
return { top: false, right: false, bottom: false, left: false }
}
/** A placement drawn on exactly one edge. */
export function placementOn(edge: LaneEdge): Placement {
return { ...emptyPlacement(), [edge]: true }
}
/**
* A stored placement, made whole.
*
* A session written before the top and left stacks existed carries two of the
* four keys, and `undefined` is falsy enough that nothing would visibly break —
* but a record with missing keys is one every `Object.entries` over it counts
* wrong, and this is the one door those values come in through.
*/
export function normalisePlacement(placement: Partial<Placement> | undefined): Placement {
const base = emptyPlacement()
if (placement === undefined) return base
for (const edge of LANE_EDGES) base[edge] = placement[edge] === true
return base
}
/**
* How a value profile is resolved into ink.
*
* The first three draw the value as a *position* across the lane and are the
* three a genome browser has always had. `points` is a position too, as a dot
* per value with nothing drawn between them — a Manhattan plot's picture, for a
* track whose values are not a continuous curve: p-values, scores over sparse
* bins, anything whose outliers are the reading. `heatmap` draws it as a **colour**
* instead, filling the lane's whole thickness, which trades the precision of a
* position for the one thing a position cannot do: it costs no height. A track
* that is unreadable at 20 pixels as a line is perfectly readable at 20 pixels
* as a strip of colour, so a stack of them fits on a screen — which is what
* multi-scale insulation needs and what `laneBands` divides a lane for.
*
* `ribbon` is the fifth and the only one that is not a statement about *this*
* track alone: it fills the band between its own curve and the curve of the
* track below it in the same lane, so a set of them draws as one nested shape.
* That is the scalogram's picture (§18.12) and it is not expressible as four
* areas — four filled areas in one lane paint over each other in the order they
* were drawn, and the reader sees the topmost one and four edges. See
* {@link nestsAsRibbons}.
*/
export type TrackMode = 'line' | 'area' | 'bar' | 'points' | 'heatmap' | 'ribbon'
/** Every mode, in the order the picker offers them. */
export const TRACK_MODES: readonly TrackMode[] = ['area', 'line', 'bar', 'points', 'heatmap', 'ribbon']
/** A clip on the value axis. `auto` recomputes from what is in view. */
export interface Limits {
auto: boolean
min: number
max: number
}
/**
* How a called set of segments is drawn on the map: its **look** (§18.7).
*
* Two families, because the two things a task calls along the diagonal are not
* the same kind of object. A **point** set — boundaries, borders, sites, peaks —
* is a list of places, drawn as a `point` on the diagonal or as a `border`, the
* short marker across it; which of the two is the reader's choice. An **extent**
* set — domains, compartments, segments — is drawn as the `corner` of the
* square each one occupies: its top edge and its right edge, meeting off the
* diagonal, so consecutive domains read as the run of corners along it that
* every domain figure draws.
*
* There was a fourth, `element`, the whole square; a stored one is read as a
* corner ({@link overlayShapeOf}), which is the same square drawn once rather
* than on both sides of the diagonal it is symmetric about.
*/
export type OverlayShape = 'point' | 'border' | 'corner'
/** The looks a point set is offered on the map, in the order the dropdown lists them. */
export const POINT_LOOKS: readonly OverlayShape[] = ['point', 'border']
/**
* A row's overlay look, read through what an older build may have stored.
*
* Untrusted like every stored style: `element` — the box this replaced — and
* anything else a hand-edited session carries are read as a corner, the look
* every extent set is drawn in.
*/
export function overlayShapeOf(style: { shape?: unknown }): OverlayShape {
const shape = style.shape
return shape === 'point' || shape === 'border' ? shape : 'corner'
}
/**
* Everything the settings row for one annotation controls.
*
* One flat record for all four kinds rather than a discriminated union per kind:
* the fields that do not apply are simply not offered by the card, the store
* stays a plain serialisable object, and adding a control to one kind does not
* mean widening a union that three other call sites switch on.
*/
/**
* What a one-signed heatmap lane draws with when nobody has chosen.
*
* `viridis` rather than the map's own `afmhot_r`: a lane is a thin strip beside
* a picture that is already `afmhot_r`, and two different quantities in one
* palette read as one. It is also the one this repository already names as
* colour-vision-safe.
*/
export const DEFAULT_HEATMAP_LANE_COLORMAP = 'viridis'
/** The axis a track's values are drawn on. See {@link AnnotationStyle.scale}. */
export type TrackScale = 'linear' | 'log2'
/** Every scale, in the order the picker offers them. */
export const TRACK_SCALES: readonly TrackScale[] = ['linear', 'log2']
/**
* The colormap a heatmap lane draws with: the reader's, or the data's own.
*
* Diverging when the values straddle zero, sequential when they do not. See
* {@link AnnotationStyle.colormap}.
*/
export function colormapOf(
style: { colormap?: string },
limits: { min: number; max: number },
): string {
if (typeof style.colormap === 'string' && style.colormap !== '') return style.colormap
return limits.min < 0 && limits.max > 0 ? DEFAULT_DIVERGING_COLORMAP : DEFAULT_HEATMAP_LANE_COLORMAP
}
/**
* Whether the tracks sharing a lane stack inside it instead of overlaying.
*
* **Heatmaps stack, they do not overlay.** Two translucent lines in one lane are
* legible because ink adds and a reader can follow either; two translucent
* colour maps are a third colour that means neither. So a lane whose members all
* draw as heatmaps divides its own thickness between them — the height the
* reader set stays the height of the *group*, which is what makes a four-scale
* insulation set cost one lane and not four — and every other mode goes on
* sharing the lane as it always did.
*
* All of them, not most: one line among three heatmaps has nowhere to be drawn
* except across the whole lane, over the colours, and dividing the lane under it
* would leave the line a fraction of the height its axis is labelled for. A
* mixed lane overlays, and the reader who wanted the stack sets the odd one out.
*
* Here rather than in either painter because both draw it, and a rule about what
* a lane *is* that lives in one renderer is a rule the other one drifts from —
* which is exactly what the group modes did before `annotations/groups.ts`.
*/
export function stacksAsHeatmaps(members: ReadonlyArray<{ style: { mode: TrackMode } }>): boolean {
return members.length > 1 && members.every((member) => member.style.mode === 'heatmap')
}
/**
* Whether the tracks sharing a lane are the *edges* of one nested picture.
*
* **Ribbons tile, they do not overlay.** A ribbon is not a track that happens to
* be filled: it is the band between its own curve and the curve of the member
* below it, so a lane of them is one picture cut into strips rather than several
* pictures drawn over each other. The scalogram is the case it exists for — the
* separations at which each bin's contacts reach a tenth, a quarter, a half, and
* the three quarters and nine tenths above them — and what a reader is meant to
* see is a single shaded shape rising and falling along the genome, not five
* lines to be matched against a legend.
*
* Two consequences, and both are what the heatmap rule already establishes for
* its own case. The members are **not dimmed**, because they do not overlap and
* dimming is what makes two overlapping tracks both visible. And the **order
* matters**: each fills down to the one before it, so a lane whose members have
* been reordered draws a different picture, which is correct — the reader has
* said the bands go in a different order.
*
* All of them, not most, exactly as the heatmap rule insists: one line among
* four ribbons has no band to be the top of, and a mixed lane therefore overlays
* as any other does.
*/
export function nestsAsRibbons(members: ReadonlyArray<{ style: { mode: TrackMode } }>): boolean {
return members.length > 1 && members.every((member) => member.style.mode === 'ribbon')
}
/** This lane's scale, or the linear one every lane starts on. */
export function scaleOf(style: { scale?: TrackScale }): TrackScale {
return style.scale === 'log2' ? 'log2' : 'linear'
}
/** What this lane's values are measured in, where it is a length. */
export function unitOf(style: { unit?: unknown }): 'bp' | undefined {
return style.unit === 'bp' ? 'bp' : undefined
}
/**
* A value as the axis draws it.
*
* `NaN` for anything a logarithm cannot answer for, which the renderer already
* knows to leave as a hole. See {@link AnnotationStyle.scale}.
*/
export function onScale(value: number, scale: TrackScale): number {
if (scale !== 'log2') return value
return value > 0 ? Math.log2(value) : Number.NaN
}
export interface AnnotationStyle {
/**
* The controls only an alignment lane has (§8.13).
*
* Optional, so nothing else changes shape — see {@link ReadStyle}.
*/
reads?: ReadStyle
/**
* The controls only a lane of intervals has (§8.16).
*
* Optional for the reason `reads` is, and a type-only import for a reason of
* its own: `colourBy` is a key of the file rather than a member of a closed
* set, so the module that can say what one means is `annotations/fields.ts`,
* and that module reads this one. See {@link FeatureStyle}.
*/
features?: FeatureStyle
/**
* The controls only a 3D model has (§25).
*
* Optional for the reason `reads` and `features` are: a representation and a
* bead radius mean nothing on a bedGraph, and being optional means no
* existing kind, no `defaultStyle` arm and no stored row changes shape.
*/
structure?: StructureStyle
/**
* The six reading frames under a sequence lane, and the code they are read
* with (§8.8).
*
* Optional for the reason the three above are; {@link translationOf} is the
* one reader.
*/
translation?: TranslationStyle
colour: string
/** Lane thickness in CSS pixels — the height of a bottom lane, width of a right one. */
size: number
mode: TrackMode
limits: Limits
/**
* Autoscale group. Tracks sharing a non-empty name resolve to one pair of
* limits over the union of what they show, which is the only way two tracks
* can be compared by eye. `''` means this track scales alone.
*/
group: string
/**
* How a called set of segments is drawn on the map; read through
* {@link overlayShapeOf}, since a stored one may predate the looks.
*/
shape: OverlayShape
/** Radius of an off-diagonal pair circle, in CSS pixels. */
radius: number
opacity: number
/**
* The axis this lane's values are drawn on.
*
* **A display scale and nothing else** — the numbers behind the lane, and
* every number that leaves it as a file, stay exactly as they were read or
* computed. What changes is where a value lands between the two limits, which
* is why the tick labels come out in the same units the axis is in and need no
* separate handling.
*
* `log2` exists for the one lane that is systematically unreadable without it:
* a ratio. On a linear axis "half" occupies the bottom half-unit and "double"
* a whole unit above the middle — the same finding at two very different
* sizes, with depletion squashed into a sliver against the floor. The
* logarithm makes the two symmetric and puts *no change* on the baseline the
* plot already draws.
*
* A value at or below zero has no logarithm and comes back as a **gap**, which
* is the same answer this app gives everywhere a log would be an infinity: a
* `-Infinity` cannot be drawn between two limits, would take the axis of the
* whole lane with it, and is not a reading. It is therefore the wrong scale
* for a difference track, which crosses zero on purpose, and the right one for
* a ratio, which cannot.
*
* Optional so that nothing else changes shape and a stored session written by
* an older build restores unchanged — {@link scaleOf} is the one reader.
*/
scale?: TrackScale
/**
* What the values are measured in, where the lane should spell it: `bp` on
* a scalogram's lane, whose `log2` axis is then ruled at 1 kb, 10 kb, 100 kb
* and labelled in distances rather than in the logarithm (§18.12). Absent is
* a plain number. {@link unitOf} is the one reader.
*/
unit?: 'bp'
/**
* The colormap a `heatmap` lane is drawn with.
*
* Absent means *chosen from the data*, which is the right answer far more
* often than a picker is: a track whose values cross zero is a departure from
* something and wants a diverging map with its middle on the zero, and a
* one-signed one wants a ramp. That is the same reading `resolveLimits`
* already makes about the axis, so a lane's colours and its limits agree
* without either being told about the other.
*
* Read only by a heatmap lane, and the control that sets it appears only when
* the style is one — a colormap on a line track is a setting that does
* nothing, which is worse than an absent one.
*/
colormap?: string
/**
* Draw the value axis upside down, floor at the top.
*
* For the half of an experiment that is measured downwards. A stranded
* RNA-seq pair is the case that asks for it: the forward strand read up from
* a shared baseline and the reverse read *down* from it is one picture of one
* locus, and two tracks both growing upwards is two pictures the reader has to
* mirror in their head. It is a property of the track rather than of the group
* because only one member of the pair is flipped.
*/
flip: boolean
/**
* The value a signal lane grows from — IGV's *center* — where it is not zero.
*
* Bars and areas grow up from it where the value is above and down where it
* is below, and the hairline that marks a signed track's origin is drawn at
* it: `1` on a ratio of two tracks is the line where one is over the other.
* Absent is zero, the lane's origin as it always was; a centre outside the
* limits in force grows from the near edge, as zero outside them does.
*/
centre?: number
}
/**
* The reading frames a sequence lane can draw under its residues (§8.8).
*
* Off by default: six rows of codons are a question about a gene, and a lane
* that answered it unasked would spend eighty pixels of every genome's stack on
* it. The code is NCBI's number (`annotations/codons.ts`) and is remembered with
* the switch off, so turning the frames back on in a Mycoplasma genome does not
* quietly go back to reading `TGA` as a stop.
*/
export interface TranslationStyle {
frames: boolean
code: number
}
/** A lane's frames setting, whether or not its style carries one. */
export function translationOf(style: AnnotationStyle): TranslationStyle {
return style.translation ?? { frames: false, code: DEFAULT_GENETIC_CODE }
}
/**
* Read a stored frames setting back: untrusted, like every stored style, so a
* table NCBI does not publish is read as the standard one rather than handed to
* a lane that would index an alphabet it does not have.
*/
export function translationStyleOf(value: unknown): TranslationStyle {
if (typeof value !== 'object' || value === null) {
return { frames: false, code: DEFAULT_GENETIC_CODE }
}
const row = value as Record<string, unknown>
const code = row['code']
return {
frames: row['frames'] === true,
code: isGeneticCode(code) ? code : DEFAULT_GENETIC_CODE,
}
}
/**
* The height one lane of each kind opens at, in pixels.
*
* A lane's height is what its contents are *read* at rather than what fits the
* most of them on a screen, and a stack that then runs off the bottom is what
* "fit tracks into the space" is for.
*
* Lifted out of {@link defaultStyle} when the seventh arm went in: a ternary
* chain nine deep is one expression where this is nine independent decisions,
* and each of them is worth a sentence.
*/
function defaultSize(kind: AnnotationKind): number {
// A coverage profile at 44 pixels is a smear with a skyline, and the peaks
// that matter are three pixels apart. Ninety is about where a yeast ATAC
// track starts showing the shape of a promoter.
if (kind === 'track') return 90
if (kind === 'gtf') return 92
if (kind === 'sequence') return 46
// A metadata profile is read at a glance beside the map rather than studied,
// and there are usually several of them: 56 pixels is enough to tell one
// MAG's contamination from its neighbour's without spending a quarter of the
// picture on it. A categorical strip needs only enough to see the colour and,
// where a run is wide, to write the category in it.
if (kind === 'metadata') return 56
if (kind === 'classes') return 20
// A chart needs height to *be* a chart: at sixteen pixels the tallest bar is
// sixteen and every other one is rounding. Sixty is about three packed rows
// of a legible chart, or one generous one (§8.17).
if (kind === 'barchart') return 60
// An ideogram is one shape and the names of its bands inside it: tall enough
// for a band name in a chromosome drawn with room either side, and for the
// two lines of its label — its name and the bands under the window — in the
// browser's column; no taller, since it is context rather than a
// measurement (§8.22).
if (kind === 'ideogram') return 40
// A sashimi arc is read by its height and its count, and a lane of them has
// two strands to hold, one above the baseline and one below: seventy is room
// for both and a count at every apex (§8.23).
if (kind === 'junctions') return 72
// A depth lane is a bigWig's picture and takes a bigWig's height. The pile
// beside it is a stack of rows, and 180 is about a dozen of them — enough
// that a heterozygous site shows two alleles rather than a coin flip.
if (kind === 'alignments') return 90
if (kind === 'reads') return 180
// Everything else is a row of glyphs against the genome: a peak set, a BED, a
// variant track, a loop set, and a structure's beads as boxes — the same
// picture a peak set is, with a phased file two rows of them. The model
// itself is drawn in a cell of the deck, which has no height here to speak of
// (§25).
//
// Twenty-four rather than sixteen, and the eight pixels buy the lane its own
// label. A name is written across the lane it names, and the rule that keeps a
// click target off the resize border (`laneLabel.clearOfGrip`) fails on a
// sixteen-pixel lane — so `ATAC peaks` was drawn through the calls it named
// and had no box to click, leaving the swatch as the only thing on the lane a
// reader could reach. §8.16's sixteen still holds for a lane dragged short;
// this is only where one opens.
return 24
}
/**
* The defaults a freshly dropped file starts with.
*/
export function defaultStyle(kind: AnnotationKind, colour: string): AnnotationStyle {
return {
colour,
size: defaultSize(kind),
// Bars, because one cell is one unit: a metadata column has a value per
// genome, per contig or per bin and nothing in between, so a line drawn
// through the tops of them would interpolate between two MAGs that have no
// ground between them to interpolate over.
mode: kind === 'sequence' ? 'line' : kind === 'metadata' ? 'bar' : 'area',
// GC content is a fraction, and a fraction has an axis whether or not this
// window happens to use all of it. Autoscaling it would make the same
// sequence look different in two views and put "0.41 – 0.44" under a line
// that is really flat — so a sequence lane starts pinned to the whole of it.
limits: kind === 'sequence' ? { auto: false, min: 0, max: 1 } : { auto: true, min: 0, max: 1 },
group: '',
shape: 'corner',
radius: 7,
opacity: kind === 'pairs' ? 0.9 : 1,
flip: false,
// Present only where it means something, which is the whole point of it
// being optional — see {@link ReadStyle}. `readsOf` supplies the same
// defaults for a lane whose stored style predates this field.
...(readsAlignments(kind) ? { reads: defaultReadStyle() } : {}),
// …and the same for a model, whose accessor is {@link structureOf}.
...(isStructure(kind) ? { structure: defaultStructureStyle() } : {}),
}
}
/**
* The value axis a file of this format opens on, where the format has one of
* its own — `undefined` for every format whose kind's default is right for it.
*
* One has, and it is the one whose values have a scale before a byte of them is
* read: a bedMethyl's are percentages, so its lane opens clipped to **0–100**
* where every other track opens on the autoscale (§8.20). A methylation lane
* autoscaled to 12–94 % says nothing about how methylated anything is, and the
* autoscale every track has is one press away. The kind cannot say this — a
* bedMethyl is a `track` exactly as a bigWig is, which is the point of it being
* one — so {@link defaultStyle} cannot either: only the bytes can, and the
* loader asks this when a freshly imported row lands.
*/
export function openingLimits(format: DetectedFormat): Limits | undefined {
return format === 'bedmethyl' ? { auto: false, min: 0, max: 100 } : undefined
}
// ---------------------------------------------------------------------------
// The structure's controls (§25)
// ---------------------------------------------------------------------------
/** How the beads of a model are drawn. */
/**
* A representation a model was stored with before the three were toggles
* (§25.3): read back into them by {@link structureStyleOf}, written by nothing.
*/
export type StructureRepresentation = 'beads' | 'sticks' | 'beads+sticks' | 'tube'
/**
* What the beads outside the deck's window are drawn as (§25).
*
* `ghost` is the default and the one that keeps the model readable as a whole
* while the map is zoomed into a domain: the domain is lit, the rest is there
* to be seen through. `hide` is for a figure of one region; `show` is the model
* with nothing about the window said on it.
*/
export type StructureOthers = 'ghost' | 'hide' | 'show'
export const STRUCTURE_OTHERS: readonly StructureOthers[] = ['ghost', 'hide', 'show']
/** Whether a missing bead breaks the tube, or is bridged by a dashed run. */
export type StructureGaps = 'break' | 'dashed'
/** The two projections a figure may be drawn in. */
export type StructureProjection = 'perspective' | 'orthographic'
/**
* The built-in colourings a model offers before it asks any other row (§25).
*
* `sequence` is one hue per contig, `haplotype` one lightness per homologue,
* `position` a ramp along each chain. Every
* other value of {@link StructureStyle.colourBy} is a key of another row —
* `track:<id>`, `features:<id>` — or one of the two the pin makes
* (`distance`, `contacts`), resolved where the row is, in
* `structure/colour.ts`.
*/
export const STRUCTURE_COLOUR_SEQUENCE = 'sequence'
export const STRUCTURE_COLOUR_HAPLOTYPE = 'haplotype'
export const STRUCTURE_COLOUR_POSITION = 'position'
export const STRUCTURE_COLOUR_DISTANCE = 'distance'
export const STRUCTURE_COLOUR_CONTACTS = 'contacts'
/** The reader's own marks: a line colours its bead, a rectangle its run (§25.5). */
export const STRUCTURE_COLOUR_MARKS = 'marks'
/**
* Each bead's distance to its bead in the **compared** model (§25.5).
*
* Here with its siblings rather than in `panels/StructurePanel.ts`, where it
* used to live: `structure/colour.ts` has to know which colourings are ramps,
* and a constant it could only reach through the panel would have made the
* panel and its own colour module import each other.
*/
export const STRUCTURE_COLOUR_DISPLACEMENT = 'displacement'
/** `track:<row id>` — a loaded track's values at each bead, on the lane's limits. */
export const STRUCTURE_COLOUR_TRACK = 'track:'
/** `features:<row id>` — a feature set's colour where a bead overlaps one. */
export const STRUCTURE_COLOUR_FEATURES = 'features:'
/**
* `names:<row id>` — a feature set's **names** as categories (§25.5): each bead
* the colour of the name over it, `A` and `B` on a compartment call. The
* colours are the set's own (`render/nameColours.ts`), kept on its row.
*/
export const STRUCTURE_COLOUR_NAMES = 'names:'
/**
* Everything the structure cell's own menu controls.
*
* A nest on {@link AnnotationStyle} rather than fields of it, so that nothing
* about the other kinds changes shape, and so that the whole of it is stored
* and restored wholesale with the row — a saved workspace brings the model back
* as the reader left it, tube and colouring and all.
*/
export interface StructureStyle {
/** See the `STRUCTURE_COLOUR_*` constants and `structure/colour.ts`. */
/**
* What the model is drawn as — any of the three together (§25.3): beads
* at each place, sticks between neighbours, a tube through the run. A model
* with none of them is its gaps, chords and halos alone, which is a choice.
*/
beads: boolean
sticks: boolean
tube: boolean
colourBy: string
/**
* Bead radius, as a fraction of the typical distance between neighbouring
* beads — so the same setting reads the same on a model in nanometres and
* one in solver units.
*/
beadRadius: number
/** Tube and stick radius, on the same scale. */
tubeRadius: number
/**
* How much of a **ghosted** bead is left (§25.5): 0 gone, 1 as drawn. The
* beads inside the window and the sequences not ghosted are always drawn
* whole; this is the one opacity there is.
*/
opacity: number
others: StructureOthers
/** Chain names at each chain's midpoint. */
labels: boolean
/**
* The centre of rotation and the axes through it (§25.5).
*
* On by default: it is where every drag turns about and where ⌘-click puts
* the next one, and a reader who cannot see it is turning the model about a
* point they have to infer from the motion.
*/
axes: boolean
/**
* The axes' own orientation, as a unit quaternion `[x, y, z, w]`.
*
* Identity is the model's own frame, which is what the axes always drew.
* **Replant** writes the camera's frame here instead (§25.5), so the arms
* lie along the screen at the moment it was pressed and every turn after
* that is read against *that* view rather than against a frame the file
* happened to be written in. Kept with the model, not the cell, for the
* reason every other reading of the picture is: it is what the reader set
* the model up to be read against.
*/
axesFrame: [number, number, number, number]
/**
* The one haplotype shown, or `''` for every one.
*
* Both homologues of a phased file are drawn unless a reader solos one from
* the key; the solo is a decision about this model and is kept with it.
*/
haplotype: string
/**
* A pairs row (BEDPE, loops) drawn as chords between its anchors' beads, by
* the row's id; `''` for none (§25.5). Kept in the style rather than the
* cell because it is a fact about how the model reads, like its colouring.
*/
loops: string
/**
* Sequences drawn ghosted whatever the window says (§25.5): the key's
* click. By name, so the choice survives a re-read of the same file.
*/
ghosted: string[]
/**
* A colour the reader chose for a sequence, by name, over the palette's.
*
* Beside it there used to be `marksHidden`, the marks the key of *colour by
* manual annotations* took off this model's colouring. A mark's key row hides
* the mark itself now, on every picture (§8.1, §25.5), so a style stored with
* that list reads without it and the model is coloured by every drawn mark.
*/
sequenceColours: Record<string, string>
/**
* How deep the fog sits, 0 (none) to 1 (the far side gone): space + scroll
* in the cell, or the slider. 0.6 is the depth cue the cell always had.
*/
fog: number
/** Whether the fog is drawn at all; {@link fog} keeps its depth while it is off. */
fogOn: boolean
/** Whether the chords of {@link loops} are drawn; the choice of set is kept while they are off. */
chords: boolean
/**
* The depth slab: 1 shows everything, smaller keeps only what lies within
* that fraction of the model's radius of the centre of rotation, front and
* back — a way into a dense globule.
*/
slab: number
/** A genomic ruler along each chain: a tick every nice step, labelled. */
ruler: boolean
/**
* The ramp a continuous colouring is drawn on, or `''` for each one's own.
*
* `''` rather than `'viridis'` as the default, because the default is not one
* ramp: a track's beads are drawn on **the lane's** colormap so that a bead
* and the lane beside it cannot disagree at the same value (§25.5), and only
* the colourings with no scale of their own fall back to `STRUCTURE_RAMP`. A
* name here overrides both, which is the reader asking for it.
*
* Nothing for a colouring by sequence, haplotype or a category: those are
* levels, not a ramp, and the control is not shown for them.
*/
colormap: string
/**
* A yellow bead on every **telomere** — the two ends of each chain (§25.5).
*
* Drawn through the same channel the reader's own marks are, so a telomere
* stands out of the fibre at the same size whatever the model is drawn as and
* whatever it is coloured by, and so the vector export carries them without a
* second road. What counts as one is `telomereBeads`: the first and last
* placed bead of every chain, with nothing asked of the session. It used to
* spare a sequence ticked circular and claim a far end only against a known
* length, which on a whole yeast genome marked five of thirty-two ends.
*/
telomeres: boolean
/**
* Another structure row superposed on this one (§25.5), by id; `''` for
* none. Aligned by the beads the two share, drawn beside this model in its
* frame, and its distance from this model reported.
*/
compare: string
/**
* Another structure row this model is turned **into the frame of** (§25.5),
* by id; `''` for none.
*
* {@link compare}'s other half, and the half a reader asking *is this the
* same fold* usually wants: the same superposition, applied to **this**
* model's own beads, with the other model **not drawn**. Two cells side by
* side, each aligned to the same row, are two models in one frame — which
* is what makes turning them together (§25.3) show the same face of each.
*/
align: string
/**
* The model reflected through the plane across the screen (`mirrorX`) or up
* it (`mirrorY`) — §25.5.
*
* A reconstruction from contacts is determined **only up to reflection**: a
* model and its mirror image fit the same counts exactly as well, and which
* one a solver returns is an accident of where it started. So a model that
* will not superpose on another may be the same fold in the other hand, and
* this is how it is brought into that hand. Both at once is a half turn
* rather than a reflection, which is the one combination that changes
* nothing about the fold; it is allowed because the two are about what the
* reader sees, and seeing a model the other way up is a reason on its own.
*/
mirrorX: boolean
mirrorY: boolean
/**
* The bead the reader pinned (§25.5), as the sequence and base pair at its
* middle — `chr14:89599000` — or `''` for none.
*
* What *spatial distance to pinned bin* and *contacts with pinned bin* colour
* from. It was the cell's alone and went with the page, so a model saved
* coloured by distance came back coloured from nothing; kept with the model,
* as the colouring that reads it is, by position rather than by bead index,
* so it still names the same place on a model of that window at another
* resolution.
*/
pin: string
}
export function defaultStructureStyle(): StructureStyle {
return {
beads: false,
sticks: false,
tube: true,
colourBy: STRUCTURE_COLOUR_SEQUENCE,
beadRadius: 0.35,
tubeRadius: 0.2,
opacity: 0.1,
others: 'ghost',
labels: true,
axes: true,
axesFrame: [0, 0, 0, 1],
haplotype: '',
loops: '',
ghosted: [],
sequenceColours: {},
fog: 0.6,
fogOn: true,
chords: true,
slab: 1,
ruler: false,
telomeres: false,
colormap: '',
compare: '',
align: '',
mirrorX: false,
mirrorY: false,
pin: '',
}
}
/**
* One row's structure controls, whether or not its style carries any.
*
* The accessor every reader goes through, for {@link readsOf}'s reason: a
* session stored before this field existed opens with the defaults rather than
* with `undefined` reaching a renderer.
*/
export function structureOf(style: AnnotationStyle): StructureStyle {
return style.structure ?? defaultStructureStyle()
}
/** A finite number within `[lo, hi]`, or the fallback. */
function bounded(value: unknown, lo: number, hi: number, fallback: number): number {
return typeof value === 'number' && Number.isFinite(value)
? Math.max(lo, Math.min(hi, value))
: fallback
}
/**
* Read a stored structure style back, clamping every field.
*
* Untrusted input, like every stored shape (see {@link readStyleOf}): a
* representation this build has never heard of, a radius of a million, an
* opacity of `-1`, each of which a renderer would faithfully draw.
*/
export function structureStyleOf(value: unknown): StructureStyle {
const fallback = defaultStructureStyle()
if (typeof value !== 'object' || value === null) return fallback
const row = value as Record<string, unknown>
const stored = row['representation']
const representation: StructureRepresentation | undefined =
stored === 'beads' || stored === 'sticks' || stored === 'beads+sticks' || stored === 'tube' ? stored : undefined
const others = row['others']
const colourBy = row['colourBy']
const haplotype = row['haplotype']
// The three toggles, or a representation stored before there were three;
// a word this build has never heard of is the default drawing.
const flag = (key: 'beads' | 'sticks' | 'tube', legacy: boolean): boolean =>
typeof row[key] === 'boolean' ? (row[key] as boolean) : representation !== undefined ? legacy : fallback[key]
return {
beads: flag('beads', representation === 'beads' || representation === 'beads+sticks'),
sticks: flag('sticks', representation === 'sticks' || representation === 'beads+sticks'),
tube: flag('tube', representation === 'tube'),
// `score` — the file's fifth column — was a colouring once and is not now:
// a g3d has no such column worth a ramp, and a session stored with it opens
// by sequence rather than on a row the menu no longer offers.
colourBy:
typeof colourBy === 'string' && colourBy.length <= 200 && colourBy !== 'score' ? colourBy : fallback.colourBy,
beadRadius: bounded(row['beadRadius'], 0.02, 3, fallback.beadRadius),
tubeRadius: bounded(row['tubeRadius'], 0.02, 3, fallback.tubeRadius),
opacity: bounded(row['opacity'], 0, 1, fallback.opacity),
others: STRUCTURE_OTHERS.includes(others as StructureOthers)
? (others as StructureOthers)
: fallback.others,
labels: row['labels'] !== false,
// `!== false`, not `=== true`: the centre is on by default now, and a
// session stored before it was would otherwise open with it off — which
// is the default arriving as its opposite for every reader who has been
// here before.
axes: row['axes'] !== false,
axesFrame: quaternionOf(row['axesFrame'], fallback.axesFrame),
haplotype: typeof haplotype === 'string' && haplotype.length <= 64 ? haplotype : '',
loops: typeof row['loops'] === 'string' && (row['loops'] as string).length <= 200 ? (row['loops'] as string) : '',
ghosted: Array.isArray(row['ghosted'])
? (row['ghosted'] as unknown[]).filter((v): v is string => typeof v === 'string' && v.length <= 200).slice(0, 10000)
: [],
sequenceColours: sequenceColoursOf(row['sequenceColours']),
fog: bounded(row['fog'], 0, 1, fallback.fog),
fogOn: row['fogOn'] !== false,
chords: row['chords'] !== false,
slab: bounded(row['slab'], 0.02, 1, fallback.slab),
ruler: row['ruler'] === true,
telomeres: row['telomeres'] === true,
colormap:
typeof row['colormap'] === 'string' && (row['colormap'] as string).length <= 64
? (row['colormap'] as string)
: '',
compare: typeof row['compare'] === 'string' && (row['compare'] as string).length <= 200 ? (row['compare'] as string) : '',
align: typeof row['align'] === 'string' && (row['align'] as string).length <= 200 ? (row['align'] as string) : '',
mirrorX: row['mirrorX'] === true,
mirrorY: row['mirrorY'] === true,
pin: typeof row['pin'] === 'string' && PIN.test(row['pin'] as string) ? (row['pin'] as string) : '',
}
}
/** A stored pin: a sequence name, a colon, a base pair (see {@link StructureStyle.pin}). */
const PIN = /^[^\s]{1,200}:\d{1,12}$/
/** A pin's sequence and base pair, or `null` for none. */
export function pinOf(pin: string): { contig: string; bp: number } | null {
if (!PIN.test(pin)) return null
const at = pin.lastIndexOf(':')
return { contig: pin.slice(0, at), bp: Number(pin.slice(at + 1)) }
}
/** A bead's place written as a pin: the sequence and the base pair at its middle. */
export function pinAt(contig: string, start: number, end: number): string {
return `${contig}:${Math.round((start + end) / 2)}`
}
/**
* A stored unit quaternion, or the fallback.
*
* Four finite numbers that are not all zero, normalised here rather than
* trusted: a quaternion of the wrong length is a rotation matrix that scales,
* and what it scales is the axes drawn over every model.
*/
function quaternionOf(
value: unknown,
fallback: [number, number, number, number],
): [number, number, number, number] {
if (!Array.isArray(value) || value.length !== 4) return fallback
const parts = value.map((v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0))
const length = Math.hypot(parts[0], parts[1], parts[2], parts[3])
if (!(length > 1e-6)) return fallback
return [parts[0] / length, parts[1] / length, parts[2] / length, parts[3] / length]
}
/** The reader's own sequence colours, kept only where both halves are strings. */
function sequenceColoursOf(value: unknown): Record<string, string> {
if (typeof value !== 'object' || value === null) return {}
const out: Record<string, string> = {}
for (const [name, colour] of Object.entries(value as Record<string, unknown>)) {
if (typeof colour === 'string' && /^#[0-9a-fA-F]{6}$/.test(colour) && name.length <= 200) out[name] = colour
}
return out
}
// ---------------------------------------------------------------------------
// The loaded entry
// ---------------------------------------------------------------------------
/**
* How many of the map's contigs this file actually mentions.
*
* Spec §8.2 makes this non-negotiable and it applies to every format, not only
* to bigWig: a silent zero-overlap — a GTF using `chrI` against a map using `I`
* — is the most likely support question there is, and it presents as "the
* feature does not work" rather than as "these two files disagree about names".
*/
export interface ContigOverlap {
matched: number
/** Contigs in the map. */
total: number
/** Up to a handful of the map's contigs this file never mentions. */
missing: string[]
/** Up to a handful of this file's contigs the map has never heard of. */
extra: string[]
/**
* The **genome layer's** view of the same question (§8.5).
*
* Present only on a file that has one, and it is what makes the report
* readable there. A metagenome is one co-assembly binned into MAGs, so a
* track over it — coverage from the same BAM, an annotation of the same
* contigs — routinely covers a handful of organisms out of fifty, and the
* sequence counts say `412 of 20,566`, which is arithmetically true and reads
* exactly like a file that failed to match. The number a reader wants is how
* many *MAGs* it touches, and which.
*
* `matched` counts genomes with at least one sequence in the file; `total` is
* every genome the map has; `named` is a handful of the matched ones, ready
* to print.
*/
genomes?: { matched: number; total: number; named: string[] }
/**
* Sequences matched across two conventions whose **lengths disagree**.
*
* The one hole the rename (§5.9) would otherwise open in §8.2's promise. A
* track renamed into the map's vocabulary is renamed *before* the overlap is
* counted, so a cross-convention match then reports as a full match and this
* warning goes quiet — which is right when the two are the same sequence and
* a lie when they are not. An hg19 track's `MT` against an hg38 map's `chrM`
* is the case: one name apiece, one rename, and two sequences two bases
* apart. Named here, so the card can say so.
*
* Written as `asked → answered`, ready to print.
*/
contested?: string[]
}
export type AnnotationStatus = 'loading' | 'ready' | 'error' | 'detached'
/**
* One row of one card.
*
* `payload` is present for the text formats, which are parsed once and then
* live here; a bigWig has none, because its data is whatever the current view
* asked for and it is fetched through the worker per view (§8.4).
*
* `detached` is the state a *local* file comes back in after a page reload: the
* settings survived in session storage, the bytes cannot — no browser will
* re-read a dropped file without a fresh gesture — so the row is shown with its
* colours and limits intact and a button to point at the file again.
*/
/**
* What a reference read by range costs to read through (§8.8, §8.21).
*
* Present on a FASTA opened by its `.fai` and on a `.2bit`, and nowhere else:
* the one fact a motif search's estimate needs before **Run** is how many
* bytes the bases it will search are, since a search over such a reference
* reads every one of them (§18.8). The residues across the whole file, for a
* search over every sequence, and what the file spends on one.
*/
export interface SequenceByRange {
bases: number
bytesPerBase: number
}
export interface AnnotationEntry {
id: string
card: AnnotationCard
kind: AnnotationKind
/** What to call it on screen. Starts as the file name, minus its suffix. */
label: string
/**
* Where this came from.
*
* `derived` is the case with no file behind it at all: the 4C profile is
* computed from the block already on screen, so there is nothing to name, to
* re-read after a reload, or to report a contig overlap against.
*/
source: AnnotationSource | { kind: 'detached'; name: string } | { kind: 'derived'; name: string }
/**
* The two tracks this one is computed from, where it is computed from any.
*
* Ids rather than samples, because the operands are ordinary rows with their
* own fetches and their own caches: a derived lane holds no data of its own and
* reads whatever the two it names have in hand at the moment it is drawn
* (`annotations/derived.ts`). Which is also why an operand that is removed
* leaves this row unable to draw rather than wrong — `annotations/store.ts`
* takes the derived row with it.
*/
derivedFrom?: { a: string; b: string; op: TrackOp }
status: AnnotationStatus
/**
* How far the file's read has got while `status === 'loading'`, where the
* worker said: a BAM's index, a FASTA under the line, a text track. Drawn
* under the row as a bar, and gone with the status.
*/
progress?: { done: number; total: number }
/** Set when `status === 'error'`; the message is shown in the row. */
error?: string
overlap?: ContigOverlap
/** One line under the label: "7,071 genes", "5,105 peaks", "16 contigs". */
summary?: string
visible: boolean
placement: Placement
style: AnnotationStyle
payload?: AnnotationPayload
/** Value extent over the whole bigWig, from its header. Seeds a manual clip. */
fileExtent?: { min: number; max: number }
/** A sequence row read by range rather than held: see {@link SequenceByRange}. */
byRange?: SequenceByRange
/**
* How many times this row's values have been replaced under the same id.
*
* A computed track run again is the case (§18.5): the new numbers take the
* old row's id, so that a second run replaces its own lane rather than adding
* one, and every sample cache holds a lane's values by id and window. Without
* a mark that they changed, each lane went on drawing the last run's numbers,
* under the new run's name, until the view moved far enough to refetch — a
* compartment track switched to another method and run again read, beside the
* map, exactly as it had before. Absent on every row whose values are fixed
* once read, which is every file.
*/
revision?: number
/**
* The feature set the browser's search reads.
*
* Several gene models can be *drawn* — comparing two annotations of one genome
* is a thing people do — but a search box that answers from all of them at
* once answers "GAL4" with four rows that differ only in which file they came
* from. So one of them is the one being searched, marked with a filled star,
* and the others carry an empty one that makes them it.
*/
primary?: boolean
/**
* The other row over the same file, where this row has one (§8.13).
*
* A BAM imports as two: its depth and its pile. They are separate rows all
* the way down — separate lanes, separate heights, either closable without
* the other — and what this holds together is the one thing that would be a
* lie if they drifted, *which reads are being counted*. A minimum mapping
* quality applied to the pile and not to the depth drawn above it is two
* pictures of two different files under one name, so `readStyleKey`'s
* settings are written through to both (`ui/laneSettings.ts`).
*
* Symmetric: each row names the other. Cleared on the survivor when one of
* them is closed, so a write-through cannot chase an id that has gone.
*/
partner?: string
}
/** The part of an entry that is worth remembering across a reload (§8.6). */
export interface StoredEntry {
id: string
card: AnnotationCard
kind: AnnotationKind
label: string
/**
* `computed` carries its recipe; the other two carry only a name and a URL,
* because the bytes behind them are not this app's to remember (§8.6).
*/
source: {
kind: 'local' | 'remote' | 'computed' | 'metadata'
name: string
url?: string
/**
* `remote` only: the sidecar index, where it was *named* rather than guessed.
*
* A guessed one (`<url>.bai`) is re-derived on every load and needs no
* storage. A named one cannot be: `#index=` exists precisely for the URLs
* where appending a suffix means nothing — a presigned URL, or one carrying
* a query string — so dropping it here would make a share link and a
* reloaded tab open a BAM this app cannot index (§8.13). A local `File`
* still cannot be written down and is not.
*/
index?: string
task?: string
recipe?: Record<string, unknown>
framing?: string
/** `metadata` only: which of the open file's tables, and which column. */
table?: 'genomes' | 'contigs' | 'bins'
column?: string
}
visible: boolean
placement: Placement
style: AnnotationStyle
/**
* What the session's own map does with this track, when it is not simply
* *draw it where the track says* — and `null` when it does not draw it at all.
*
* ## Why the lead needs a record of its own
*
* Every other map's lane is written down by file, in `deck:lanes`
* (`AnnotationStore.parkedFor`), and the lead is deliberately excluded there:
* a `StoredLane` is keyed on the companion's URI and the lead has no such key
* to be found under. So the two things a lane can say that a track cannot —
* *I am not on this map* and *my range here is different* — were sayable about
* every map in a deck except the one every session has.
*
* Both were losable in ordinary use. Taking a track off the lead is one press
* of the last lit edge in the location row (`ui/lanePlacement.ts`), and every
* analysis result computed against a starred companion is placed on that
* companion and taken off the lead (`analysis/workspace.ts`) — after which
* `placement` still names the edge the track *would* go on, and a restore
* dutifully put it there. A deck of four came back with the fourth's tracks
* stacked on the first as well.
*
* ## Absent is not `null`
*
* Three states, and conflating any two of them loses something:
*
* - **absent** — every config written before this field existed, and every
* track that is on the lead at its own placement with nothing to add. Read
* as *drawn on the lead, as `placement` says*, which is what those files
* meant and what a plain track means today.
* - **`null`** — the lead does not draw it. `placement` is still carried,
* because it is the default any *new* lane of this track is given.
* - **an object** — the lead draws it, and here is what it says differently:
* its own edges, and the sparse patch `LANE_OVERRIDES` allows over the
* track's style (`annotations/placement.ts`).
*/
lead?: { placement: Placement; style?: Partial<Pick<AnnotationStyle, 'limits' | 'flip'>> } | null
/** Which feature set the search reads; see {@link AnnotationEntry.primary}. */
primary?: boolean
/**
* The other row over the same BAM; see {@link AnnotationEntry.partner}.
*
* Written down because the pair is a *decision*, not a derivation. A reader
* who closed the pile and kept the depth must get back a lone depth row, and
* a restore that re-derived the pairing from "this row is a BAM" would hand
* them the pile again on every reload. Ids survive a restore intact, so the
* link comes back pointing at the same two rows.
*/
partner?: string
}
// ---------------------------------------------------------------------------
// Alignments: a window of reads (§8.13)
// ---------------------------------------------------------------------------
/**
* The reads under one window, packed as parallel arrays.
*
* The same shape, for the same reason, as {@link FeatureSet}: twenty thousand
* reads as an array of objects is megabytes of pointer-chasing to draw a
* picture that is a few thousand rectangles, and none of it survives
* `postMessage` without being copied. Everything here is a typed array or a
* `string[]`, so `transfersFor` moves the buffers rather than cloning them.
*
* Unlike a `FeatureSet` this is **not** the whole file. It is one window,
* fetched for one view and thrown away when the view moves, because a BAM has
* no bounded whole-file form to hold (§8.13). That is also why the rows are
* packed here rather than at paint time: row assignment depends on which *other*
* reads are present, so packing per frame would make reads jump rows under the
* hand.
*/
export interface ReadPile {
/** The map's spelling for the sequence these are on. */
contig: string
/** The window packed, which is wider than the view by `FETCH_PAD`. */
start: number
end: number
/** Number of reads. Every per-read array below is this long. */
length: number
/** First aligned reference base. Soft clips are *not* included. */
refStart: Int32Array
/** One past the last aligned reference base. */
refEnd: Int32Array
/** Soft-clipped bases before `refStart` and after `refEnd`, for drawing them. */
clipStart: Int32Array
clipEnd: Int32Array
/** Which packed row this read is drawn in. */
row: Int16Array
/** The SAM flag, whole — strand, pairing and the rest are read off it. */
flags: Uint16Array
mapq: Uint8Array
/**
* Index into {@link contigs} for the mate, or `-1` where there is none.
*
* An index into that compact table rather than the file's own reference id:
* see `nameMates`, which does the remapping and is also what puts the *map's*
* spelling on the read's own sequence.
*/
mateContig: Int32Array
/** The mate's leftmost position, or `-1`. */
matePos: Int32Array
/** Observed template length; `0` where the file carries none. */
tlen: Int32Array
/** Index into {@link groups} — which colour this read takes. */
group: Uint8Array
/**
* The other end of this template, as an index into this pile, or `-1`.
*
* Resolved here and not by the renderer, because it is a join over the whole
* window and doing it per frame is a map rebuilt sixty times a second. `-1`
* covers both "not paired" and "the mate is outside this window", which are
* drawn the same way: a stub, not a line to nowhere.
*/
mate: Int32Array
/**
* The read's CIGAR, flattened: ops `opAt[i] … opAt[i + 1]`.
*
* The same `n + 1` offset array a `FeatureSet` flattens its exons with. A
* read needs its ops rather than only its extent because an insertion, a
* deletion and a spliced gap are three different marks, and the gap in
* particular is most of what an RNA-seq pile is *for*.
*/
opAt: Int32Array
opCode: Uint8Array
opLen: Int32Array
/**
* Where this read disagrees with the reference, flattened the same way.
*
* Empty for every read when {@link ReadWindow.diffs} is `'none'` — which is
* the common case, since neither `bwa` nor `minimap2` writes an `MD` tag by
* default and `=`/`X` operations are rarer still.
*/
diffAt: Int32Array
/** Reference position of each disagreement. */
diffPos: Int32Array
/** The base the *read* has there, as a `BASE_CODE` value. */
diffBase: Uint8Array
/**
* Contig names this pile refers to, for mates on another sequence.
*
* Only the ones some mate in this window actually lands on — a header's whole
* `@SQ` list is a hundred thousand strings on a scaffolded assembly, and it
* would be sent per window. Empty when nothing here has a mate.
*/
contigs: string[]
/** Category labels, indexed by {@link group}. */
groups: string[]
/** Read names, kept only where a reader could ask for one. `[]` otherwise. */
names: string[]
/**
* The base-modification calls of the window's reads, from their `MM`/`ML`
* tags (§8.13), flattened as the ops are: calls `mods.at[i] … mods.at[i + 1]`.
*
* Present only on a pile fetched to be coloured by base modification — the
* tags are read and the sequence unpacked for nothing otherwise — and then
* present even where no read carried a tag, with empty columns, so "this
* file has no modification calls here" is a pile that says so.
*/
mods?: PileMods
}
/** See {@link ReadPile.mods}. */
export interface PileMods {
at: Int32Array
/** Reference position of each call, 0-based. */
pos: Int32Array
/** Index into {@link codes}. */
code: Uint8Array
/** The `ML` byte: the probability is `(p + 0.5) / 256`. */
prob: Uint8Array
/**
* The codes the window's calls use (`m`, `h`, `a`, a ChEBI number …), the
* one with most calls of *modified* first — see {@link counts}.
*/
codes: string[]
/**
* Calls of each code at `p ≥ ½`, parallel to {@link codes}: what an unset
* choice of code is ranked by. Not all calls, because a `.` group makes an
* implicit unmodified call at every base it steps over — a file with `A+a.`
* has a call on every adenine — and ranking by those would choose 6mA over
* 5mC on a file that is mostly about 5mC.
*/
counts: number[]
}
/**
* How a pile is coloured.
*
* Not how it is *grouped*: `place` in `bam/reads.ts` packs rows by position and
* has never looked at the category, so the colours are interleaved down the
* pile. A band per category is a separate feature this lane does not have, and
* saying otherwise here was the doc describing an intention rather than a
* function.
*
* Here rather than in `bam/reads.ts` because it crosses the worker boundary
* *and* is written into a stored session: a type in either position cannot live
* in a module that imports this one.
*/
export type ColourBy = 'strand' | 'mapq' | 'pair' | 'mate' | 'insert' | 'basemod' | 'none'
/**
* The controls an alignment lane has that no other kind does (§8.13).
*
* A nested optional record rather than fields on {@link AnnotationStyle}
* itself. The doc there defends one flat shape for every kind, and it is right
* to: a `mode` or a `colour` means something for all of them. These do not —
* a MAPQ floor is meaningless on a bedGraph — and being optional means no
* existing kind, no `defaultStyle` arm and no stored row changes shape.
*/
export interface ReadStyle {
colourBy: ColourBy
/** Reads below this mapping quality are not drawn, and not counted. */
minMapq: number
/**
* Hide reads a pipeline marked as duplicates.
*
* Off by default, and that is the decision §8.13 records: marking them is a
* pipeline's choice, and hiding them silently would make this app disagree
* with every other reading of the same file.
*/
hideDuplicates: boolean
/**
* How a template is laid out, and what is drawn between its two ends.
*
* `pairs` is the default and is what a paired library wants: both ends on one
* row, joined by a dotted thread. It replaced a `pairs: boolean` that drew a
* solid thread and left the packing to position alone — so the two ends of a
* fragment landed on different rows at any real depth, and the thread ran
* from one of them towards a mate that was not there. The picture read as a
* scatter of unrelated reads.
*
* `fragments` fills the gap instead of threading it, for a reader who wants
* the fragment as one object; `reads` packs every record on its own, which is
* what a single-ended library wants and what this lane did originally.
*/
layout: PairLayout
/**
* Show supplementary alignments — the other pieces of a split read.
*
* Off by default and separate from everything else here, because it is the
* one switch that changes what an alignment *is*. A supplementary record is a
* second piece of one molecule that the aligner could not place in one run:
* across a structural breakpoint, across a fusion, or across the junction a
* long read spans. Showing them is how a reader sees a split; counting them
* would be counting the same molecule twice.
*
* So it reaches what is *drawn* and never what is *counted*: `readFilterKey`
* deliberately does not mention it, so the depth row beside the pile never
* sees it. The pile itself takes the one filter, the drawing one.
*/
splits: boolean
/**
* Draw the introns the reads skip as a sashimi plot's arcs, over the depth
* (§8.23).
*
* On by default, because it costs nothing where there is nothing to draw: a
* DNA library's reads skip no introns, so its lane is unchanged, and an
* RNA-seq file's is the picture a reader of one expects. Off is for a figure
* of the depth alone.
*/
sashimi: boolean
/**
* The fewest reads an arc is drawn from: sashimi plots' *minimum junction
* coverage*. Two by default, because a junction one read long is as often an
* aligner's guess as a splice, and in a deep file those guesses are most of
* the arcs.
*/
sashimiFloor: number
/**
* Which base modification the pile's calls and the depth's fraction are of
* (§8.13): a code as `MM` writes it — `m`, `h`, `a`, a ChEBI number — or
* `''` for whichever the window holds most modified calls of.
*
* Written through to the other row, like the filters, because the pile's
* marks and the depth's percentage beside them are one measurement and
* reading them as two would be two different modifications under one name.
*/
modCode: string
/**
* The probability a call must reach to count as modified, and whose
* complement it must fall to to count as unmodified; between the two a call
* is neither drawn nor counted. `0.5` — every call counts — to `1`.
*/
modThreshold: number
/**
* Draw the depth row as the **percentage of calls modified** rather than as
* depth: a bedMethyl's picture, made from the reads (§8.13, §8.20). The depth
* row's alone.
*/
modFraction: boolean
}
/** What an alignment lane starts with. */
export function defaultReadStyle(): ReadStyle {
return {
colourBy: 'strand',
minMapq: 0,
hideDuplicates: false,
layout: 'pairs',
splits: false,
sashimi: true,
sashimiFloor: 2,
modCode: '',
modThreshold: 0.5,
modFraction: false,
}
}
/**
* One lane's read controls, whether or not its style carries any.
*
* The accessor every reader of the field goes through, so that `reads` being
* optional costs one call rather than a `??` at each of a dozen sites — and so
* that a session stored before this field existed opens with the defaults
* instead of with `undefined` reaching a renderer.
*
* It does **not** validate, and that is worth being explicit about: the
* clamping happens once, at the boundary, in `AnnotationStore.stored`. That is
* enough only because `reads` is not among the per-map style overrides and no
* caller resolves a lane style before reaching here. If either changes, this is
* where {@link readStyleOf} has to be called instead.
*/
export function readsOf(style: AnnotationStyle): ReadStyle {
return style.reads ?? defaultReadStyle()
}
/**
* Read a stored read style back, clamping every field.
*
* Stored state outlives the build that wrote it and arrives from a share link,
* so this is untrusted input: `isStoredEntry` checks that `style` is an object
* and no more, which is enough to hand a renderer a negative MAPQ or a colouring
* it has never heard of.
*/
export function readStyleOf(value: unknown): ReadStyle {
const fallback = defaultReadStyle()
if (typeof value !== 'object' || value === null) return fallback
const row = value as Record<string, unknown>
const by = row['colourBy']
const mapq = row['minMapq']
return {
colourBy: COLOUR_BY.includes(by as ColourBy) ? (by as ColourBy) : fallback.colourBy,
minMapq:
typeof mapq === 'number' && Number.isFinite(mapq)
? Math.max(0, Math.min(255, Math.round(mapq)))
: fallback.minMapq,
hideDuplicates: row['hideDuplicates'] === true,
// A session stored before this was a mode carries `pairs: false`, which
// meant "do not join them" — which is `reads`.
layout: PAIR_LAYOUTS.includes(row['layout'] as PairLayout)
? (row['layout'] as PairLayout)
: row['pairs'] === false
? 'reads'
: fallback.layout,
splits: row['splits'] === true,
// Absent means *on*, for the same reason as every default here: a session
// stored before the arcs existed opens on what a new lane opens on.
sashimi: row['sashimi'] !== false,
sashimiFloor:
typeof row['sashimiFloor'] === 'number' && Number.isFinite(row['sashimiFloor'])
? Math.max(1, Math.min(1_000_000, Math.round(row['sashimiFloor'] as number)))
: fallback.sashimiFloor,
// A code as `MM` spells one — letters or a ChEBI number — and nothing else:
// it is drawn into a label and a select, and it arrives from a share link.
modCode:
typeof row['modCode'] === 'string' && /^([A-Za-z]|\d{1,8})$/.test(row['modCode'])
? row['modCode']
: fallback.modCode,
modThreshold:
typeof row['modThreshold'] === 'number' && Number.isFinite(row['modThreshold'])
? Math.max(0.5, Math.min(1, row['modThreshold'] as number))
: fallback.modThreshold,
modFraction: row['modFraction'] === true,
}
}
/** Every colouring, in the order they are offered. */
export const COLOUR_BY: ColourBy[] = ['strand', 'pair', 'insert', 'mate', 'mapq', 'basemod', 'none']
/**
* How the two ends of a template are laid out. See {@link ReadStyle.layout}.
*
* Here rather than in `render/alignments.ts` for the reason {@link ColourBy} is:
* it is persisted into a stored session, so it cannot live in a module that
* imports this one.
*/
export type PairLayout = 'pairs' | 'fragments' | 'reads'
/** Every layout, in the order they are offered. */
export const PAIR_LAYOUTS: PairLayout[] = ['pairs', 'fragments', 'reads']
/**
* The style as a cache key, for the caches that hold *reads*.
*
* Four of the five fields are in it, because each changes what comes back: the
* colouring decides the category column the worker sends back, and the floor,
* the duplicate switch and the split switch decide which reads exist at all.
*
* `pairs` is deliberately **out**. It is the one field the worker never reads —
* a mate thread is drawn from columns the pile already carries, so the reply is
* byte-identical with it on or off — and putting it in the key spends a whole
* window of BGZF decoding on a switch that only changes what the renderer does
* with what it already has. If that ever stops being true, this is the line
* that has to change with it.
*/
export function readStyleKey(style: ReadStyle): string {
const flags = `${style.hideDuplicates ? 'd' : ''}${style.splits ? 's' : ''}`
return `${style.colourBy}|${style.minMapq}|${flags}`
}
/**
* The lane's own reading of a stored `pairs` boolean, for the one caller that
* still has to understand the old spelling. See {@link ReadStyle.layout}.
*/
export function joinsPairs(style: ReadStyle): boolean {
return style.layout !== 'reads'
}
/**
* The style as a cache key, for the caches that hold *coverage*.
*
* Deliberately narrower than {@link readStyleKey}: depth is a count, and a
* count does not care how the reads that made it were coloured or whether their
* mates were joined. Using the wider key here would throw away a whole-genome
* coverage scan every time a reader tried a different colouring — the most
* expensive read in the application, discarded for a change that cannot move
* the line by a pixel.
*/
export function readFilterKey(style: ReadStyle): string {
// …and, when the depth row is drawn as a fraction modified, which
// modification and at what threshold: then the row *is* that count, and
// changing either is a different answer from the worker. Off, neither is in
// the key, so a reader trying codes on the pile costs the depth nothing.
const mods = style.modFraction ? `|m:${style.modCode}:${style.modThreshold}` : ''
return `${style.minMapq}|${style.hideDuplicates ? 'd' : ''}${mods}`
}
/** Where a lane's disagreements came from, or why there are none. */
export type DiffSource =
/** The aligner wrote `=`/`X` operations, which say it outright. */
| 'cigar'
/** Derived from the `MD` tag against the read's own bases. */
| 'md'
/** Compared against a FASTA already open in this worker. */
| 'reference'
/** Nothing to compare against — *not* "no differences here". */
| 'none'
/** The answer to a `reads` request: what to draw, and what was left out. */
export interface ReadWindow {
piles: ReadPile[]
/** Reads the window held before filtering and downsampling. */
total: number
/** Reads actually returned. */
kept: number
/** `true` when reads were dropped to fit — the lane says so (§8.13). */
capped: boolean
/** How many rows the packing used, so a lane can size itself. */
rows: number
/**
* Where the disagreements came from.
*
* Surfaced rather than inferred, because "no mismatches in this window" and
* "mismatches cannot be computed for this file" are the same empty picture
* and must not read as the same claim.
*/
diffs: DiffSource
/** Why nothing was returned, when nothing was — the same contract as `TrackSampleSet.note`. */
note?: string
}
|