react-msaview 5.9.0 → 5.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/bundle/index.js +76 -76
  2. package/bundle/index.js.map +3 -3
  3. package/dist/components/ConservationTrack.js +21 -16
  4. package/dist/components/ConservationTrack.js.map +1 -1
  5. package/dist/components/MSAView.js +17 -23
  6. package/dist/components/MSAView.js.map +1 -1
  7. package/dist/components/TextTrack.js +26 -21
  8. package/dist/components/TextTrack.js.map +1 -1
  9. package/dist/components/header/settingsMenuItems.js +39 -72
  10. package/dist/components/header/settingsMenuItems.js.map +1 -1
  11. package/dist/components/msa/MSACanvasBlock.js +19 -16
  12. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  13. package/dist/components/msa/MSAMouseoverCanvas.js +13 -6
  14. package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
  15. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +9 -6
  16. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  17. package/dist/components/msa/renderMSAMouseover.js +31 -31
  18. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  19. package/dist/components/overlayColors.d.ts +2 -0
  20. package/dist/components/overlayColors.js +5 -0
  21. package/dist/components/overlayColors.js.map +1 -1
  22. package/dist/components/tree/TreeCanvas.js +29 -24
  23. package/dist/components/tree/TreeCanvas.js.map +1 -1
  24. package/dist/components/tree/TreeCanvasBlock.js +19 -12
  25. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  26. package/dist/components/tree/renderTreeCanvas.d.ts +0 -38
  27. package/dist/components/tree/renderTreeCanvas.js +17 -17
  28. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  29. package/dist/model.js +82 -113
  30. package/dist/model.js.map +1 -1
  31. package/dist/neighborJoining.js +12 -19
  32. package/dist/neighborJoining.js.map +1 -1
  33. package/dist/parseAsn1.js +32 -43
  34. package/dist/parseAsn1.js.map +1 -1
  35. package/dist/renderToSvg.js +3 -0
  36. package/dist/renderToSvg.js.map +1 -1
  37. package/dist/seqPosToGlobalCol.d.ts +6 -19
  38. package/dist/seqPosToGlobalCol.js +6 -33
  39. package/dist/seqPosToGlobalCol.js.map +1 -1
  40. package/dist/useCanvasAutorun.d.ts +23 -1
  41. package/dist/useCanvasAutorun.js +20 -8
  42. package/dist/useCanvasAutorun.js.map +1 -1
  43. package/dist/version.d.ts +1 -1
  44. package/dist/version.js +1 -1
  45. package/dist/version.js.map +1 -1
  46. package/package.json +3 -3
  47. package/src/components/ConservationTrack.tsx +7 -5
  48. package/src/components/MSAView.tsx +24 -30
  49. package/src/components/TextTrack.tsx +7 -12
  50. package/src/components/header/settingsMenuItems.ts +40 -72
  51. package/src/components/msa/MSACanvasBlock.tsx +16 -21
  52. package/src/components/msa/MSAMouseoverCanvas.tsx +10 -6
  53. package/src/components/msa/renderBoxFeatureCanvasBlock.test.ts +51 -0
  54. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +13 -8
  55. package/src/components/msa/renderMSAMouseover.ts +37 -32
  56. package/src/components/overlayColors.ts +6 -0
  57. package/src/components/tree/TreeCanvas.tsx +6 -4
  58. package/src/components/tree/TreeCanvasBlock.tsx +11 -12
  59. package/src/components/tree/renderTreeCanvas.ts +20 -17
  60. package/src/model.ts +97 -130
  61. package/src/modelFilehandleLoaders.test.ts +137 -0
  62. package/src/neighborJoining.test.ts +12 -0
  63. package/src/neighborJoining.ts +12 -20
  64. package/src/parseAsn1.ts +32 -40
  65. package/src/{renderDomainLegendSvg.test.tsx → renderDomainsSvg.test.tsx} +44 -20
  66. package/src/renderToSvg.tsx +3 -0
  67. package/src/seqPosToGlobalCol.test.ts +39 -51
  68. package/src/seqPosToGlobalCol.ts +6 -41
  69. package/src/useCanvasAutorun.test.tsx +98 -0
  70. package/src/useCanvasAutorun.ts +30 -11
  71. package/src/version.ts +1 -1
