react-msaview 8.2.0 → 8.3.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 (192) hide show
  1. package/bundle/index.js +104 -117
  2. package/bundle/index.js.map +4 -4
  3. package/dist/columnCounts.d.ts +4 -3
  4. package/dist/columnCounts.js +11 -6
  5. package/dist/columnCounts.js.map +1 -1
  6. package/dist/components/CopyButton.d.ts +1 -1
  7. package/dist/components/CopyButton.js +1 -1
  8. package/dist/components/CopyButton.js.map +1 -1
  9. package/dist/components/DragHandle.js +14 -2
  10. package/dist/components/DragHandle.js.map +1 -1
  11. package/dist/components/Loading.js +10 -11
  12. package/dist/components/Loading.js.map +1 -1
  13. package/dist/components/MSAViewer.d.ts +12 -1
  14. package/dist/components/MSAViewer.js +12 -1
  15. package/dist/components/MSAViewer.js.map +1 -1
  16. package/dist/components/SequenceTextArea.d.ts +11 -0
  17. package/dist/components/SequenceTextArea.js +40 -8
  18. package/dist/components/SequenceTextArea.js.map +1 -1
  19. package/dist/components/StatusMessage.d.ts +12 -0
  20. package/dist/components/StatusMessage.js +43 -0
  21. package/dist/components/StatusMessage.js.map +1 -0
  22. package/dist/components/Track.js +3 -3
  23. package/dist/components/Track.js.map +1 -1
  24. package/dist/components/VerticalScrollbar.js +6 -1
  25. package/dist/components/VerticalScrollbar.js.map +1 -1
  26. package/dist/components/dialogs/ExportSVGDialog.d.ts +3 -2
  27. package/dist/components/dialogs/ExportSVGDialog.js +4 -2
  28. package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
  29. package/dist/components/dialogs/FeatureDialog.js +15 -10
  30. package/dist/components/dialogs/FeatureDialog.js.map +1 -1
  31. package/dist/components/getVisibleLeaves.d.ts +1 -1
  32. package/dist/components/header/FileMenu.js +9 -0
  33. package/dist/components/header/FileMenu.js.map +1 -1
  34. package/dist/components/header/GappynessSlider.js +15 -3
  35. package/dist/components/header/GappynessSlider.js.map +1 -1
  36. package/dist/components/header/Header.js +6 -7
  37. package/dist/components/header/Header.js.map +1 -1
  38. package/dist/components/header/{HeaderStatusArea.d.ts → RowSearch.d.ts} +2 -2
  39. package/dist/components/header/RowSearch.js +15 -0
  40. package/dist/components/header/RowSearch.js.map +1 -0
  41. package/dist/components/header/revealRow.d.ts +3 -0
  42. package/dist/components/header/revealRow.js +15 -0
  43. package/dist/components/header/revealRow.js.map +1 -0
  44. package/dist/components/header/settingsMenuItems.js +57 -1
  45. package/dist/components/header/settingsMenuItems.js.map +1 -1
  46. package/dist/components/import/ImportForm.js +10 -4
  47. package/dist/components/import/ImportForm.js.map +1 -1
  48. package/dist/components/import/ImportFormExamples.js +19 -27
  49. package/dist/components/import/ImportFormExamples.js.map +1 -1
  50. package/dist/components/minimap/Minimap.js +6 -1
  51. package/dist/components/minimap/Minimap.js.map +1 -1
  52. package/dist/components/msa/AnnotationLegend.js +11 -5
  53. package/dist/components/msa/AnnotationLegend.js.map +1 -1
  54. package/dist/components/msa/MSACanvas.js +6 -1
  55. package/dist/components/msa/MSACanvas.js.map +1 -1
  56. package/dist/components/msa/MSACanvasBlock.js +3 -7
  57. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  58. package/dist/components/msa/msaKeys.d.ts +5 -0
  59. package/dist/components/msa/msaKeys.js +35 -0
  60. package/dist/components/msa/msaKeys.js.map +1 -0
  61. package/dist/components/msa/useMsaBlockMouse.d.ts +2 -4
  62. package/dist/components/msa/useMsaBlockMouse.js +18 -5
  63. package/dist/components/msa/useMsaBlockMouse.js.map +1 -1
  64. package/dist/components/rowpanels/RowPanelBlock.js +10 -14
  65. package/dist/components/rowpanels/RowPanelBlock.js.map +1 -1
  66. package/dist/components/tracks/drawTracks.d.ts +1 -0
  67. package/dist/components/tracks/drawTracks.js +14 -2
  68. package/dist/components/tracks/drawTracks.js.map +1 -1
  69. package/dist/components/tracks/useTrackHover.d.ts +1 -5
  70. package/dist/components/tracks/useTrackHover.js +5 -5
  71. package/dist/components/tracks/useTrackHover.js.map +1 -1
  72. package/dist/components/tree/ScaleBarSVG.d.ts +14 -0
  73. package/dist/components/tree/ScaleBarSVG.js +11 -0
  74. package/dist/components/tree/ScaleBarSVG.js.map +1 -0
  75. package/dist/components/tree/TreeCanvas.js +8 -26
  76. package/dist/components/tree/TreeCanvas.js.map +1 -1
  77. package/dist/components/tree/TreeCanvasBlock.js +11 -20
  78. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  79. package/dist/components/tree/{TreeBranchMenu.d.ts → TreeMenu.d.ts} +5 -4
  80. package/dist/components/tree/TreeMenu.js +48 -0
  81. package/dist/components/tree/TreeMenu.js.map +1 -0
  82. package/dist/components/tree/TreeRuler.js +3 -3
  83. package/dist/components/tree/TreeRuler.js.map +1 -1
  84. package/dist/components/tree/renderTreeCanvas.js +19 -9
  85. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  86. package/dist/components/tree/renderTreeMouseover.d.ts +5 -0
  87. package/dist/components/tree/renderTreeMouseover.js +20 -0
  88. package/dist/components/tree/renderTreeMouseover.js.map +1 -0
  89. package/dist/components/tree/useTreeHover.d.ts +3 -8
  90. package/dist/components/tree/useTreeHover.js +8 -10
  91. package/dist/components/tree/useTreeHover.js.map +1 -1
  92. package/dist/components/useHoverAnchor.d.ts +19 -0
  93. package/dist/components/useHoverAnchor.js +18 -0
  94. package/dist/components/useHoverAnchor.js.map +1 -0
  95. package/dist/constants.d.ts +4 -1
  96. package/dist/constants.js +12 -3
  97. package/dist/constants.js.map +1 -1
  98. package/dist/expandSpec.js +3 -3
  99. package/dist/expandSpec.js.map +1 -1
  100. package/dist/exportFileName.d.ts +3 -3
  101. package/dist/exportFileName.js +4 -4
  102. package/dist/exportFileName.js.map +1 -1
  103. package/dist/hierarchy.d.ts +51 -0
  104. package/dist/hierarchy.js +125 -0
  105. package/dist/hierarchy.js.map +1 -1
  106. package/dist/index.d.ts +2 -2
  107. package/dist/model/treeModel.d.ts +12 -0
  108. package/dist/model/treeModel.js +14 -1
  109. package/dist/model/treeModel.js.map +1 -1
  110. package/dist/model.d.ts +168 -47
  111. package/dist/model.js +435 -192
  112. package/dist/model.js.map +1 -1
  113. package/dist/renderToSvg.js +2 -4
  114. package/dist/renderToSvg.js.map +1 -1
  115. package/dist/rerootTree.d.ts +33 -0
  116. package/dist/rerootTree.js +144 -0
  117. package/dist/rerootTree.js.map +1 -0
  118. package/dist/svgTestUtil.d.ts +106 -20
  119. package/dist/types.d.ts +25 -12
  120. package/dist/useMsaSvgFigure.d.ts +2 -1
  121. package/dist/useMsaSvgFigure.js +11 -4
  122. package/dist/useMsaSvgFigure.js.map +1 -1
  123. package/dist/version.d.ts +1 -1
  124. package/dist/version.js +1 -1
  125. package/dist/writeNewick.d.ts +8 -0
  126. package/dist/writeNewick.js +34 -0
  127. package/dist/writeNewick.js.map +1 -0
  128. package/package.json +3 -3
  129. package/src/columnCounts.ts +14 -6
  130. package/src/components/CopyButton.tsx +2 -2
  131. package/src/components/DragHandle.tsx +21 -1
  132. package/src/components/Loading.tsx +14 -30
  133. package/src/components/MSAViewer.tsx +26 -0
  134. package/src/components/SequenceTextArea.tsx +54 -8
  135. package/src/components/StatusMessage.tsx +70 -0
  136. package/src/components/Track.tsx +8 -4
  137. package/src/components/VerticalScrollbar.tsx +7 -0
  138. package/src/components/dialogs/ExportSVGDialog.tsx +5 -2
  139. package/src/components/dialogs/FeatureDialog.tsx +59 -38
  140. package/src/components/header/FileMenu.tsx +9 -0
  141. package/src/components/header/GappynessSlider.tsx +17 -4
  142. package/src/components/header/Header.tsx +7 -8
  143. package/src/components/header/RowSearch.tsx +34 -0
  144. package/src/components/header/revealRow.ts +17 -0
  145. package/src/components/header/settingsMenuItems.ts +66 -0
  146. package/src/components/import/ImportForm.tsx +14 -7
  147. package/src/components/import/ImportFormExamples.tsx +20 -28
  148. package/src/components/minimap/Minimap.tsx +7 -0
  149. package/src/components/msa/AnnotationLegend.tsx +12 -6
  150. package/src/components/msa/MSACanvas.tsx +8 -0
  151. package/src/components/msa/MSACanvasBlock.tsx +5 -11
  152. package/src/components/msa/msaKeys.ts +39 -0
  153. package/src/components/msa/useMsaBlockMouse.ts +23 -7
  154. package/src/components/rowpanels/RowPanelBlock.tsx +16 -23
  155. package/src/components/tracks/drawTracks.ts +15 -2
  156. package/src/components/tracks/useTrackHover.ts +5 -5
  157. package/src/components/tree/ScaleBarSVG.tsx +30 -0
  158. package/src/components/tree/TreeCanvas.tsx +9 -40
  159. package/src/components/tree/TreeCanvasBlock.tsx +19 -39
  160. package/src/components/tree/TreeMenu.tsx +129 -0
  161. package/src/components/tree/TreeRuler.tsx +7 -13
  162. package/src/components/tree/renderTreeCanvas.ts +20 -8
  163. package/src/components/tree/renderTreeMouseover.ts +41 -0
  164. package/src/components/tree/useTreeHover.ts +8 -17
  165. package/src/components/useHoverAnchor.ts +31 -0
  166. package/src/constants.ts +14 -3
  167. package/src/expandSpec.ts +3 -3
  168. package/src/exportFileName.ts +4 -3
  169. package/src/hierarchy.ts +151 -4
  170. package/src/index.ts +3 -1
  171. package/src/model/treeModel.ts +22 -0
  172. package/src/model.ts +534 -223
  173. package/src/renderToSvg.tsx +6 -9
  174. package/src/rerootTree.ts +172 -0
  175. package/src/types.ts +34 -12
  176. package/src/useMsaSvgFigure.ts +11 -4
  177. package/src/version.ts +1 -1
  178. package/src/writeNewick.ts +38 -0
  179. package/dist/components/PortalTooltip.d.ts +0 -11
  180. package/dist/components/PortalTooltip.js +0 -29
  181. package/dist/components/PortalTooltip.js.map +0 -1
  182. package/dist/components/header/HeaderStatusArea.js +0 -21
  183. package/dist/components/header/HeaderStatusArea.js.map +0 -1
  184. package/dist/components/tree/TreeBranchMenu.js +0 -29
  185. package/dist/components/tree/TreeBranchMenu.js.map +0 -1
  186. package/dist/components/tree/TreeNodeMenu.d.ts +0 -13
  187. package/dist/components/tree/TreeNodeMenu.js +0 -41
  188. package/dist/components/tree/TreeNodeMenu.js.map +0 -1
  189. package/src/components/PortalTooltip.tsx +0 -47
  190. package/src/components/header/HeaderStatusArea.tsx +0 -32
  191. package/src/components/tree/TreeBranchMenu.tsx +0 -68
  192. package/src/components/tree/TreeNodeMenu.tsx +0 -95
