react-msaview 5.6.1 → 5.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/bundle/index.js +95 -95
  2. package/bundle/index.js.map +4 -4
  3. package/dist/clustalX.d.ts +4 -3
  4. package/dist/clustalX.js +25 -4
  5. package/dist/clustalX.js.map +1 -1
  6. package/dist/columnCounts.d.ts +37 -0
  7. package/dist/columnCounts.js +148 -0
  8. package/dist/columnCounts.js.map +1 -0
  9. package/dist/components/MSAView.js +1 -8
  10. package/dist/components/MSAView.js.map +1 -1
  11. package/dist/components/getVisibleLeaves.d.ts +6 -0
  12. package/dist/components/getVisibleLeaves.js.map +1 -0
  13. package/dist/components/msa/MSACanvasBlock.js +17 -54
  14. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  15. package/dist/components/msa/renderBoxFeatureCanvasBlock.d.ts +2 -1
  16. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +21 -19
  17. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  18. package/dist/components/msa/renderMSABlock.js +97 -96
  19. package/dist/components/msa/renderMSABlock.js.map +1 -1
  20. package/dist/components/msa/renderMSAMouseover.js +9 -32
  21. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  22. package/dist/components/msa/useMsaBlockMouse.d.ts +23 -0
  23. package/dist/components/msa/useMsaBlockMouse.js +45 -0
  24. package/dist/components/msa/useMsaBlockMouse.js.map +1 -0
  25. package/dist/components/tree/TreeCanvas.js +14 -19
  26. package/dist/components/tree/TreeCanvas.js.map +1 -1
  27. package/dist/components/tree/TreeCanvasBlock.js +40 -111
  28. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  29. package/dist/components/tree/renderTreeCanvas.d.ts +2 -0
  30. package/dist/components/tree/renderTreeCanvas.js +15 -3
  31. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  32. package/dist/components/tree/useTreeHover.d.ts +24 -0
  33. package/dist/components/tree/useTreeHover.js +44 -0
  34. package/dist/components/tree/useTreeHover.js.map +1 -0
  35. package/dist/hierarchy.js +15 -9
  36. package/dist/hierarchy.js.map +1 -1
  37. package/dist/index.d.ts +2 -1
  38. package/dist/model/msaModel.js +4 -3
  39. package/dist/model/msaModel.js.map +1 -1
  40. package/dist/model/treeModel.js +9 -8
  41. package/dist/model/treeModel.js.map +1 -1
  42. package/dist/model.d.ts +52 -10
  43. package/dist/model.js +208 -93
  44. package/dist/model.js.map +1 -1
  45. package/dist/propertyConservation.d.ts +2 -1
  46. package/dist/propertyConservation.js +36 -25
  47. package/dist/propertyConservation.js.map +1 -1
  48. package/dist/seqPosToGlobalCol.d.ts +7 -0
  49. package/dist/seqPosToGlobalCol.js +17 -0
  50. package/dist/seqPosToGlobalCol.js.map +1 -1
  51. package/dist/stripDefault.d.ts +21 -0
  52. package/dist/stripDefault.js +26 -0
  53. package/dist/stripDefault.js.map +1 -0
  54. package/dist/types.d.ts +16 -0
  55. package/dist/version.d.ts +1 -1
  56. package/dist/version.js +1 -1
  57. package/package.json +3 -3
  58. package/src/clustalX.test.ts +9 -11
  59. package/src/clustalX.ts +27 -28
  60. package/src/columnCounts.test.ts +58 -0
  61. package/src/columnCounts.ts +170 -0
  62. package/src/components/MSAView.tsx +1 -9
  63. package/src/components/{msa/getVisibleLeaves.ts → getVisibleLeaves.ts} +1 -1
  64. package/src/components/msa/MSACanvasBlock.tsx +42 -110
  65. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +25 -18
  66. package/src/components/msa/renderMSABlock.ts +113 -125
  67. package/src/components/msa/renderMSAMouseover.ts +11 -34
  68. package/src/components/msa/useMsaBlockMouse.ts +81 -0
  69. package/src/components/tree/TreeCanvas.tsx +16 -23
  70. package/src/components/tree/TreeCanvasBlock.tsx +62 -143
  71. package/src/components/tree/renderTreeCanvas.test.ts +100 -190
  72. package/src/components/tree/renderTreeCanvas.ts +14 -3
  73. package/src/components/tree/useTreeHover.ts +69 -0
  74. package/src/domainBands.test.ts +98 -0
  75. package/src/hierarchy.ts +15 -9
  76. package/src/index.ts +7 -0
  77. package/src/model/msaModel.ts +4 -6
  78. package/src/model/treeModel.ts +9 -14
  79. package/src/model.ts +241 -112
  80. package/src/propertyConservation.test.ts +2 -13
  81. package/src/propertyConservation.ts +40 -31
  82. package/src/rowSpace.test.ts +46 -0
  83. package/src/seqPosToGlobalCol.ts +18 -0
  84. package/src/stripDefault.test.ts +48 -0
  85. package/src/stripDefault.ts +35 -0
  86. package/src/types.ts +23 -0
  87. package/src/version.ts +1 -1
  88. package/dist/components/msa/getVisibleLeaves.d.ts +0 -6
  89. package/dist/components/msa/getVisibleLeaves.js.map +0 -1
  90. package/dist/useDevicePixelRatio.d.ts +0 -1
  91. package/dist/useDevicePixelRatio.js +0 -27
  92. package/dist/useDevicePixelRatio.js.map +0 -1
  93. package/src/useDevicePixelRatio.ts +0 -29
  94. /package/dist/components/{msa/getVisibleLeaves.js → getVisibleLeaves.js} +0 -0