@@ -1,24 +1,14 @@
1
- import { isBlank } from './util.js';
2
1
  /**
3
- * Convert a sequence position (ungapped, 0-based) to a global column index.
4
- * This finds the global column that contains the Nth non-gap character.
2
+ * The global column of every ungapped sequence position in a row, i.e.
3
+ * index[seqPos] is the column holding the seqPos-th non-gap character.
5
4
  *
6
- * @param row - The row's sequence string (including gaps)
7
- * @param seqPos - The sequence position (0-based count of non-gap characters)
8
- * @returns The global column index containing the seqPos-th non-gap character
5
+ * A row is indexed once and then answers seqPos -> column by lookup, which is
6
+ * what makes the domain overlay affordable: it resolves one position per feature
7
+ * per row on every redraw, and scanning the row for each would be quadratic.
9
8
  *
10
9
  * @example
11
10
  * // Row: "A-TG-C" (A at 0, T at 2, G at 3, C at 5)
12
- * seqPosToGlobalCol({ row: "A-TG-C", seqPos: 0 }) // → 0 (A)
13
- * seqPosToGlobalCol({ row: "A-TG-C", seqPos: 1 }) // → 2 (T)
14
- * seqPosToGlobalCol({ row: "A-TG-C", seqPos: 2 }) // → 3 (G)
15
- * seqPosToGlobalCol({ row: "A-TG-C", seqPos: 3 }) // → 5 (C)
16
- */
17
- /**
18
- * The global column of every ungapped sequence position in a row, i.e.
19
- * index[seqPos] is the column holding the seqPos-th non-gap character. Amortizes
20
- * repeated seqPosToGlobalCol lookups (the domain overlay does one per feature
21
- * per row) into a single pass per row.
11
+ * buildSeqPosIndex('A-TG-C') // → [0, 2, 3, 5]
22
12
  */
23
13
  export function buildSeqPosIndex(row) {
24
14
  const index = new Int32Array(row.length);
@@ -31,21 +21,4 @@ export function buildSeqPosIndex(row) {
31
21
  }
32
22
  return index.subarray(0, n);
33
23
  }
34
- export function seqPosToGlobalCol({ row, seqPos, }) {
35
- let nonGapCount = 0;
36
- let globalCol = 0;
37
- // Find the seqPos-th non-gap character
38
- while (globalCol < row.length) {
39
- if (!isBlank(row[globalCol])) {
40
- if (nonGapCount === seqPos) {
41
- return globalCol;
42
- }
43
- nonGapCount++;
44
- }
45
- globalCol++;
46
- }
47
- // If seqPos is 0 and we didn't find any non-gap character, return 0
48
- // Otherwise return globalCol (which is row.length at this point)
49
- return seqPos === 0 ? 0 : globalCol;
50
- }
51
24
  //# sourceMappingURL=seqPosToGlobalCol.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"seqPosToGlobalCol.js","sourceRoot":"","sources":["../src/seqPosToGlobalCol.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAEnC;;;;;;;;;;;;;;GAcG;AACH;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IACxC,IAAI,CAAC,GAAG,CAAC,CAAA;IACT,KAAK,IAAI,GAAG,GAAG,CAAC,EAAE,GAAG,GAAG,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE,CAAC;QAC1C,oEAAoE;QACpE,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;YAC7C,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,GAAG,CAAA;QAClB,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;AAC7B,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,EAChC,GAAG,EACH,MAAM,GAIP;IACC,IAAI,WAAW,GAAG,CAAC,CAAA;IACnB,IAAI,SAAS,GAAG,CAAC,CAAA;IACjB,uCAAuC;IACvC,OAAO,SAAS,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC;QAC9B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC;YAC7B,IAAI,WAAW,KAAK,MAAM,EAAE,CAAC;gBAC3B,OAAO,SAAS,CAAA;YAClB,CAAC;YACD,WAAW,EAAE,CAAA;QACf,CAAC;QACD,SAAS,EAAE,CAAA;IACb,CAAC;IACD,oEAAoE;IACpE,iEAAiE;IACjE,OAAO,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;AACrC,CAAC"}