@@ -1,12 +1,13 @@
1
1
  import { useEffect, useState } from 'react';
2
2
  import { createJBrowseTheme } from '@jbrowse/core/ui/theme';
3
3
  import { getSnapshot } from '@jbrowse/mobx-state-tree';
4
- import { compareStructural, reaction } from 'mobx';
4
+ import { compareShallow, reaction } from 'mobx';
5
5
  import { renderToSvg } from './renderToSvg.js';
6
6
  /**
7
7
  * The viewer's SVG export of its viewport, as markup for the page to insert.
8
8
  * It redraws after the view settles: `delay` ms after the last scroll, zoom,
9
- * resize or change to the snapshot. A draw a later one overtakes is dropped.
9
+ * resize, highlight or change to the snapshot. A draw a later one overtakes is
10
+ * dropped.
10
11
  *
11
12
  * The figure's row names and letters are SVG text, so the browser's find
12
13
  * locates them, a reader can select them, and print stylesheets apply.
@@ -16,7 +17,13 @@ export function useMsaSvgFigure(model, { theme, includeTracks = false, delay = 3
16
17
  useEffect(() => {
17
18
  let latest = 0;
18
19
  const dispose = reaction(() => model.viewInitialized && model.dataInitialized
19
- ? [getSnapshot(model), model.width, model.height]
20
+ ? [
21
+ getSnapshot(model),
22
+ model.width,
23
+ model.height,
24
+ model.resolvedHighlights,
25
+ model.highlightedColumnRuns,
26
+ ]
20
27
  : undefined, key => {
21
28
  if (!key) {
22
29
  return;
@@ -35,7 +42,7 @@ export function useMsaSvgFigure(model, { theme, includeTracks = false, delay = 3
35
42
  setFigure({ error });
36
43
  }
37
44
  });
38
- }, { delay, fireImmediately: true, equals: compareStructural });
45
+ }, { delay, fireImmediately: true, equals: compareShallow });
39
46
  return () => {
40
47
  latest = -1;
41
48
  dispose();
@@ -1 +1 @@
1
- {"version":3,"file":"useMsaSvgFigure.js","sourceRoot":"","sources":["../src/useMsaSvgFigure.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAE3C,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AACtD,OAAO,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAA;AAElD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAmB,CAAA;AAW/C;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAC7B,KAAmB,EACnB,EACE,KAAK,EACL,aAAa,GAAG,KAAK,EACrB,KAAK,GAAG,GAAG,GACZ,GAA+D,EAAE;IAElE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAe,EAAE,CAAC,CAAA;IACtD,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,MAAM,GAAG,CAAC,CAAA;QACd,MAAM,OAAO,GAAG,QAAQ,CACtB,GAAG,EAAE,CACH,KAAK,CAAC,eAAe,IAAI,KAAK,CAAC,eAAe;YAC5C,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC;YACjD,CAAC,CAAC,SAAS,EACf,GAAG,CAAC,EAAE;YACJ,IAAI,CAAC,GAAG,EAAE,CAAC;gBACT,OAAM;YACR,CAAC;YACD,MAAM,IAAI,GAAG,EAAE,MAAM,CAAA;YACrB,WAAW,CAAC,KAAK,EAAE;gBACjB,KAAK,EAAE,KAAK,IAAI,kBAAkB,EAAE;gBACpC,UAAU,EAAE,UAAU;gBACtB,aAAa;aACd,CAAC,CAAC,IAAI,CACL,GAAG,CAAC,EAAE;gBACJ,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;oBACpB,SAAS,CAAC,EAAE,GAAG,EAAE,CAAC,CAAA;gBACpB,CAAC;YACH,CAAC,EACD,CAAC,KAAc,EAAE,EAAE;gBACjB,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;oBACpB,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC,CAAA;gBACtB,CAAC;YACH,CAAC,CACF,CAAA;QACH,CAAC,EACD,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAC5D,CAAA;QACD,OAAO,GAAG,EAAE;YACV,MAAM,GAAG,CAAC,CAAC,CAAA;YACX,OAAO,EAAE,CAAA;QACX,CAAC,CAAA;IACH,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,KAAK,CAAC,CAAC,CAAA;IACxC,OAAO,MAAM,CAAA;AACf,CAAC"}
1
+ {"version":3,"file":"useMsaSvgFigure.js","sourceRoot":"","sources":["../src/useMsaSvgFigure.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAE3C,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AACtD,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAA;AAE/C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAmB,CAAA;AAW/C;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAC7B,KAAmB,EACnB,EACE,KAAK,EACL,aAAa,GAAG,KAAK,EACrB,KAAK,GAAG,GAAG,GACZ,GAA+D,EAAE;IAElE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAe,EAAE,CAAC,CAAA;IACtD,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,MAAM,GAAG,CAAC,CAAA;QACd,MAAM,OAAO,GAAG,QAAQ,CACtB,GAAG,EAAE,CACH,KAAK,CAAC,eAAe,IAAI,KAAK,CAAC,eAAe;YAC5C,CAAC,CAAC;gBACE,WAAW,CAAC,KAAK,CAAC;gBAClB,KAAK,CAAC,KAAK;gBACX,KAAK,CAAC,MAAM;gBACZ,KAAK,CAAC,kBAAkB;gBACxB,KAAK,CAAC,qBAAqB;aAC5B;YACH,CAAC,CAAC,SAAS,EACf,GAAG,CAAC,EAAE;YACJ,IAAI,CAAC,GAAG,EAAE,CAAC;gBACT,OAAM;YACR,CAAC;YACD,MAAM,IAAI,GAAG,EAAE,MAAM,CAAA;YACrB,WAAW,CAAC,KAAK,EAAE;gBACjB,KAAK,EAAE,KAAK,IAAI,kBAAkB,EAAE;gBACpC,UAAU,EAAE,UAAU;gBACtB,aAAa;aACd,CAAC,CAAC,IAAI,CACL,GAAG,CAAC,EAAE;gBACJ,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;oBACpB,SAAS,CAAC,EAAE,GAAG,EAAE,CAAC,CAAA;gBACpB,CAAC;YACH,CAAC,EACD,CAAC,KAAc,EAAE,EAAE;gBACjB,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;oBACpB,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC,CAAA;gBACtB,CAAC;YACH,CAAC,CACF,CAAA;QACH,CAAC,EACD,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,CACzD,CAAA;QACD,OAAO,GAAG,EAAE;YACV,MAAM,GAAG,CAAC,CAAC,CAAA;YACX,OAAO,EAAE,CAAA;QACX,CAAC,CAAA;IACH,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,KAAK,CAAC,CAAC,CAAA;IACxC,OAAO,MAAM,CAAA;AACf,CAAC"}
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const version = "8.2.0";
1
+ export declare const version = "8.3.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
- export const version = '8.2.0';
1
+ export const version = '8.3.0';
2
2
  //# sourceMappingURL=version.js.map
@@ -0,0 +1,8 @@
1
+ import type { HierarchyNode } from './hierarchy.ts';
2
+ /**
3
+ * The tree as Newick, in the order it is drawn. An internal node whose name is
4
+ * only its generated id goes unnamed. Each subtree's string is dropped once its
5
+ * parent takes it in, since a caterpillar tree is as deep as it has tips and
6
+ * keeping every one is quadratic.
7
+ */
8
+ export declare function writeNewick(root: HierarchyNode): string;
@@ -0,0 +1,34 @@
1
+ import { descendants } from './hierarchy.js';
2
+ // a name holding any of these reads back as grammar unless it is quoted
3
+ const needsQuotes = /[\s(),:;[\]']/;
4
+ function quote(name) {
5
+ return needsQuotes.test(name) ? `'${name.replaceAll("'", "''")}'` : name;
6
+ }
7
+ /**
8
+ * The tree as Newick, in the order it is drawn. An internal node whose name is
9
+ * only its generated id goes unnamed. Each subtree's string is dropped once its
10
+ * parent takes it in, since a caterpillar tree is as deep as it has tips and
11
+ * keeping every one is quadratic.
12
+ */
13
+ export function writeNewick(root) {
14
+ const written = new Map();
15
+ for (const node of descendants(root).reverse()) {
16
+ const { id, name, length } = node.data;
17
+ let text = '';
18
+ if (node.children) {
19
+ text = `(${node.children.map(c => written.get(c)).join(',')})`;
20
+ for (const child of node.children) {
21
+ written.delete(child);
22
+ }
23
+ }
24
+ if (!(node.children && name === id)) {
25
+ text += quote(name);
26
+ }
27
+ if (length !== undefined) {
28
+ text += `:${length}`;
29
+ }
30
+ written.set(node, text);
31
+ }
32
+ return `${written.get(root)};`;
33
+ }
34
+ //# sourceMappingURL=writeNewick.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"writeNewick.js","sourceRoot":"","sources":["../src/writeNewick.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAI5C,wEAAwE;AACxE,MAAM,WAAW,GAAG,eAAe,CAAA;AAEnC,SAAS,KAAK,CAAC,IAAY;IACzB,OAAO,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAA;AAC1E,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,IAAmB;IAC7C,MAAM,OAAO,GAAG,IAAI,GAAG,EAAyB,CAAA;IAChD,KAAK,MAAM,IAAI,IAAI,WAAW,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC;QAC/C,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,IAAI,CAAA;QACtC,IAAI,IAAI,GAAG,EAAE,CAAA;QACb,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,IAAI,GAAG,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAA;YAC/D,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAClC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;YACvB,CAAC;QACH,CAAC;QACD,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,KAAK,EAAE,CAAC,EAAE,CAAC;YACpC,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,CAAA;QACrB,CAAC;QACD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,IAAI,IAAI,IAAI,MAAM,EAAE,CAAA;QACtB,CAAC;QACD,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;IACzB,CAAC;IACD,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAE,GAAG,CAAA;AACjC,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-msaview",
3
3
  "author": "Colin",
4
- "version": "8.2.0",
4
+ "version": "8.3.0",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "repository": {
@@ -50,10 +50,10 @@
50
50
  },
51
51
  "dependencies": {
52
52
  "@gmod/newick": "^1.0.3",
53
- "@jbrowse/svgcanvas": "8.2.0",
53
+ "@jbrowse/svgcanvas": "8.3.0",
54
54
  "colord": "^2.10.0",
55
55
  "flatbush": "^4.6.2",
56
- "msa-parsers": "8.2.0"
56
+ "msa-parsers": "8.3.0"
57
57
  },
58
58
  "scripts": {
59
59
  "clean": "rimraf dist bundle",
@@ -63,15 +63,21 @@ export class ColumnCounts {
63
63
  this.totals = new Uint32Array(numColumns)
64
64
  }
65
65
 
66
+ /** tallies a row, counting the columns past a short row's end as gaps */
66
67
  addRow(row: string) {
67
- const { counts, totals, slotTotals } = this
68
- const n = Math.min(row.length, this.numColumns)
68
+ const { counts, totals, slotTotals, numColumns } = this
69
+ const n = Math.min(row.length, numColumns)
69
70
  for (let col = 0; col < n; col++) {
70
71
  const slot = slotOf(row.charCodeAt(col))
71
72
  counts[col * SLOTS + slot]!++
72
73
  totals[col]!++
73
74
  slotTotals[slot]!++
74
75
  }
76
+ for (let col = n; col < numColumns; col++) {
77
+ counts[col * SLOTS + DASH]!++
78
+ totals[col]!++
79
+ }
80
+ slotTotals[DASH]! += numColumns - n
75
81
  }
76
82
 
77
83
  add(col: number, letter: string) {
@@ -161,11 +167,13 @@ export const numResidueSlots = SLOTS
161
167
 
162
168
  /**
163
169
  * Tally counts from alignment rows (gapped sequence strings, one per row). The
164
- * column count follows the longest row, matching what a per-column tally of
165
- * ragged input produces.
170
+ * column count defaults to the longest row, and a shorter row counts as gapped
171
+ * to the end.
166
172
  */
167
- export function columnCountsFromRows(rows: string[]) {
168
- const numColumns = rows.reduce((max, row) => Math.max(max, row.length), 0)
173
+ export function columnCountsFromRows(
174
+ rows: string[],
175
+ numColumns = rows.reduce((max, row) => Math.max(max, row.length), 0),
176
+ ) {
169
177
  const counts = new ColumnCounts(numColumns)
170
178
  for (const row of rows) {
171
179
  counts.addRow(row)
@@ -12,7 +12,7 @@ export default function CopyButton({
12
12
  text,
13
13
  label = 'Copy to clipboard',
14
14
  }: {
15
- text: string
15
+ text: string | (() => string)
16
16
  label?: string
17
17
  }) {
18
18
  const [copied, setCopied] = useState(false)
@@ -30,7 +30,7 @@ export default function CopyButton({
30
30
  variant="contained"
31
31
  color="primary"
32
32
  onClick={() => {
33
- copy(text)
33
+ copy(typeof text === 'string' ? text : text())
34
34
  setCopied(true)
35
35
  clearTimeout(timer.current)
36
36
  timer.current = setTimeout(() => {
@@ -8,6 +8,10 @@ import type { Theme } from '@mui/material'
8
8
 
9
9
  export const scrollbarThumbFill = 'rgba(66,119,127,0.3)'
10
10
 
11
+ // the model's resizeHandleWidth feeds the layout, so a divider keeps its drawn
12
+ // width and widens its target with this much invisible margin each side
13
+ const hitSlop = 4
14
+
11
15
  // The scroll thumbs (minimap, vertical scrollbar) and the resize dividers are
12
16
  // one widget -- press, drag along an axis, report the pixel delta -- differing
13
17
  // only in the affordance they present, so the variant carries the whole
@@ -71,6 +75,10 @@ export default function DragHandle({
71
75
 
72
76
  return (
73
77
  <div
78
+ {...(variant === 'resizer' && {
79
+ role: 'separator',
80
+ 'aria-orientation': axis === 'x' ? 'vertical' : 'horizontal',
81
+ })}
74
82
  onMouseEnter={() => {
75
83
  setHovered(true)
76
84
  }}
@@ -87,6 +95,18 @@ export default function DragHandle({
87
95
  background: hovered ? fillHovered(theme) : fill(theme),
88
96
  ...style,
89
97
  }}
90
- />
98
+ >
99
+ {variant === 'resizer' ? (
100
+ <div
101
+ style={{
102
+ position: 'absolute',
103
+ zIndex: 101,
104
+ ...(axis === 'x'
105
+ ? { top: 0, bottom: 0, left: -hitSlop, right: -hitSlop }
106
+ : { left: 0, right: 0, top: -hitSlop, bottom: -hitSlop }),
107
+ }}
108
+ />
109
+ ) : null}
110
+ </div>
91
111
  )
92
112
  }
@@ -2,35 +2,15 @@ import React from 'react'
2
2
 
3
3
  import { ErrorMessage } from '@jbrowse/core/ui'
4
4
  import { ErrorBoundary } from '@jbrowse/core/ui/ErrorBoundary'
5
- import { Button, CircularProgress, Typography } from '@mui/material'
5
+ import { Button } from '@mui/material'
6
6
  import { observer } from 'mobx-react'
7
7
 
8
8
  import MSAView from './MSAView.tsx'
9
+ import StatusMessage from './StatusMessage.tsx'
9
10
  import ImportForm from './import/ImportForm.tsx'
10
11
 
11
12
  import type { MsaViewModel } from '../model.ts'
12
13
 
13
- const LoadingSpinner = observer(function ({ model }: { model: MsaViewModel }) {
14
- const { status } = model
15
- return (
16
- <div
17
- style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 20 }}
18
- >
19
- <CircularProgress size={24} />
20
- <Typography variant="h6">{status?.msg ?? 'Loading...'}</Typography>
21
- {status?.onCancel ? (
22
- <Button
23
- variant="outlined"
24
- size="small"
25
- onClick={() => status.onCancel?.()}
26
- >
27
- Cancel
28
- </Button>
29
- ) : null}
30
- </div>
31
- )
32
- })
33
-
34
14
  const Reset = observer(function ({
35
15
  model,
36
16
  error,
@@ -58,12 +38,15 @@ const Loading = observer(function ({ model }: { model: MsaViewModel }) {
58
38
  const {
59
39
  isLoading,
60
40
  dataInitialized,
41
+ data,
61
42
  error,
62
43
  resetCount,
63
44
  msaFilehandle,
64
45
  treeFilehandle,
65
46
  } = model
66
- const hasPendingFilehandle = !!(msaFilehandle || treeFilehandle)
47
+ const hasData = !!(data.msa || data.tree)
48
+ // a failed load keeps its filehandle, so the error outranks the spinner
49
+ const loading = !error && !!(msaFilehandle || treeFilehandle || isLoading)
67
50
 
68
51
  return (
69
52
  <div>
@@ -76,18 +59,19 @@ const Loading = observer(function ({ model }: { model: MsaViewModel }) {
76
59
  >
77
60
  {dataInitialized ? (
78
61
  isLoading ? (
79
- <LoadingSpinner model={model} />
62
+ <StatusMessage model={model} variant="page" />
80
63
  ) : (
81
64
  <MSAView model={model} />
82
65
  )
83
- ) : error ? (
84
- // checked before the spinner: a failed load keeps its filehandle
85
- // (only an abort clears it), so hasPendingFilehandle stays true
66
+ ) : hasData ? (
86
67
  <Reset model={model} error={error} />
87
- ) : hasPendingFilehandle || isLoading ? (
88
- <LoadingSpinner model={model} />
89
68
  ) : (
90
- <ImportForm model={model} />
69
+ <>
70
+ {loading ? <StatusMessage model={model} variant="page" /> : null}
71
+ <div style={{ display: loading ? 'none' : undefined }}>
72
+ <ImportForm model={model} />
73
+ </div>
74
+ </>
91
75
  )}
92
76
  </ErrorBoundary>
93
77
  </div>
@@ -20,6 +20,8 @@ import type {
20
20
  ResidueEncoding,
21
21
  ResidueMapping,
22
22
  RowPanelSpec,
23
+ TreeOrder,
24
+ TreeRoot,
23
25
  Viewport,
24
26
  } from '../types.ts'
25
27
  import type { FileLocation as FileLocationType } from '@jbrowse/core/util/types'
@@ -94,6 +96,17 @@ export interface MSAViewerProps {
94
96
  autoTreeAreaWidth?: boolean
95
97
  /** draw branch lengths (default true); false draws a cladogram */
96
98
  showBranchLen?: boolean
99
+ /**
100
+ * the order each node's children draw in: `branchLength` (default,
101
+ * shortest first), `input` (as the file gives them), `ladderize` (fewest
102
+ * tips first) or `ladderizeReverse` (most tips first)
103
+ */
104
+ treeOrder?: TreeOrder
105
+ /**
106
+ * reroot the tree: `midpoint`, or `{outgroup: [tip names]}` on the branch
107
+ * above those tips' common ancestor
108
+ */
109
+ treeRoot?: TreeRoot
97
110
  /**
98
111
  * which channel `colorScheme` paints: `fill` (default) colors the background
99
112
  * of the cell a residue sits in, `color` colors the letter itself and leaves
@@ -215,6 +228,8 @@ function Viewer({
215
228
  treeAreaWidth,
216
229
  autoTreeAreaWidth,
217
230
  showBranchLen,
231
+ treeOrder,
232
+ treeRoot,
218
233
  region,
219
234
  hideHeader,
220
235
  theme,
@@ -258,6 +273,8 @@ function Viewer({
258
273
  ...(treeAreaWidth ? { treeAreaWidth } : {}),
259
274
  ...(autoTreeAreaWidth ? { autoTreeAreaWidth } : {}),
260
275
  ...(showBranchLen !== undefined ? { showBranchLen } : {}),
276
+ ...(treeOrder ? { treeOrder } : {}),
277
+ ...(treeRoot ? { treeRoot } : {}),
261
278
  ...(residueEncoding !== undefined
262
279
  ? { bgColor: residueEncoding === 'fill' }
263
280
  : {}),
@@ -317,6 +334,11 @@ function Viewer({
317
334
  model.setShowBranchLen(showBranchLen)
318
335
  }
319
336
  }, [model, showBranchLen])
337
+ useEffect(() => {
338
+ if (treeOrder !== undefined) {
339
+ model.setTreeOrder(treeOrder)
340
+ }
341
+ }, [model, treeOrder])
320
342
  useEffect(() => {
321
343
  if (residueEncoding !== undefined) {
322
344
  model.setBgColor(residueEncoding === 'fill')
@@ -336,6 +358,10 @@ function Viewer({
336
358
  useEffect(() => {
337
359
  model.setHighlights(JSON.parse(highlightsKey))
338
360
  }, [model, highlightsKey])
361
+ const treeRootKey = JSON.stringify(treeRoot ?? null)
362
+ useEffect(() => {
363
+ model.setTreeRoot(JSON.parse(treeRootKey) ?? undefined)
364
+ }, [model, treeRootKey])
339
365
  const cladesKey = JSON.stringify(clades ?? [])
340
366
  useEffect(() => {
341
367
  model.setClades(JSON.parse(cladesKey))
@@ -1,9 +1,10 @@
1
1
  import React, { useState } from 'react'
2
2
 
3
3
  import { makeStyles } from '@jbrowse/core/util/tss-react'
4
- import { TextField } from '@mui/material'
4
+ import { Button, TextField } from '@mui/material'
5
5
  import { getUngappedSequence } from 'msa-parsers'
6
6
 
7
+ import { saveAs } from '../vendor/fileSaver.ts'
7
8
  import Checkbox2 from './Checkbox2.tsx'
8
9
  import CopyButton from './CopyButton.tsx'
9
10
 
@@ -18,19 +19,59 @@ const useStyles = makeStyles()(theme => ({
18
19
  },
19
20
  }))
20
21
 
22
+ export const maxShownRecords = 100
23
+ export const maxShownChars = 200_000
24
+
25
+ function fastaRecord([name, seq]: [string, string], showGaps: boolean) {
26
+ return `>${name}\n${showGaps ? seq : getUngappedSequence(seq)}`
27
+ }
28
+
29
+ function isAllGaps(seq: string) {
30
+ return !/[^.-]/.test(seq)
31
+ }
32
+
33
+ /**
34
+ * The first records of `rows` as FASTA, up to `maxShownRecords` records or
35
+ * `maxShownChars` characters, whichever comes first. A textarea holding every
36
+ * row of a large alignment freezes the page.
37
+ */
38
+ export function fastaPreview(rows: [string, string][], showGaps: boolean) {
39
+ const shown: string[] = []
40
+ let chars = 0
41
+ for (const row of rows) {
42
+ if (shown.length >= maxShownRecords || chars >= maxShownChars) {
43
+ break
44
+ }
45
+ const record = fastaRecord(row, showGaps)
46
+ shown.push(record)
47
+ chars += record.length + 1
48
+ }
49
+ return { text: shown.join('\n'), hidden: rows.length - shown.length }
50
+ }
51
+
21
52
  export default function SequenceTextArea({ str }: { str: [string, string][] }) {
22
53
  const { classes } = useStyles()
23
54
  const [showGaps, setShowGaps] = useState(false)
24
55
  const [showEmpty, setShowEmpty] = useState(false)
25
56
 
26
- const disp = str
27
- .map(([s1, s2]) => [s1, showGaps ? s2 : getUngappedSequence(s2)] as const)
28
- .filter(([, s2]) => showEmpty || !!s2)
29
- .map(([s1, s2]) => `>${s1}\n${s2}`)
30
- .join('\n')
57
+ const rows = showEmpty
58
+ ? str
59
+ : str.filter(([, seq]) => (showGaps ? !!seq : !isAllGaps(seq)))
60
+ const { text, hidden } = fastaPreview(rows, showGaps)
61
+ const fullText = () => rows.map(row => fastaRecord(row, showGaps)).join('\n')
31
62
  return (
32
63
  <>
33
- <CopyButton text={disp} />
64
+ <CopyButton text={fullText} />
65
+ <Button
66
+ onClick={() => {
67
+ saveAs(
68
+ new Blob([fullText()], { type: 'text/plain' }),
69
+ 'sequences.fasta',
70
+ )
71
+ }}
72
+ >
73
+ Download
74
+ </Button>
34
75
  <Checkbox2
35
76
  label="Show gaps"
36
77
  checked={showGaps}
@@ -52,7 +93,12 @@ export default function SequenceTextArea({ str }: { str: [string, string][] }) {
52
93
  minRows={5}
53
94
  maxRows={10}
54
95
  fullWidth
55
- value={disp}
96
+ value={text}
97
+ helperText={
98
+ hidden > 0
99
+ ? `Showing ${rows.length - hidden} of ${rows.length.toLocaleString()} sequences. ${hidden.toLocaleString()} more are in the copy and the download.`
100
+ : undefined
101
+ }
56
102
  slotProps={{
57
103
  input: {
58
104
  readOnly: true,
@@ -0,0 +1,70 @@
1
+ import React from 'react'
2
+
3
+ import { LoadingEllipses } from '@jbrowse/core/ui'
4
+ import { makeStyles } from '@jbrowse/core/util/tss-react'
5
+ import { Button, CircularProgress, Typography } from '@mui/material'
6
+ import { observer } from 'mobx-react'
7
+
8
+ import type { MsaViewModel } from '../model.ts'
9
+
10
+ const useStyles = makeStyles()({
11
+ page: {
12
+ display: 'flex',
13
+ alignItems: 'center',
14
+ gap: 12,
15
+ padding: 20,
16
+ },
17
+ header: {
18
+ display: 'flex',
19
+ alignItems: 'center',
20
+ gap: 4,
21
+ margin: 'auto',
22
+ marginLeft: 10,
23
+ },
24
+ })
25
+
26
+ /**
27
+ * The model's status line with its Cancel button: a spinner and a heading in
28
+ * place of the view while it loads, or a line in the header afterwards, which
29
+ * disappears when there is no status.
30
+ */
31
+ const StatusMessage = observer(function ({
32
+ model,
33
+ variant,
34
+ }: {
35
+ model: MsaViewModel
36
+ variant: 'page' | 'header'
37
+ }) {
38
+ const { status } = model
39
+ const { classes } = useStyles()
40
+ if (variant === 'header' && !status) {
41
+ return null
42
+ }
43
+ const message = status?.msg ?? 'Loading...'
44
+ const onCancel = status?.onCancel
45
+ return (
46
+ <div className={classes[variant]}>
47
+ {variant === 'page' ? (
48
+ <>
49
+ <CircularProgress size={24} />
50
+ <Typography variant="h6">{message}</Typography>
51
+ </>
52
+ ) : (
53
+ <LoadingEllipses message={message} component="span" />
54
+ )}
55
+ {onCancel ? (
56
+ <Button
57
+ variant={variant === 'page' ? 'outlined' : 'text'}
58
+ size="small"
59
+ onClick={() => {
60
+ onCancel()
61
+ }}
62
+ >
63
+ Cancel
64
+ </Button>
65
+ ) : null}
66
+ </div>
67
+ )
68
+ })
69
+
70
+ export default StatusMessage
@@ -146,7 +146,7 @@ const Track = observer(function ({
146
146
  )
147
147
  useWheelScroll({ ref, onScrollX })
148
148
  const { classes } = useStyles()
149
- const { hover, onMouseMove, onMouseLeave } = useTrackHover({
149
+ const { anchor, onMouseMove, onMouseLeave } = useTrackHover({
150
150
  model,
151
151
  tooltip: showColumnStats,
152
152
  })
@@ -172,9 +172,13 @@ const Track = observer(function ({
172
172
  }}
173
173
  >
174
174
  <track.ReactComponent model={model} track={track} />
175
- {hover ? (
176
- <BaseTooltip clientPoint={{ x: hover.x, y: hover.y + 15 }}>
177
- <TrackTooltipContent model={model} track={track} col={hover.col} />
175
+ {anchor ? (
176
+ <BaseTooltip clientPoint={anchor.clientPoint}>
177
+ <TrackTooltipContent
178
+ model={model}
179
+ track={track}
180
+ col={anchor.value}
181
+ />
178
182
  </BaseTooltip>
179
183
  ) : null}
180
184
  </div>
@@ -23,6 +23,13 @@ const VerticalScrollbar = observer(({ model }: { model: MsaViewModel }) => {
23
23
 
24
24
  return (
25
25
  <div
26
+ onMouseDown={event => {
27
+ if (event.button === 0 && event.target === event.currentTarget) {
28
+ const y =
29
+ event.clientY - event.currentTarget.getBoundingClientRect().top
30
+ model.setScrollY(-y / unit + msaAreaHeight / 2)
31
+ }
32
+ }}
26
33
  style={{
27
34
  position: 'relative',
28
35
  width,
@@ -15,6 +15,7 @@ import {
15
15
  Typography,
16
16
  useTheme,
17
17
  } from '@mui/material'
18
+ import { observer } from 'mobx-react'
18
19
 
19
20
  import Checkbox2 from '../Checkbox2.tsx'
20
21
 
@@ -30,7 +31,7 @@ function afterPaint(fn: () => void) {
30
31
  })
31
32
  }
32
33
 
33
- export default function ExportSVGDialog({
34
+ const ExportSVGDialog = observer(function ({
34
35
  model,
35
36
  onClose,
36
37
  }: {
@@ -181,4 +182,6 @@ export default function ExportSVGDialog({
181
182
  </DialogActions>
182
183
  </Dialog>
183
184
  )
184
- }
185
+ })
186
+
187
+ export default ExportSVGDialog