@@ -1,3 +1,7 @@
1
+ import { numResidueSlots, residueSlotOfLetter } from './columnCounts.ts'
2
+
3
+ import type { ColumnCounts } from './columnCounts.ts'
4
+
1
5
  // Disjoint physicochemical classes for the 20 standard amino acids. Property
2
6
  // conservation asks a different question than identity conservation: a column
3
7
  // can vary in exact residue yet stay within one property class (a conservative
@@ -29,8 +33,15 @@ const aminoPropertyClass: Record<string, string> = {
29
33
  P: 'special',
30
34
  }
31
35
 
32
- const numPropertyClasses = new Set(Object.values(aminoPropertyClass)).size
33
- const maxPropertyEntropy = Math.log2(numPropertyClasses)
36
+ const propertyClasses = [...new Set(Object.values(aminoPropertyClass))]
37
+ const maxPropertyEntropy = Math.log2(propertyClasses.length)
38
+
39
+ // residue slot -> index into propertyClasses (-1 for residues with no class,
40
+ // e.g. X or '*'), so the per-column tally needs no string lookups
41
+ const classOfSlot = new Int8Array(numResidueSlots).fill(-1)
42
+ for (const [letter, cls] of Object.entries(aminoPropertyClass)) {
43
+ classOfSlot[residueSlotOfLetter(letter)] = propertyClasses.indexOf(cls)
44
+ }
34
45
 
35
46
  /**
36
47
  * Per-column property conservation using Shannon entropy over physicochemical
@@ -38,36 +49,34 @@ const maxPropertyEntropy = Math.log2(numPropertyClasses)
38
49
  * residue shares one property class. Mirrors the identity-conservation formula
39
50
  * so the two tracks are directly comparable: (1 - H/Hmax) * (1 - gapFraction).
40
51
  */