1
+ {"version":3,"file":"seqPosToGlobalCol.js","sourceRoot":"","sources":["../src/seqPosToGlobalCol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IACxC,IAAI,CAAC,GAAG,CAAC,CAAA;IACT,KAAK,IAAI,GAAG,GAAG,CAAC,EAAE,GAAG,GAAG,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE,CAAC;QAC1C,oEAAoE;QACpE,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;YAC7C,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,GAAG,CAAA;QAClB,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;AAC7B,CAAC"}
@@ -1,2 +1,24 @@
1
1
  import type React from 'react';
2
- export declare function useCanvasAutorun(draw: (ctx: CanvasRenderingContext2D) => void, deps: React.DependencyList): React.RefObject<HTMLCanvasElement | null>;
2
+ /**
3
+ * Wires a canvas ref to a mobx autorun: grabs the 2d context once the canvas
4
+ * mounts and re-runs `draw` whenever any observable it reads changes. Collapses
5
+ * the getContext/null-guard/autorun-dispose boilerplate repeated by every canvas
6
+ * block host.
7
+ *
8
+ * `width`/`height` are the canvas *backing store* size the caller renders onto
9
+ * the element (logical size times the high-res scale factor), and they are load
10
+ * bearing rather than informational. Assigning either resets the canvas bitmap,
11
+ * and React assigns them on every commit where they changed -- which lands
12
+ * *after* the mobx reaction that already redrew at the old size. Without a redraw
13
+ * on the far side of that commit the canvas is left blank: it is what blanked the
14
+ * tree on a tree-area resize and the consensus tracks on a vertical zoom.
15
+ *
16
+ * `deps` covers everything else the effect should re-subscribe on, matching a
17
+ * normal useEffect dep array.
18
+ */
19
+ export declare function useCanvasAutorun({ draw, width, height, deps, }: {
20
+ draw: (ctx: CanvasRenderingContext2D) => void;
21
+ width: number;
22
+ height: number;
23
+ deps: React.DependencyList;
24
+ }): React.RefObject<HTMLCanvasElement | null>;
@@ -1,14 +1,26 @@
1
1
  import { useEffect, useRef } from 'react';
2
2
  import { autorun } from 'mobx';
3
- // Wires a canvas ref to a mobx autorun: grabs the 2d context once the canvas
4
- // mounts and re-runs `draw` whenever any observable it reads changes. Collapses
5
- // the getContext/null-guard/autorun-dispose boilerplate repeated by every
6
- // canvas block host. `deps` controls when the effect (and thus the context
7
- // lookup) is torn down and re-created, matching a normal useEffect dep array.
8
- export function useCanvasAutorun(draw, deps) {
3
+ /**
4
+ * Wires a canvas ref to a mobx autorun: grabs the 2d context once the canvas
5
+ * mounts and re-runs `draw` whenever any observable it reads changes. Collapses
6
+ * the getContext/null-guard/autorun-dispose boilerplate repeated by every canvas
7
+ * block host.
8
+ *
9
+ * `width`/`height` are the canvas *backing store* size the caller renders onto
10
+ * the element (logical size times the high-res scale factor), and they are load
11
+ * bearing rather than informational. Assigning either resets the canvas bitmap,
12
+ * and React assigns them on every commit where they changed -- which lands
13
+ * *after* the mobx reaction that already redrew at the old size. Without a redraw
14
+ * on the far side of that commit the canvas is left blank: it is what blanked the
15
+ * tree on a tree-area resize and the consensus tracks on a vertical zoom.
16
+ *
17
+ * `deps` covers everything else the effect should re-subscribe on, matching a
18
+ * normal useEffect dep array.
19
+ */
20
+ export function useCanvasAutorun({ draw, width, height, deps, }) {
9
21
  const ref = useRef(null);
10
22
  // the autorun tracks its own observable reads, so `draw` is intentionally not
11
- // a dependency; `deps` alone controls when the effect re-subscribes
23
+ // a dependency; `deps` plus the canvas size control when it re-subscribes
12
24
  useEffect(() => {
13
25
  const ctx = ref.current?.getContext('2d');
14
26
  return ctx
@@ -17,7 +29,7 @@ export function useCanvasAutorun(draw, deps) {
17
29
  })
18
30
  : undefined;
19
31
  // eslint-disable-next-line react-hooks/exhaustive-deps
20
- }, deps);
32
+ }, [...deps, width, height]);
21
33
  return ref;