41
- export function calculatePropertyConservation(
42
- colStats: Record<string, number>[],
43
- colStatsSums: number[],
44
- ) {
45
- return colStats.map((stats, i) => {
46
- const total = colStatsSums[i]
47
- if (!total) {
48
- return 0
49
- }
50
-
51
- let nonGapTotal = 0
52
- const classCounts: Record<string, number> = {}
53
- for (const [letter, count] of Object.entries(stats)) {
54
- const cls = aminoPropertyClass[letter]
55
- if (cls) {
56
- classCounts[cls] = (classCounts[cls] ?? 0) + count
57
- nonGapTotal += count
52
+ export function calculatePropertyConservation(counts: ColumnCounts) {
53
+ const classCounts = new Float64Array(propertyClasses.length)
54
+ return Array.from({ length: counts.numColumns }, (_, col) => {
55
+ const total = counts.total(col)
56
+ let score = 0
57
+ if (total) {
58
+ classCounts.fill(0)
59
+ let nonGapTotal = 0
60
+ counts.forEachResidue(col, (slot, count) => {
61
+ const cls = classOfSlot[slot]!
62
+ if (cls >= 0) {
63
+ classCounts[cls]! += count
64
+ nonGapTotal += count
65
+ }
66
+ })
67
+ if (nonGapTotal > 0) {
68
+ let entropy = 0
69
+ for (const count of classCounts) {
70
+ if (count > 0) {
71
+ const freq = count / nonGapTotal
72
+ entropy -= freq * Math.log2(freq)
73
+ }
74
+ }
75
+ const gapFraction = (total - nonGapTotal) / total
76
+ score =
77
+ Math.max(0, 1 - entropy / maxPropertyEntropy) * (1 - gapFraction)
58
78
  }
59
79
  }
60
- if (nonGapTotal === 0) {
61
- return 0
62
- }
63
-
64
- let entropy = 0
65
- for (const count of Object.values(classCounts)) {
66
- const freq = count / nonGapTotal
67
- entropy -= freq * Math.log2(freq)
68
- }
69
-
70
- const gapFraction = (total - nonGapTotal) / total
71
- return Math.max(0, 1 - entropy / maxPropertyEntropy) * (1 - gapFraction)
80
+ return score
72
81
  })
73
82
  }
@@ -0,0 +1,46 @@
1
+ // @vitest-environment jsdom
2
+ import { expect, test } from 'vitest'
3
+
4
+ import MSAModelF from './model.ts'
5
+
6
+ // the tree names a tip the alignment has no sequence for. It still occupies a
7
+ // row of vertical space (drawn blank), so anything that maps y-pixels to rows has
8
+ // to count it, or the last row becomes unhoverable and fit-to-height overshoots.
9
+ const msa = `>a
10
+ ACGT
11
+ >b
12
+ ACGT`
13
+ const tree = '((a:1,b:1):1,ghost:1);'
14
+
15
+ function makeModel() {
16
+ const model = MSAModelF().create({
17
+ id: 'row-space-test',
18
+ type: 'MsaView',
19
+ msaFormat: 'fasta',
20
+ height: 400,
21
+ data: { msa, tree },
22
+ })
23
+ model.setWidth(800)
24
+ return model
25
+ }
26
+
27
+ test('row space counts tree leaves, not just rows with sequence', () => {
28
+ const model = makeModel()
29
+ expect(model.rowNames).toEqual(['a', 'b', 'ghost'])
30
+ expect(model.rows.map(r => r[0])).toEqual(['a', 'b'])
31
+ expect(model.numRows).toBe(3)
32
+ expect(model.totalHeight).toBe(3 * model.rowHeight)
33
+ })
34
+
35
+ test('the last row is addressable', () => {
36
+ const model = makeModel()
37
+ const lastRow = model.numRows - 1
38
+ model.setMousePos(0, lastRow)
39
+ expect(model.mouseOverRowName).toBe('ghost')
40
+ })
41
+
42
+ test('fit vertically leaves every row on screen', () => {
43
+ const model = makeModel()
44
+ model.fitVertically()
45
+ expect(model.totalHeight).toBeLessThanOrEqual(model.msaAreaHeight)
46
+ })
@@ -15,6 +15,24 @@ import { isBlank } from './util.ts'
15
15
  * seqPosToGlobalCol({ row: "A-TG-C", seqPos: 2 }) // → 3 (G)
16
16
  * seqPosToGlobalCol({ row: "A-TG-C", seqPos: 3 }) // → 5 (C)
17
17
  */
18
+ /**
19
+ * The global column of every ungapped sequence position in a row, i.e.
20
+ * index[seqPos] is the column holding the seqPos-th non-gap character. Amortizes
21
+ * repeated seqPosToGlobalCol lookups (the domain overlay does one per feature
22
+ * per row) into a single pass per row.
23
+ */
24
+ export function buildSeqPosIndex(row: string) {
25
+ const index = new Int32Array(row.length)
26
+ let n = 0
27
+ for (let col = 0; col < row.length; col++) {
28
+ // bit trick: (code - 45) >>> 0 <= 1 checks for '-' (45) or '.' (46)
29
+ if (!((row.charCodeAt(col) - 45) >>> 0 <= 1)) {
30
+ index[n++] = col
31
+ }
32
+ }
33
+ return index.subarray(0, n)
34
+ }
35
+
18
36
  export function seqPosToGlobalCol({
19
37
  row,
20
38
  seqPos,
@@ -0,0 +1,48 @@
1
+ import { getSnapshot, types } from '@jbrowse/mobx-state-tree'
2
+ import { afterEach, expect, test } from 'vitest'
3
+
4
+ import { stripDefault } from './stripDefault.ts'
5
+
6
+ const real = types.stripDefault
7
+
8
+ afterEach(() => {
9
+ types.stripDefault = real
10
+ })
11
+
12
+ test('strips defaults from the snapshot when the host mst supports it', () => {
13
+ const model = types
14
+ .model({ a: stripDefault(types.boolean, false) })
15
+ .create({})
16
+ expect(model.a).toBe(false)
17
+ expect(getSnapshot(model).a).toBeUndefined()
18
+ })
19
+
20
+ // The path that actually runs inside every RELEASED jbrowse-web: the host
21
+ // supplies its own mobx-state-tree, and stripDefault only exists in the copy
22
+ // that ships with unreleased @jbrowse/core. Calling it there used to throw while
23
+ // the model was built, which error-paged the whole app. dev has the API, so
24
+ // without stubbing it away this path is never exercised.
25
+ test('falls back to types.optional when the host mst lacks stripDefault', () => {
26
+ // @ts-expect-error deleting it is the point: simulate an older host
27
+ delete types.stripDefault
28
+ const model = types
29
+ .model({ a: stripDefault(types.boolean, false) })
30
+ .create({})
31
+ expect(model.a).toBe(false)
32
+ // the only difference: the default is serialized rather than omitted
33
+ expect(getSnapshot(model).a).toBe(false)
34
+ })
35
+
36
+ test('non-default values survive both paths', () => {
37
+ const withApi = types
38
+ .model({ a: stripDefault(types.number, 1) })
39
+ .create({ a: 5 })
40
+ expect(getSnapshot(withApi).a).toBe(5)
41
+
42
+ // @ts-expect-error see above
43
+ delete types.stripDefault
44
+ const without = types
45
+ .model({ a: stripDefault(types.number, 1) })
46
+ .create({ a: 5 })
47
+ expect(getSnapshot(without).a).toBe(5)
48
+ })
@@ -0,0 +1,35 @@
1
+ import { types } from '@jbrowse/mobx-state-tree'
2
+
3
+ import type {
4
+ IAnyType,
5
+ IOptionalIType,
6
+ OptionalDefaultValueOrFunction,
7
+ } from '@jbrowse/mobx-state-tree'
8
+
9
+ /**
10
+ * `types.stripDefault`, degrading to `types.optional` on hosts that lack it.
11
+ *
12
+ * stripDefault keeps defaulted properties out of snapshots, but it exists only
13
+ * in the mobx-state-tree that ships with unreleased @jbrowse/core. This package
14
+ * declares `@jbrowse/mobx-state-tree: ^5.0.0`, and inside jbrowse-web the host
15
+ * supplies its own copy at runtime -- the plugin bundle externalizes it -- so we
16
+ * genuinely do not know which version we are calling until we are running.
17
+ *
18
+ * Calling it where it is absent throws while the model is being constructed,
19
+ * which in a plugin bundle means PluginLoader rejects and the entire app renders
20
+ * its error page. Losing minimal snapshots is a far better failure than losing
21
+ * the session, so this degrades instead.
22
+ *
23
+ * The two signatures are identical, so behaviour differs only in that defaults
24
+ * reappear in snapshots on older hosts (bigger session snapshots and shared
25
+ * URLs, nothing incorrect). stripDefaultSnapshot.test.ts still pins the stripped
26
+ * behaviour, since dev runs against a version that has it.
27
+ */
28
+ export function stripDefault<IT extends IAnyType>(
29
+ type: IT,
30
+ defaultValue: OptionalDefaultValueOrFunction<IT>,
31
+ ): IOptionalIType<IT, [undefined]> {
32
+ return typeof types.stripDefault === 'function'
33
+ ? types.stripDefault(type, defaultValue)
34
+ : types.optional(type, defaultValue)
35
+ }
package/src/types.ts CHANGED
@@ -26,6 +26,29 @@ export interface BasicTrack {
26
26
  model: TextTrackModel & BarTrackModel
27
27
  }
28
28
 
29
+ // one InterProScan/GFF annotation location flattened onto a single row, as
30
+ // produced by the model's tidyInterProAnnotations
31
+ export interface TidyDomainAnnotation {
32
+ id: string
33
+ name: string
34
+ accession: string
35
+ description: string
36
+ featureType?: string
37
+ start: number
38
+ end: number
39
+ strand?: number
40
+ }
41
+
42
+ // a tidy annotation resolved to the visible column span it is drawn across.
43
+ // stackIndex is its position within its row's annotation list, which the
44
+ // sub-row layout uses to stack boxes.
45
+ export interface DomainBand {
46
+ annotation: TidyDomainAnnotation
47
+ startCol: number
48
+ endCol: number
49
+ stackIndex: number
50
+ }
51
+
29
52
  export interface Node {
30
53
  children?: Node[]
31
54
  name?: string
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '5.6.1'
1
+ export const version = '5.6.3'
@@ -1,6 +0,0 @@
1
- import type { MsaViewModel } from '../../model.ts';
2
- export declare function getVisibleLeaves({ model, offsetY, blockSizeY, }: {
3
- model: MsaViewModel;
4
- offsetY: number;
5
- blockSizeY: number;
6
- }): import("../../hierarchy.ts").HierarchyNode<import("../../types.ts").NodeWithIdsAndLength>[];
@@ -1 +0,0 @@
1
- {"version":3,"file":"getVisibleLeaves.js","sourceRoot":"","sources":["../../../src/components/msa/getVisibleLeaves.ts"],"names":[],"mappings":"AAEA,2EAA2E;AAC3E,2DAA2D;AAC3D,MAAM,UAAU,gBAAgB,CAAC,EAC/B,KAAK,EACL,OAAO,EACP,UAAU,GAKX;IACC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,KAAK,CAAA;IACnC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC,CAAC,CAAA;IACzE,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CACnB,CAAC,EACD,IAAI,CAAC,IAAI,CAAC,CAAC,OAAO,GAAG,UAAU,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC,CAC1D,CAAA;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;AACnC,CAAC"}
@@ -1 +0,0 @@
1
- export declare function useDevicePixelRatio(): number;
@@ -1,27 +0,0 @@
1
- import { useSyncExternalStore } from 'react';
2
- // Subscribes to devicePixelRatio changes (moving the window between monitors,
3
- // browser zoom). The matchMedia query is pinned to the current ratio, so on each
4
- // change we re-register against the new ratio to keep tracking subsequent moves.
5
- function subscribe(callback) {
6
- if (typeof window === 'undefined') {
7
- return () => { };
8
- }
9
- let mql = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
10
- function onChange() {
11
- callback();
12
- mql.removeEventListener('change', onChange);
13
- mql = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
14
- mql.addEventListener('change', onChange);
15
- }
16
- mql.addEventListener('change', onChange);
17
- return () => {
18
- mql.removeEventListener('change', onChange);
19
- };
20
- }
21
- function getSnapshot() {
22
- return typeof window === 'undefined' ? 1 : window.devicePixelRatio;
23
- }
24
- export function useDevicePixelRatio() {
25
- return useSyncExternalStore(subscribe, getSnapshot, () => 1);
26
- }
27
- //# sourceMappingURL=useDevicePixelRatio.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useDevicePixelRatio.js","sourceRoot":"","sources":["../src/useDevicePixelRatio.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAA;AAE5C,8EAA8E;AAC9E,iFAAiF;AACjF,iFAAiF;AACjF,SAAS,SAAS,CAAC,QAAoB;IACrC,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;QAClC,OAAO,GAAG,EAAE,GAAE,CAAC,CAAA;IACjB,CAAC;IACD,IAAI,GAAG,GAAG,MAAM,CAAC,UAAU,CAAC,gBAAgB,MAAM,CAAC,gBAAgB,OAAO,CAAC,CAAA;IAC3E,SAAS,QAAQ;QACf,QAAQ,EAAE,CAAA;QACV,GAAG,CAAC,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;QAC3C,GAAG,GAAG,MAAM,CAAC,UAAU,CAAC,gBAAgB,MAAM,CAAC,gBAAgB,OAAO,CAAC,CAAA;QACvE,GAAG,CAAC,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;IAC1C,CAAC;IACD,GAAG,CAAC,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;IACxC,OAAO,GAAG,EAAE;QACV,GAAG,CAAC,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;IAC7C,CAAC,CAAA;AACH,CAAC;AAED,SAAS,WAAW;IAClB,OAAO,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,gBAAgB,CAAA;AACpE,CAAC;AAED,MAAM,UAAU,mBAAmB;IACjC,OAAO,oBAAoB,CAAC,SAAS,EAAE,WAAW,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,CAAA;AAC9D,CAAC"}
@@ -1,29 +0,0 @@
1
- import { useSyncExternalStore } from 'react'
2
-
3
- // Subscribes to devicePixelRatio changes (moving the window between monitors,
4
- // browser zoom). The matchMedia query is pinned to the current ratio, so on each
5
- // change we re-register against the new ratio to keep tracking subsequent moves.
6
- function subscribe(callback: () => void) {
7
- if (typeof window === 'undefined') {
8
- return () => {}
9
- }
10
- let mql = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`)
11
- function onChange() {
12
- callback()
13
- mql.removeEventListener('change', onChange)
14
- mql = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`)
15
- mql.addEventListener('change', onChange)
16
- }
17
- mql.addEventListener('change', onChange)
18
- return () => {
19
- mql.removeEventListener('change', onChange)
20
- }
21
- }
22
-
23
- function getSnapshot() {
24
- return typeof window === 'undefined' ? 1 : window.devicePixelRatio
25
- }
26
-
27
- export function useDevicePixelRatio() {
28
- return useSyncExternalStore(subscribe, getSnapshot, () => 1)
29
- }