22
34
  }
23
35
  //# sourceMappingURL=useCanvasAutorun.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useCanvasAutorun.js","sourceRoot":"","sources":["../src/useCanvasAutorun.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAEzC,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAI9B,6EAA6E;AAC7E,gFAAgF;AAChF,0EAA0E;AAC1E,2EAA2E;AAC3E,8EAA8E;AAC9E,MAAM,UAAU,gBAAgB,CAC9B,IAA6C,EAC7C,IAA0B;IAE1B,MAAM,GAAG,GAAG,MAAM,CAAoB,IAAI,CAAC,CAAA;IAC3C,8EAA8E;IAC9E,oEAAoE;IACpE,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,EAAE,UAAU,CAAC,IAAI,CAAC,CAAA;QACzC,OAAO,GAAG;YACR,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE;gBACX,IAAI,CAAC,GAAG,CAAC,CAAA;YACX,CAAC,CAAC;YACJ,CAAC,CAAC,SAAS,CAAA;QACb,uDAAuD;IACzD,CAAC,EAAE,IAAI,CAAC,CAAA;IACR,OAAO,GAAG,CAAA;AACZ,CAAC"}
1
+ {"version":3,"file":"useCanvasAutorun.js","sourceRoot":"","sources":["../src/useCanvasAutorun.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAEzC,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAI9B;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,gBAAgB,CAAC,EAC/B,IAAI,EACJ,KAAK,EACL,MAAM,EACN,IAAI,GAML;IACC,MAAM,GAAG,GAAG,MAAM,CAAoB,IAAI,CAAC,CAAA;IAC3C,8EAA8E;IAC9E,0EAA0E;IAC1E,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,EAAE,UAAU,CAAC,IAAI,CAAC,CAAA;QACzC,OAAO,GAAG;YACR,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE;gBACX,IAAI,CAAC,GAAG,CAAC,CAAA;YACX,CAAC,CAAC;YACJ,CAAC,CAAC,SAAS,CAAA;QACb,uDAAuD;IACzD,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAA;IAC5B,OAAO,GAAG,CAAA;AACZ,CAAC"}
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const version = "5.9.0";
1
+ export declare const version = "5.10.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
- export const version = '5.9.0';
1
+ export const version = '5.10.0';
2
2
  //# sourceMappingURL=version.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAA"}
1
+ {"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,OAAO,GAAG,QAAQ,CAAA"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-msaview",
3
3
  "author": "Colin",
4
- "version": "5.9.0",
4
+ "version": "5.10.0",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "repository": {
@@ -43,8 +43,8 @@
43
43
  "@gmod/newick": "^1.0.0",
44
44
  "colord": "^2.9.3",
45
45
  "flatbush": "^4.6.2",
46
- "@jbrowse/svgcanvas": "5.9.0",
47
- "msa-parsers": "5.9.0"
46
+ "@jbrowse/svgcanvas": "5.10.0",
47
+ "msa-parsers": "5.10.0"
48
48
  },
49
49
  "scripts": {
50
50
  "clean": "rimraf dist bundle",
@@ -25,9 +25,9 @@ const ConservationBlock = observer(function ({
25
25
  }) {
26
26
  const { blockSize, scrollX, highResScaleFactor } = model
27
27
 
28
- const ref = useCanvasAutorun(
29
- ctx => {
30
- const { blockSize, colWidth, highResScaleFactor } = model
28
+ const ref = useCanvasAutorun({
29
+ draw: ctx => {
30
+ const { colWidth } = model
31
31
  ctx.resetTransform()
32
32
  ctx.scale(highResScaleFactor, highResScaleFactor)
33
33
  ctx.clearRect(0, 0, blockSize, trackHeight)
@@ -43,8 +43,10 @@ const ConservationBlock = observer(function ({
43
43
  blockSize,
44
44
  })
45
45
  },
46
- [model, track, offsetX, trackHeight],
47
- )
46
+ width: blockSize * highResScaleFactor,
47
+ height: trackHeight * highResScaleFactor,
48
+ deps: [model, track, offsetX, trackHeight],
49
+ })
48
50
 
49
51
  return (
50
52
  <canvas
@@ -11,6 +11,7 @@ import VerticalScrollbar from './VerticalScrollbar.tsx'
11
11
  import Header from './header/Header.tsx'
12
12
  import Minimap from './minimap/Minimap.tsx'
13
13
  import MSAPanel from './msa/MSAPanel.tsx'
14
+ import { clickColor, hoverColor } from './overlayColors.ts'
14
15
  import TreePanel from './tree/TreePanel.tsx'
15
16
  import TreeRuler from './tree/TreeRuler.tsx'
16
17
 
@@ -43,47 +44,40 @@ const TrackColumnIndicator = observer(function ({
43
44
  verticalScrollbarWidth,
44
45
  } = model
45
46
 
46
- const left = treeAreaWidth + resizeHandleWidth
47
- const clipWidth = msaAreaWidth - verticalScrollbarWidth
47
+ // hovered column, then the pinned one, matching the alignment's own overlay
48
+ const bands = [
49
+ { col: mouseCol, color: hoverColor },
50
+ { col: mouseClickCol, color: clickColor },
51
+ ]
48
52
 
49
53
  return (
50
54
  <div
51
55
  style={{
52
56
  position: 'absolute',
53
- left,
57
+ left: treeAreaWidth + resizeHandleWidth,
54
58
  top: 0,
55
- width: clipWidth,
59
+ width: msaAreaWidth - verticalScrollbarWidth,
56
60
  height: totalTrackAreaHeight,
57
61
  overflow: 'hidden',
58
62
  pointerEvents: 'none',
59
63
  }}
60
64
  >
61
- {mouseCol !== undefined ? (
62
- <div
63
- style={{
64
- position: 'absolute',
65
- left: mouseCol * colWidth + scrollX,
66
- top: 0,
67
- width: colWidth,
68
- height: totalTrackAreaHeight,
69
- backgroundColor: 'rgba(0,0,0,0.15)',
70
- zIndex: 100,
71
- }}
72
- />
73
- ) : null}
74
- {mouseClickCol !== undefined ? (
75
- <div
76
- style={{
77
- position: 'absolute',
78
- left: mouseClickCol * colWidth + scrollX,
79
- top: 0,
80
- width: colWidth,
81
- height: totalTrackAreaHeight,
82
- backgroundColor: 'rgba(128,128,0,0.2)',
83
- zIndex: 100,
84
- }}
85
- />
86
- ) : null}
65
+ {bands.map(({ col, color }) =>
66
+ col === undefined ? null : (
67
+ <div
68
+ key={color}
69
+ style={{
70
+ position: 'absolute',
71
+ left: col * colWidth + scrollX,
72
+ top: 0,
73
+ width: colWidth,
74
+ height: totalTrackAreaHeight,
75
+ backgroundColor: color,
76
+ zIndex: 100,
77
+ }}
78
+ />
79
+ ),
80
+ )}
87
81
  </div>
88
82
  )
89
83
  })
@@ -32,16 +32,9 @@ const AnnotationBlock = observer(function ({
32
32
 
33
33
  const colorScheme = customColorScheme ?? modelColorScheme
34
34
  const { theme, contrastScheme } = useColorContrast(colorScheme)
35
- const ref = useCanvasAutorun(
36
- ctx => {
37
- const {
38
- blockSize,
39
- bgColor,
40
- colWidth,
41
- fontSize,
42
- rowHeight,
43
- highResScaleFactor,
44
- } = model
35
+ const ref = useCanvasAutorun({
36
+ draw: ctx => {
37
+ const { bgColor, colWidth, fontSize } = model
45
38
  ctx.resetTransform()
46
39
  ctx.scale(highResScaleFactor, highResScaleFactor)
47
40
  ctx.clearRect(0, 0, blockSize, rowHeight)
@@ -62,8 +55,10 @@ const AnnotationBlock = observer(function ({
62
55
  blockSize,
63
56
  })
64
57
  },
65
- [model, offsetX, theme, contrastScheme, colorScheme, data],
66
- )
58
+ width: blockSize * highResScaleFactor,
59
+ height: rowHeight * highResScaleFactor,
60
+ deps: [model, offsetX, theme, contrastScheme, colorScheme, data],
61
+ })
67
62
  return (
68
63
  <canvas
69
64
  ref={ref}
@@ -1,41 +1,34 @@
1
1
  import type { MsaViewModel } from '../../model.ts'
2
2
  import type { MenuItem } from '@jbrowse/core/ui/Menu'
3
3
 
4
+ function toggle(label: string, checked: boolean, set: (arg: boolean) => void) {
5
+ return {
6
+ label,
7
+ type: 'checkbox' as const,
8
+ checked,
9
+ onClick: () => {
10
+ set(!checked)
11
+ },
12
+ }
13
+ }
14
+
4
15
  export function msaSettingsMenuItems(model: MsaViewModel): MenuItem[] {
5
16
  const { drawMsaLetters, hideGaps, bgColor, showColumnStats } = model
6
17
  return [
7
- {
8
- label: 'Show column statistics on hover',
9
- type: 'checkbox',
10
- checked: showColumnStats,
11
- onClick: () => {
12
- model.setShowColumnStats(!showColumnStats)
13
- },
14
- },
15
- {
16
- label: 'Draw letters',
17
- type: 'checkbox',
18
- checked: drawMsaLetters,
19
- onClick: () => {
20
- model.setDrawMsaLetters(!drawMsaLetters)
21
- },
22
- },
23
- {
24
- label: 'Color letters instead of background of tiles',
25
- type: 'checkbox',
26
- checked: !bgColor,
27
- onClick: () => {
28
- model.setBgColor(!bgColor)
29
- },
30
- },
31
- {
32
- label: 'Enable hiding gappy columns?',
33
- type: 'checkbox',
34
- checked: hideGaps,
35
- onClick: () => {
36
- model.setHideGaps(!hideGaps)
37
- },
38
- },
18
+ toggle('Show column statistics on hover', showColumnStats, arg => {
19
+ model.setShowColumnStats(arg)
20
+ }),
21
+ toggle('Draw letters', drawMsaLetters, arg => {
22
+ model.setDrawMsaLetters(arg)
23
+ }),
24
+ // the checkbox reads as the inverse of the property it sets: bgColor draws
25
+ // the tile, and turning it off is what leaves the letter itself colored
26
+ toggle('Color letters instead of background of tiles', !bgColor, arg => {
27
+ model.setBgColor(!arg)
28
+ }),
29
+ toggle('Enable hiding gappy columns?', hideGaps, arg => {
30
+ model.setHideGaps(arg)
31
+ }),
39
32
  ]
40
33
  }
41
34
 
@@ -48,45 +41,20 @@ export function treeSettingsMenuItems(model: MsaViewModel): MenuItem[] {
48
41
  drawLabels,
49
42
  } = model
50
43
  return [
51
- {
52
- label: 'Show branch length',
53
- type: 'checkbox',
54
- checked: showBranchLen,
55
- onClick: () => {
56
- model.setShowBranchLen(!showBranchLen)
57
- },
58
- },
59
- {
60
- label: 'Show tree',
61
- type: 'checkbox',
62
- checked: drawTree,
63
- onClick: () => {
64
- model.setDrawTree(!drawTree)
65
- },
66
- },
67
- {
68
- label: 'Draw clickable bubbles on tree branches',
69
- type: 'checkbox',
70
- checked: drawNodeBubbles,
71
- onClick: () => {
72
- model.setDrawNodeBubbles(!drawNodeBubbles)
73
- },
74
- },
75
- {
76
- label: 'Tree labels align right',
77
- type: 'checkbox',
78
- checked: labelsAlignRight,
79
- onClick: () => {
80
- model.setLabelsAlignRight(!labelsAlignRight)
81
- },
82
- },
83
- {
84
- label: 'Draw labels',
85
- type: 'checkbox',
86
- checked: drawLabels,
87
- onClick: () => {
88
- model.setDrawLabels(!drawLabels)
89
- },
90
- },
44
+ toggle('Show branch length', showBranchLen, arg => {
45
+ model.setShowBranchLen(arg)
46
+ }),
47
+ toggle('Show tree', drawTree, arg => {
48
+ model.setDrawTree(arg)
49
+ }),
50
+ toggle('Draw clickable bubbles on tree branches', drawNodeBubbles, arg => {
51
+ model.setDrawNodeBubbles(arg)
52
+ }),
53
+ toggle('Tree labels align right', labelsAlignRight, arg => {
54
+ model.setLabelsAlignRight(arg)
55
+ }),
56
+ toggle('Draw labels', drawLabels, arg => {
57
+ model.setDrawLabels(arg)
58
+ }),
91
59
  ]
92
60
  }
@@ -31,24 +31,17 @@ const MSACanvasBlock = observer(function ({
31
31
  },
32
32
  )
33
33
 
34
- const ref = useCanvasAutorun(
35
- ctx => {
36
- const { blockSize, highResScaleFactor, actuallyShowDomains } = model
34
+ const canvasSize = blockSize * highResScaleFactor
35
+ const ref = useCanvasAutorun({
36
+ draw: ctx => {
37
37
  ctx.resetTransform()
38
- ctx.clearRect(
39
- 0,
40
- 0,
41
- blockSize * highResScaleFactor,
42
- blockSize * highResScaleFactor,
43
- )
44
- if (actuallyShowDomains) {
45
- renderBoxFeatureCanvasBlock({
46
- ctx,
47
- offsetX,
48
- offsetY,
49
- model,
50
- })
51
- }
38
+ ctx.clearRect(0, 0, canvasSize, canvasSize)
39
+ renderBoxFeatureCanvasBlock({
40
+ ctx,
41
+ offsetX,
42
+ offsetY,
43
+ model,
44
+ })
52
45
  renderMSABlock({
53
46
  ctx,
54
47
  theme,
@@ -58,8 +51,10 @@ const MSACanvasBlock = observer(function ({
58
51
  model,
59
52
  })
60
53
  },
61
- [model, offsetX, offsetY, theme, contrastScheme],
62
- )
54
+ width: canvasSize,
55
+ height: canvasSize,
56
+ deps: [model, offsetX, offsetY, theme, contrastScheme],
57
+ })
63
58
 
64
59
  const { hoveredInsertion, mouseOverDomains, showColumnStats } = model
65
60
 
@@ -80,8 +75,8 @@ const MSACanvasBlock = observer(function ({
80
75
  onMouseLeave={() => {
81
76
  onMouseLeave()
82
77
  }}
83
- width={blockSize * highResScaleFactor}
84
- height={blockSize * highResScaleFactor}
78
+ width={canvasSize}
79
+ height={canvasSize}
85
80
  style={{
86
81
  position: 'absolute',
87
82
  top: scrollY + offsetY,
@@ -16,21 +16,25 @@ const MSAMouseoverCanvas = observer(function ({
16
16
  const { height, msaAreaWidth, verticalScrollbarWidth, highResScaleFactor } =
17
17
  model
18
18
  const width = msaAreaWidth - verticalScrollbarWidth
19
- const ref = useCanvasAutorun(
20
- ctx => {
19
+ const canvasWidth = width * highResScaleFactor
20
+ const canvasHeight = height * highResScaleFactor
21
+ const ref = useCanvasAutorun({
22
+ draw: ctx => {
21
23
  if (isAlive(model)) {
22
24
  renderMouseover({ ctx, model })
23
25
  }
24
26
  },
25
- [model],
26
- )
27
+ width: canvasWidth,
28
+ height: canvasHeight,
29
+ deps: [model],
30
+ })
27
31
 
28
32
  return (
29
33
  <canvas
30
34
  ref={ref}
31
35
  id="mouseover"
32
- width={width * highResScaleFactor}
33
- height={height * highResScaleFactor}
36
+ width={canvasWidth}
37
+ height={canvasHeight}
34
38
  style={{
35
39
  position: 'absolute',
36
40
  top: 0,
@@ -0,0 +1,51 @@
1
+ import { expect, test } from 'vitest'
2
+
3
+ import stateModelFactory from '../../model.ts'
4
+ import { renderBoxFeatureCanvasBlock } from './renderBoxFeatureCanvasBlock.ts'
5
+
6
+ import type { RenderCtx } from '../renderCtx.ts'
7
+
8
+ const row = 'ACDEFGHIKLMNPQRSTVWY'.repeat(3)
9
+
10
+ // only `human` carries the gene model; `mouse` is there to push it off the top
11
+ // of the block
12
+ const msa = `>mouse\n${row}\n>human\n${row}`
13
+
14
+ const gff = `##gff-version 3
15
+ human src exon 1 20 . . . ID=human.exon1;Name=exon-1
16
+ human src exon 21 40 . . . ID=human.exon2;Name=exon-2
17
+ human src exon 41 60 . . . ID=human.exon3;Name=exon-3`
18
+
19
+ function drawnLabels() {
20
+ const model = stateModelFactory().create({
21
+ type: 'MsaView',
22
+ data: { msa },
23
+ })
24
+ model.setWidth(1000)
25
+ model.setRowHeight(20)
26
+ // segment numbers are drawn in place of residue letters, never on top of them
27
+ model.setDrawMsaLetters(false)
28
+ model.applyGFFText(gff)
29
+ expect(model.rowNames).toEqual(['mouse', 'human'])
30
+
31
+ const labels: string[] = []
32
+ const ctx = {
33
+ font: '12px sans-serif',
34
+ resetTransform() {},
35
+ scale() {},
36
+ translate() {},
37
+ fillRect() {},
38
+ strokeRect() {},
39
+ measureText: (text: string) => ({ width: text.length * 6 }),
40
+ fillText(text: string) {
41
+ labels.push(text)
42
+ },
43
+ } as unknown as RenderCtx
44
+
45
+ renderBoxFeatureCanvasBlock({ model, ctx, offsetX: 0, offsetY: 0 })
46
+ return labels
47
+ }
48
+
49
+ test('segment numbers are drawn once each, even when row 0 has no gene model', () => {
50
+ expect(drawnLabels()).toEqual(['1', '2', '3'])
51
+ })
@@ -77,8 +77,13 @@ function drawTiles({
77
77
  const xMin = offsetX - cull
78
78
  const xMax = offsetX + blockWidth + cull
79
79
 
80
- for (let i = 0, l1 = visibleLeaves.length; i < l1; i++) {
81
- const node = visibleLeaves[i]!
80
+ // a segment (exon) number labels its band once per block, on the topmost
81
+ // visible row carrying that segment, so it reads as a column header for the
82
+ // whole band. Keyed by accession rather than drawn on row 0, because the rows
83
+ // carrying the gene model are often not the first ones in the alignment
84
+ const labelled = drawSegmentLabels ? new Set<string>() : undefined
85
+
86
+ for (const node of visibleLeaves) {
82
87
  const y = node.x!
83
88
  const bands = domainBands.get(node.data.name)
84
89
 
@@ -94,15 +99,15 @@ function drawTiles({
94
99
  if (strand === undefined) {
95
100
  ctx.fillRect(x, t, lw, h)
96
101
  ctx.strokeRect(x, t, lw, h)
97
- // segment (exon) number drawn once per band, on the top visible row,
98
- // so it reads as a column header for the whole band
99
- const label = drawSegmentLabels
100
- ? segmentLabels.get(accession)
101
- : undefined
102
- if (label !== undefined && i === 0) {
102
+ const label =
103
+ labelled && !labelled.has(accession)
104
+ ? segmentLabels.get(accession)
105
+ : undefined
106
+ if (label !== undefined) {
103
107
  const fontSize = Math.min(h - 2, 11)
104
108
  ctx.font = `${fontSize}px sans-serif`
105
109
  if (ctx.measureText(label).width + 2 <= lw) {
110
+ labelled!.add(accession)
106
111
  ctx.fillStyle = '#222'
107
112
  ctx.textAlign = 'center'
108
113
  ctx.textBaseline = 'middle'