react-msaview 6.2.2 → 6.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 (80) hide show
  1. package/bundle/index.js +104 -104
  2. package/bundle/index.js.map +4 -4
  3. package/dist/components/MSAViewer.d.ts +10 -1
  4. package/dist/components/MSAViewer.js +3 -1
  5. package/dist/components/MSAViewer.js.map +1 -1
  6. package/dist/components/minimap/Minimap.js +1 -1
  7. package/dist/components/minimap/Minimap.js.map +1 -1
  8. package/dist/components/minimap/MinimapSVG.js +2 -2
  9. package/dist/components/minimap/MinimapSVG.js.map +1 -1
  10. package/dist/components/msa/MSAMouseoverCanvas.js +4 -2
  11. package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
  12. package/dist/components/msa/msaRaster.d.ts +5 -4
  13. package/dist/components/msa/msaRaster.js +10 -6
  14. package/dist/components/msa/msaRaster.js.map +1 -1
  15. package/dist/components/msa/renderHighlights.d.ts +31 -0
  16. package/dist/components/msa/renderHighlights.js +49 -0
  17. package/dist/components/msa/renderHighlights.js.map +1 -0
  18. package/dist/components/msa/renderMSAMouseover.d.ts +3 -1
  19. package/dist/components/msa/renderMSAMouseover.js +11 -1
  20. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  21. package/dist/components/tracks/drawTracks.js +4 -0
  22. package/dist/components/tracks/drawTracks.js.map +1 -1
  23. package/dist/components/tree/renderTreeCanvas.js +31 -0
  24. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  25. package/dist/exportFileName.d.ts +6 -0
  26. package/dist/exportFileName.js +15 -0
  27. package/dist/exportFileName.js.map +1 -0
  28. package/dist/headlessRenderEnv.d.ts +4 -1
  29. package/dist/headlessRenderEnv.js +6 -3
  30. package/dist/headlessRenderEnv.js.map +1 -1
  31. package/dist/index.d.ts +2 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/model.d.ts +172 -9
  34. package/dist/model.js +134 -3
  35. package/dist/model.js.map +1 -1
  36. package/dist/renderToSvg.js +30 -6
  37. package/dist/renderToSvg.js.map +1 -1
  38. package/dist/svgTestUtil.d.ts +1662 -0
  39. package/dist/svgTestUtil.js +28 -0
  40. package/dist/svgTestUtil.js.map +1 -0
  41. package/dist/types.d.ts +33 -0
  42. package/dist/umd.d.ts +1 -0
  43. package/dist/umd.js +3 -0
  44. package/dist/umd.js.map +1 -1
  45. package/dist/util.d.ts +1 -0
  46. package/dist/util.js +7 -0
  47. package/dist/util.js.map +1 -1
  48. package/dist/version.d.ts +1 -1
  49. package/dist/version.js +1 -1
  50. package/package.json +3 -3
  51. package/src/columnTracks.test.ts +97 -0
  52. package/src/components/MSAViewer.tsx +13 -0
  53. package/src/components/minimap/Minimap.tsx +1 -1
  54. package/src/components/minimap/MinimapSVG.tsx +2 -2
  55. package/src/components/msa/MSAMouseoverCanvas.tsx +4 -2
  56. package/src/components/msa/msaRaster.ts +10 -6
  57. package/src/components/msa/renderHighlights.ts +86 -0
  58. package/src/components/msa/renderMSAMouseover.ts +14 -0
  59. package/src/components/tracks/drawTracks.ts +4 -0
  60. package/src/components/tree/renderTreeCanvas.ts +56 -0
  61. package/src/exportFileName.test.ts +40 -0
  62. package/src/exportFileName.ts +20 -0
  63. package/src/headlessRenderEnv.ts +20 -11
  64. package/src/highlights.test.ts +70 -0
  65. package/src/impgRender.test.tsx +2 -4
  66. package/src/index.ts +4 -0
  67. package/src/model.ts +162 -10
  68. package/src/renderColumnTracksSvg.test.tsx +108 -0
  69. package/src/renderDomainsSvg.test.tsx +25 -10
  70. package/src/renderHighlightsSvg.test.tsx +85 -0
  71. package/src/renderMinimapSvg.test.tsx +6 -13
  72. package/src/renderRasterSvg.test.tsx +21 -93
  73. package/src/renderSequenceLogoSvg.test.tsx +4 -17
  74. package/src/renderToSvg.test.tsx +7 -21
  75. package/src/renderToSvg.tsx +32 -6
  76. package/src/svgTestUtil.ts +40 -0
  77. package/src/types.ts +39 -0
  78. package/src/umd.ts +3 -0
  79. package/src/util.ts +8 -0
  80. package/src/version.ts +1 -1
@@ -0,0 +1,28 @@
1
+ // Shared setup for the tests that drive renderToSvg headlessly.
2
+ import { enableStaticRendering } from 'mobx-react';
3
+ import { installHeadlessRenderEnv } from './headlessRenderEnv.js';
4
+ import MSAModelF from './model.js';
5
+ export function installSvgTestEnv(contextExtras) {
6
+ enableStaticRendering(true);
7
+ installHeadlessRenderEnv(globalThis, contextExtras);
8
+ }
9
+ export function createTestModel(snapshot, width = 800) {
10
+ const model = MSAModelF().create({
11
+ type: 'MsaView',
12
+ msaFormat: 'fasta',
13
+ height: 400,
14
+ ...snapshot,
15
+ });
16
+ model.setWidth(width);
17
+ return model;
18
+ }
19
+ // a protein alignment with no two neighbouring cells alike, so any color or
20
+ // letter landing in the wrong cell shows up
21
+ export function syntheticProteinMsa(rows, cols) {
22
+ const letters = 'ACDEFGHIKLMNPQRSTVWY';
23
+ return Array.from({ length: rows }, (_, r) => {
24
+ const seq = Array.from({ length: cols }, (_, c) => letters[(r * 7 + c * 3) % letters.length]).join('');
25
+ return `>seq${r}\n${seq}`;
26
+ }).join('\n');
27
+ }
28
+ //# sourceMappingURL=svgTestUtil.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"svgTestUtil.js","sourceRoot":"","sources":["../src/svgTestUtil.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAChE,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAElD,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAA;AACjE,OAAO,SAAS,MAAM,YAAY,CAAA;AAMlC,MAAM,UAAU,iBAAiB,CAC/B,aAAiD;IAEjD,qBAAqB,CAAC,IAAI,CAAC,CAAA;IAC3B,wBAAwB,CAAC,UAAU,EAAE,aAAa,CAAC,CAAA;AACrD,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,QAAgC,EAAE,KAAK,GAAG,GAAG;IAC3E,MAAM,KAAK,GAAG,SAAS,EAAE,CAAC,MAAM,CAAC;QAC/B,IAAI,EAAE,SAAS;QACf,SAAS,EAAE,OAAO;QAClB,MAAM,EAAE,GAAG;QACX,GAAG,QAAQ;KACZ,CAAC,CAAA;IACF,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;IACrB,OAAO,KAAK,CAAA;AACd,CAAC;AAED,4EAA4E;AAC5E,4CAA4C;AAC5C,MAAM,UAAU,mBAAmB,CAAC,IAAY,EAAE,IAAY;IAC5D,MAAM,OAAO,GAAG,sBAAsB,CAAA;IACtC,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QAC3C,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CACpB,EAAE,MAAM,EAAE,IAAI,EAAE,EAChB,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CACpD,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QACV,OAAO,OAAO,CAAC,KAAK,GAAG,EAAE,CAAA;IAC3B,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACf,CAAC"}
package/dist/types.d.ts CHANGED
@@ -23,6 +23,18 @@ export interface BasicTrack {
23
23
  ReactComponent: React.FC<any>;
24
24
  model: TextTrackModel & BarTrackModel;
25
25
  }
26
+ export interface ColumnTrackSpec {
27
+ id: string;
28
+ name: string;
29
+ kind: 'bar' | 'text';
30
+ values?: number[];
31
+ data?: string;
32
+ max?: number;
33
+ color?: string;
34
+ colors?: Record<string, string>;
35
+ row?: string;
36
+ height?: number;
37
+ }
26
38
  export type { Annotation };
27
39
  export type TidyDomainAnnotation = Annotation;
28
40
  export interface DomainBand {
@@ -31,6 +43,27 @@ export interface DomainBand {
31
43
  endCol: number;
32
44
  stackIndex: number;
33
45
  }
46
+ /**
47
+ * A persistent, labeled highlight. Coordinates are 1-based and inclusive, as
48
+ * GFF's are. `row` makes `start`/`end` residues of that row; without it they
49
+ * are alignment columns. `rows` marks whole rows instead.
50
+ */
51
+ export interface Highlight {
52
+ row?: string;
53
+ rows?: string[];
54
+ start?: number;
55
+ end?: number;
56
+ label?: string;
57
+ color?: string;
58
+ }
59
+ /** a Highlight resolved to visible column indices and row indices */
60
+ export interface ResolvedHighlight {
61
+ startCol?: number;
62
+ endCol?: number;
63
+ rowIndices: number[];
64
+ label?: string;
65
+ color?: string;
66
+ }
34
67
  export interface Node {
35
68
  children?: Node[];
36
69
  name?: string;
package/dist/umd.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as React from 'react';
2
+ export { destroy } from '@jbrowse/mobx-state-tree';
2
3
  export { default as MSAView } from './components/Loading.tsx';
3
4
  export { type MsaViewModel, default as MSAModelF } from './model.ts';
4
5
  export * from 'react-dom/client';
package/dist/umd.js CHANGED
@@ -1,4 +1,7 @@
1
1
  import * as React from 'react';
2
+ // a host that swaps models (the R htmlwidget on re-render) has to destroy the
3
+ // old one, or the disposers its autoruns registered never run
4
+ export { destroy } from '@jbrowse/mobx-state-tree';
2
5
  export { default as MSAView } from './components/Loading.js';
3
6
  export { default as MSAModelF } from './model.js';
4
7
  export * from 'react-dom/client';
package/dist/umd.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"umd.js","sourceRoot":"","sources":["../src/umd.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,yBAA0B,CAAA;AAC7D,OAAO,EAAqB,OAAO,IAAI,SAAS,EAAE,MAAM,YAAY,CAAA;AAEpE,cAAc,kBAAkB,CAAA;AAEhC,OAAO,EAAE,KAAK,EAAE,CAAA"}
1
+ {"version":3,"file":"umd.js","sourceRoot":"","sources":["../src/umd.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,8EAA8E;AAC9E,8DAA8D;AAC9D,OAAO,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAA;AAClD,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,yBAA0B,CAAA;AAC7D,OAAO,EAAqB,OAAO,IAAI,SAAS,EAAE,MAAM,YAAY,CAAA;AAEpE,cAAc,kBAAkB,CAAA;AAEhC,OAAO,EAAE,KAAK,EAAE,CAAA"}
package/dist/util.d.ts CHANGED
@@ -6,6 +6,7 @@ export declare function transform<T>(obj: Record<string, T>, cb: (arg0: [string,
6
6
  export declare function colorContrast(colorScheme: Record<string, string>, theme: Theme): {
7
7
  [k: string]: string;
8
8
  };
9
+ export declare function dropBlanks<T>(blanks: number[], arr: T[]): T[];
9
10
  export declare function skipBlanks(blanks: number[], str: string): string;
10
11
  export declare function computeRowInsertions(blanks: number[], seq: string): {
11
12
  pos: number;
package/dist/util.js CHANGED
@@ -32,6 +32,13 @@ export function colorContrast(colorScheme, theme) {
32
32
  theme.palette.getContrastText(colord(color).toHex()),
33
33
  ]);
34
34
  }
35
+ export function dropBlanks(blanks, arr) {
36
+ if (blanks.length === 0) {
37
+ return arr;
38
+ }
39
+ const set = new Set(blanks);
40
+ return arr.filter((_, i) => !set.has(i));
41
+ }
35
42
  export function skipBlanks(blanks, str) {
36
43
  if (blanks.length === 0) {
37
44
  return str;
package/dist/util.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AACvC,OAAO,UAAU,MAAM,qBAAqB,CAAA;AAC5C,OAAO,WAAW,MAAM,sBAAsB,CAAA;AAI9C,MAAM,CAAC,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC,CAAA;AAEjC,MAAM,mBAAmB,GAAG,IAAI,GAAG,EAAkB,CAAA;AAErD,0EAA0E;AAC1E,uEAAuE;AACvE,8DAA8D;AAC9D,MAAM,UAAU,sBAAsB,CAAC,KAAa,EAAE,EAAU;IAC9D,MAAM,QAAQ,GAAG,GAAG,KAAK,IAAI,EAAE,EAAE,CAAA;IACjC,MAAM,MAAM,GAAG,mBAAmB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IAChD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,MAAM,CAAA;IACf,CAAC;IACD,IAAI,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IACrB,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE,CAAC;QACtB,MAAM,QAAQ,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC,SAAS,EAAE,GAAG,GAAG,CAAA;QAC7C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YACjD,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QACjD,CAAC;IACH,CAAC;IACD,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,CAAA;IACxB,mBAAmB,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;IACzC,OAAO,MAAM,CAAA;AACf,CAAC;AAED,MAAM,UAAU,SAAS,CACvB,GAAsB,EACtB,EAAsC;IAEtC,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;AACxD,CAAC;AAED,MAAM,UAAU,aAAa,CAC3B,WAAmC,EACnC,KAAY;IAEZ,OAAO,SAAS,CAAC,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC;QACjD,MAAM;QACN,KAAK,CAAC,OAAO,CAAC,eAAe,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,CAAC;KACrD,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,MAAgB,EAAE,GAAW;IACtD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO,GAAG,CAAA;IACZ,CAAC;IACD,MAAM,MAAM,GAAG,EAAE,CAAA;IACjB,IAAI,OAAO,GAAG,CAAC,CAAA;IACf,KAAK,MAAM,QAAQ,IAAI,MAAM,EAAE,CAAC;QAC9B,IAAI,QAAQ,GAAG,OAAO,EAAE,CAAC;YACvB,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAA;QAC3C,CAAC;QACD,OAAO,GAAG,QAAQ,GAAG,CAAC,CAAA;IACxB,CAAC;IACD,IAAI,OAAO,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC;QACzB,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAA;IACjC,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;AACxB,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,MAAgB,EAAE,GAAW;IAChE,MAAM,UAAU,GAAuC,EAAE,CAAA;IACzD,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAA;IAC/B,IAAI,UAAU,GAAG,CAAC,CAAA;IAClB,IAAI,QAAQ,GAAG,CAAC,CAAA;IAChB,IAAI,gBAAgB,GAAG,CAAC,CAAC,CAAA;IACzB,IAAI,WAAW,GAAa,EAAE,CAAA;IAC9B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACpC,IAAI,QAAQ,GAAG,SAAS,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YACnD,oEAAoE;YACpE,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;gBAC3C,IAAI,gBAAgB,KAAK,UAAU,EAAE,CAAC;oBACpC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAE,CAAC,CAAA;gBAC3B,CAAC;qBAAM,CAAC;oBACN,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;wBAC3B,UAAU,CAAC,IAAI,CAAC;4BACd,GAAG,EAAE,gBAAgB;4BACrB,OAAO,EAAE,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC;yBAC9B,CAAC,CAAA;oBACJ,CAAC;oBACD,gBAAgB,GAAG,UAAU,CAAA;oBAC7B,WAAW,GAAG,CAAC,GAAG,CAAC,CAAC,CAAE,CAAC,CAAA;gBACzB,CAAC;YACH,CAAC;YACD,QAAQ,EAAE,CAAA;QACZ,CAAC;aAAM,CAAC;YACN,UAAU,EAAE,CAAA;QACd,CAAC;IACH,CAAC;IACD,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3B,UAAU,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,gBAAgB,EAAE,OAAO,EAAE,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,CAAA;IAC3E,CAAC;IACD,OAAO,UAAU,CAAA;AACnB,CAAC;AAED,MAAM,UAAU,GAAG,CAAC,CAAiC;IACnD,OAAO,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,CAAA;AACxB,CAAC;AAED,8CAA8C;AAC9C,oCAAoC;AACpC,MAAM,UAAU,OAAO,CAAC,CAAU;IAChC,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG,CAAA;AAC/B,CAAC"}
1
+ {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"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"}
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const version = "6.2.2";
1
+ export declare const version = "6.3.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
- export const version = '6.2.2';
1
+ export const version = '6.3.0';
2
2
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-msaview",
3
3
  "author": "Colin",
4
- "version": "6.2.2",
4
+ "version": "6.3.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": "6.2.2",
47
- "msa-parsers": "6.2.2"
46
+ "@jbrowse/svgcanvas": "6.3.0",
47
+ "msa-parsers": "6.3.0"
48
48
  },
49
49
  "scripts": {
50
50
  "clean": "rimraf dist bundle",
@@ -0,0 +1,97 @@
1
+ import { getSnapshot } from '@jbrowse/mobx-state-tree'
2
+ import { expect, test } from 'vitest'
3
+
4
+ import stateModelFactory from './model.ts'
5
+
6
+ import type { ColumnTrackSpec } from './types.ts'
7
+
8
+ const MsaView = stateModelFactory()
9
+
10
+ // s1 has a gap at column 1 and s2 a gap at column 3, and every row is gapped at
11
+ // column 5, so hiding gappy columns drops that one
12
+ const msa = '>s1\nA-CDE-F\n>s2\nAB-DE-F\n>s3\nABCDE-F\n'
13
+
14
+ function makeModel(columnTracks: ColumnTrackSpec[]) {
15
+ return MsaView.create({
16
+ type: 'MsaView',
17
+ msaFormat: 'fasta',
18
+ data: { msa },
19
+ columnTracks,
20
+ })
21
+ }
22
+
23
+ test('column-indexed values normalize by max and clamp to 0..1', () => {
24
+ const model = makeModel([
25
+ { id: 't', name: 'T', kind: 'bar', values: [1, 2, 4, -1], max: 2 },
26
+ ])
27
+ expect(model.columnTrackContent.get('t')?.values).toEqual([0.5, 1, 1, 0])
28
+ })
29
+
30
+ test('row-indexed values land on the residues of that row', () => {
31
+ const model = makeModel([
32
+ { id: 't', name: 'T', kind: 'bar', values: [0.1, 0.2, 0.3], row: 's1' },
33
+ ])
34
+ // residues 1..3 of s1 are A, C, D at global columns 0, 2, 3
35
+ expect(model.columnTrackContent.get('t')?.values).toEqual([
36
+ 0.1, 0, 0.2, 0.3, 0, 0, 0,
37
+ ])
38
+ })
39
+
40
+ test('a row-indexed text track spreads its letters the same way', () => {
41
+ const model = makeModel([
42
+ { id: 'f', name: 'Frame', kind: 'text', data: '123', row: 's2' },
43
+ ])
44
+ // residues of s2 sit at columns 0, 1, 3
45
+ expect(model.columnTrackContent.get('f')?.data).toBe('12 3 ')
46
+ })
47
+
48
+ test('hiding gappy columns drops them from the values and the text', () => {
49
+ const model = makeModel([
50
+ { id: 't', name: 'T', kind: 'bar', values: [1, 2, 3, 4, 5, 6, 7], max: 7 },
51
+ { id: 'f', name: 'F', kind: 'text', data: 'abcdefg' },
52
+ ])
53
+ model.setHideGaps(true)
54
+ model.setAllowedGappyness(99)
55
+ expect(model.blanks).toEqual([5])
56
+ expect(model.columnTrackContent.get('t')?.values).toHaveLength(6)
57
+ expect(model.columnTrackContent.get('f')?.data).toBe('abcdeg')
58
+ })
59
+
60
+ test('data tracks join the track list with their own color and height', () => {
61
+ const model = makeModel([
62
+ { id: 't', name: 'dN/dS', kind: 'bar', values: [1], color: 'red' },
63
+ { id: 'f', name: 'Frame', kind: 'text', data: '1', colors: { 1: 'blue' } },
64
+ ])
65
+ const ids = model.tracks.map(t => t.model.id)
66
+ expect(ids).toContain('t')
67
+ expect(ids).toContain('f')
68
+ const bar = model.tracks.find(t => t.model.id === 't')!.model
69
+ expect(bar.barColor).toBe('red')
70
+ expect(bar.height).toBe(model.conservationTrackHeight)
71
+ const text = model.tracks.find(t => t.model.id === 'f')!.model
72
+ expect(text.customColorScheme).toEqual({ 1: 'blue' })
73
+ expect(text.height).toBe(model.rowHeight)
74
+ expect(text.data).toBe('1')
75
+
76
+ expect(model.turnedOnTracks.map(t => t.model.id)).toContain('t')
77
+ model.toggleTrack('t')
78
+ expect(model.turnedOnTracks.map(t => t.model.id)).not.toContain('t')
79
+ })
80
+
81
+ test('setColumnTracks replaces the set and the snapshot carries it', () => {
82
+ const model = makeModel([])
83
+ expect(getSnapshot(model)).not.toHaveProperty('columnTracks')
84
+ model.setColumnTracks([{ id: 't', name: 'T', kind: 'bar', values: [0.5] }])
85
+ expect(getSnapshot(model).columnTracks).toEqual([
86
+ { id: 't', name: 'T', kind: 'bar', values: [0.5] },
87
+ ])
88
+ })
89
+
90
+ test('a track too large to share leaves the snapshot but keeps drawing', () => {
91
+ const model = makeModel([
92
+ { id: 'big', name: 'Big', kind: 'bar', values: Array(20_000).fill(0.5) },
93
+ { id: 'small', name: 'Small', kind: 'bar', values: [0.5] },
94
+ ])
95
+ expect(model.columnTrackContent.get('big')?.values).toHaveLength(20_000)
96
+ expect(getSnapshot(model).columnTracks?.map(t => t.id)).toEqual(['small'])
97
+ })
@@ -7,6 +7,7 @@ import { ThemeProvider } from '@mui/material/styles'
7
7
  import MSAModelF from '../model.ts'
8
8
  import Loading from './Loading.tsx'
9
9
 
10
+ import type { ColumnTrackSpec, Highlight } from '../types.ts'
10
11
  import type { FileLocation as FileLocationType } from '@jbrowse/core/util/types'
11
12
 
12
13
  const theme = createJBrowseTheme()
@@ -26,8 +27,16 @@ interface MSAViewerProps {
26
27
  rowHeight?: number
27
28
  /** alignment columns (0-based) to highlight with a persistent overlay */
28
29
  highlightColumns?: number[]
30
+ /**
31
+ * labeled highlights in 1-based inclusive coordinates: `{start, end}` for
32
+ * alignment columns, `{row, start, end}` for residues of that row, `{rows}`
33
+ * for whole rows, each with an optional `label` and `color`
34
+ */
35
+ highlights?: Highlight[]
29
36
  /** row name to diff every other row against (matches render as ".") */
30
37
  relativeTo?: string
38
+ /** tracks supplied as data: per-column bar values or a text row (see docs/layers.md) */
39
+ columnTracks?: ColumnTrackSpec[]
31
40
  /** draw the phylogenetic tree (default true); false leaves a labels-only gutter */
32
41
  drawTree?: boolean
33
42
  /** fixed width (px) of the tree/label area */
@@ -48,7 +57,9 @@ export default function MSAViewer({
48
57
  colWidth,
49
58
  rowHeight,
50
59
  highlightColumns,
60
+ highlights,
51
61
  relativeTo,
62
+ columnTracks,
52
63
  drawTree,
53
64
  treeAreaWidth,
54
65
  autoTreeAreaWidth,
@@ -69,7 +80,9 @@ export default function MSAViewer({
69
80
  ...(colWidth ? { colWidth } : {}),
70
81
  ...(rowHeight ? { rowHeight } : {}),
71
82
  ...(highlightColumns ? { highlightColumns } : {}),
83
+ ...(highlights ? { highlights } : {}),
72
84
  ...(relativeTo ? { relativeTo } : {}),
85
+ ...(columnTracks ? { columnTracks } : {}),
73
86
  ...(drawTree !== undefined ? { drawTree } : {}),
74
87
  ...(treeAreaWidth ? { treeAreaWidth } : {}),
75
88
  ...(autoTreeAreaWidth ? { autoTreeAreaWidth } : {}),
@@ -76,7 +76,7 @@ const Minimap = observer(function ({ model }: { model: MsaViewModel }) {
76
76
  width: msaCanvasWidth,
77
77
  height: MINIMAP_BAR_HEIGHT,
78
78
  boxSizing: 'border-box',
79
- border: '1px solid #555',
79
+ border: `1px solid ${theme.palette.text.secondary}`,
80
80
  }}
81
81
  />
82
82
  <DragHandle
@@ -42,7 +42,7 @@ export default function MinimapSVG({
42
42
  y={0}
43
43
  width={msaCanvasWidth}
44
44
  height={MINIMAP_BAR_HEIGHT}
45
- stroke="#555"
45
+ stroke={theme.palette.text.secondary}
46
46
  fill="none"
47
47
  />
48
48
  <rect
@@ -51,7 +51,7 @@ export default function MinimapSVG({
51
51
  width={w}
52
52
  height={MINIMAP_BAR_HEIGHT}
53
53
  fill={scrollbarThumbFill}
54
- stroke="#555"
54
+ stroke={theme.palette.text.secondary}
55
55
  />
56
56
  <g transform={`translate(0 ${MINIMAP_BAR_HEIGHT})`}>
57
57
  <polygon fill={scrollbarThumbFill} points={polygonPoints} />
@@ -1,6 +1,7 @@
1
1
  import React from 'react'
2
2
 
3
3
  import { isAlive } from '@jbrowse/mobx-state-tree'
4
+ import { useTheme } from '@mui/material'
4
5
  import { observer } from 'mobx-react'
5
6
 
6
7
  import { useCanvasAutorun } from '../../useCanvasAutorun.ts'
@@ -14,17 +15,18 @@ const MSAMouseoverCanvas = observer(function ({
14
15
  model: MsaViewModel
15
16
  }) {
16
17
  const { height, msaCanvasWidth: width, highResScaleFactor } = model
18
+ const theme = useTheme()
17
19
  const canvasWidth = width * highResScaleFactor
18
20
  const canvasHeight = height * highResScaleFactor
19
21
  const ref = useCanvasAutorun({
20
22
  draw: ctx => {
21
23
  if (isAlive(model)) {
22
- renderMouseover({ ctx, model })
24
+ renderMouseover({ ctx, model, theme })
23
25
  }
24
26
  },
25
27
  width: canvasWidth,
26
28
  height: canvasHeight,
27
- deps: [model],
29
+ deps: [model, theme],
28
30
  })
29
31
 
30
32
  return (
@@ -320,20 +320,24 @@ export function drawMsaRaster({
320
320
  /**
321
321
  * A raster canvas as a PNG data URI, for embedding in the SVG export.
322
322
  *
323
- * toDataURL is the synchronous read-back and only the DOM canvas has it, so an
323
+ * toDataURL is the synchronous read-back. The DOM canvas has it, and so does a
324
+ * node canvas standing in for OffscreenCanvas headlessly; a browser
324
325
  * OffscreenCanvas -- which is what the tile and thumbnail caches hold wherever
325
- * it exists -- gets copied onto one first. Returns undefined wherever the
326
- * read-back is unavailable (jsdom) rather than throwing, which is the signal to
327
- * fall back to drawing the thing in vector.
326
+ * it exists -- gets copied onto a DOM canvas first. Returns undefined wherever
327
+ * the read-back is unavailable (jsdom) rather than throwing, which is the
328
+ * signal to fall back to drawing the thing in vector.
328
329
  */
329
330
  export function canvasHref(canvas: RasterCanvas | undefined) {
330
- if (!canvas || typeof document === 'undefined') {
331
+ if (!canvas) {
331
332
  return undefined
332
333
  }
333
334
  try {
334
- if (canvas instanceof HTMLCanvasElement) {
335
+ if ('toDataURL' in canvas && typeof canvas.toDataURL === 'function') {
335
336
  return canvas.toDataURL('image/png')
336
337
  }
338
+ if (typeof document === 'undefined') {
339
+ return undefined
340
+ }
337
341
  const out = document.createElement('canvas')
338
342
  out.width = canvas.width
339
343
  out.height = canvas.height
@@ -0,0 +1,86 @@
1
+ import { setFontSize } from '../../setFontSize.ts'
2
+
3
+ import type { MsaViewModel } from '../../model.ts'
4
+ import type { RenderCtx } from '../renderCtx.ts'
5
+ import type { Theme } from '@mui/material'
6
+
7
+ export const highlightFill = 'rgba(255,140,0,0.28)'
8
+ export const highlightBorder = 'rgba(210,90,0,0.95)'
9
+ export const highlightRowFill = 'rgba(255,140,0,0.18)'
10
+
11
+ export const labelFontSize = 11
12
+ const labelPad = 3
13
+
14
+ export function drawHighlightLabel({
15
+ ctx,
16
+ theme,
17
+ label,
18
+ x,
19
+ y,
20
+ spanWidth,
21
+ }: {
22
+ ctx: RenderCtx
23
+ theme: Theme
24
+ label: string
25
+ x: number
26
+ y: number
27
+ spanWidth: number
28
+ }) {
29
+ setFontSize(ctx, labelFontSize)
30
+ ctx.textAlign = 'start'
31
+ const boxWidth = ctx.measureText(label).width + labelPad * 2
32
+ const boxHeight = labelFontSize + labelPad * 2
33
+ const left = boxWidth <= spanWidth ? x : x + spanWidth + 2
34
+ ctx.fillStyle = theme.palette.background.paper
35
+ ctx.fillRect(left, y, boxWidth, boxHeight)
36
+ ctx.fillStyle = theme.palette.text.primary
37
+ ctx.fillText(label, left + labelPad, y + labelPad + labelFontSize - 2)
38
+ }
39
+
40
+ /**
41
+ * The persistent `highlights` overlay on the alignment: row tints, bordered
42
+ * column bands, and a label above each band. `offsetX`/`offsetY` are the
43
+ * content coordinates at the canvas origin, the convention renderMSABlock
44
+ * uses, so the live overlay passes -scrollX/-scrollY and the export passes its
45
+ * layout offsets.
46
+ */
47
+ export function renderHighlights({
48
+ ctx,
49
+ model,
50
+ theme,
51
+ offsetX,
52
+ offsetY,
53
+ width,
54
+ height,
55
+ }: {
56
+ ctx: RenderCtx
57
+ model: MsaViewModel
58
+ theme: Theme
59
+ offsetX: number
60
+ offsetY: number
61
+ width: number
62
+ height: number
63
+ }) {
64
+ const { resolvedHighlights, colWidth, rowHeight } = model
65
+ ctx.lineWidth = 2
66
+ for (const { rowIndices, color } of resolvedHighlights) {
67
+ ctx.fillStyle = color ?? highlightRowFill
68
+ for (const index of rowIndices) {
69
+ ctx.fillRect(0, index * rowHeight - offsetY, width, rowHeight)
70
+ }
71
+ }
72
+ for (const { startCol, endCol, label, color } of resolvedHighlights) {
73
+ if (startCol === undefined || endCol === undefined) {
74
+ continue
75
+ }
76
+ const x = startCol * colWidth - offsetX
77
+ const spanWidth = (endCol - startCol + 1) * colWidth
78
+ ctx.fillStyle = color ?? highlightFill
79
+ ctx.fillRect(x, 0, spanWidth, height)
80
+ ctx.strokeStyle = color ?? highlightBorder
81
+ ctx.strokeRect(x, 0, spanWidth, height)
82
+ if (label) {
83
+ drawHighlightLabel({ ctx, theme, label, x, y: 0, spanWidth })
84
+ }
85
+ }
86
+ }
@@ -4,8 +4,10 @@ import {
4
4
  multiRowHoverColor,
5
5
  referenceColor,
6
6
  } from '../overlayColors.ts'
7
+ import { renderHighlights } from './renderHighlights.ts'
7
8
 
8
9
  import type { MsaViewModel } from '../../model.ts'
10
+ import type { Theme } from '@mui/material'
9
11
 
10
12
  // the persistent highlightColumns overlay: a stronger fill plus a solid border
11
13
  // so a domain/motif band reads clearly over the colored alignment cells (the
@@ -16,9 +18,11 @@ const highlightColumnsBorder = 'rgba(210,90,0,0.95)'
16
18
  export function renderMouseover({
17
19
  ctx,
18
20
  model,
21
+ theme,
19
22
  }: {
20
23
  ctx: CanvasRenderingContext2D
21
24
  model: MsaViewModel
25
+ theme: Theme
22
26
  }) {
23
27
  const {
24
28
  mouseCol,
@@ -72,6 +76,16 @@ export function renderMouseover({
72
76
  ctx.strokeRect(x, 0, w, height)
73
77
  }
74
78
 
79
+ renderHighlights({
80
+ ctx,
81
+ model,
82
+ theme,
83
+ offsetX: -scrollX,
84
+ offsetY: -scrollY,
85
+ width,
86
+ height,
87
+ })
88
+
75
89
  ctx.fillStyle = hoverColor
76
90
  if (mouseCol !== undefined) {
77
91
  colBand(mouseCol)
@@ -43,6 +43,10 @@ export function drawConservationBars({
43
43
  // the bar-track values live on model getters (kept off the plain track object so
44
44
  // the live canvas autorun stays reactive); select the right one by track id
45
45
  export function barTrackValues(model: MsaViewModel, trackId: string) {
46
+ const supplied = model.columnTrackContent.get(trackId)?.values
47
+ if (supplied) {
48
+ return supplied
49
+ }
46
50
  return trackId === 'property-conservation'
47
51
  ? model.propertyConservation
48
52
  : model.conservation
@@ -5,6 +5,10 @@ import {
5
5
  } from '../../hierarchy.ts'
6
6
  import { setFontSize } from '../../setFontSize.ts'
7
7
  import { getVisibleLeaves } from '../getVisibleLeaves.ts'
8
+ import {
9
+ drawHighlightLabel,
10
+ highlightRowFill,
11
+ } from '../msa/renderHighlights.ts'
8
12
 
9
13
  import type { HierarchyNode } from '../../hierarchy.ts'
10
14
  import type { MsaViewModel } from '../../model.ts'
@@ -349,6 +353,55 @@ function renderTreeLabels({
349
353
  ctx.setLineDash([])
350
354
  }
351
355
 
356
+ // the row sets of `highlights`, tinted across the tree area under the labels
357
+ // with the label in the gutter at the first row of each set
358
+ function renderRowHighlights({
359
+ ctx,
360
+ model,
361
+ theme,
362
+ offsetY,
363
+ blockSizeYOverride,
364
+ }: {
365
+ ctx: RenderCtx
366
+ model: MsaViewModel
367
+ theme: Theme
368
+ offsetY: number
369
+ blockSizeYOverride?: number
370
+ }) {
371
+ const {
372
+ resolvedHighlights,
373
+ rowHeight,
374
+ treeAreaWidth,
375
+ marginLeft,
376
+ blockSize,
377
+ } = model
378
+ const by = blockSizeYOverride ?? blockSize
379
+ for (const { rowIndices, label, color } of resolvedHighlights) {
380
+ ctx.fillStyle = color ?? highlightRowFill
381
+ for (const index of rowIndices) {
382
+ const y = index * rowHeight
383
+ if (inYBlock(y + rowHeight / 2, offsetY, by)) {
384
+ ctx.fillRect(-marginLeft, y, treeAreaWidth, rowHeight)
385
+ }
386
+ }
387
+ const first = Math.min(...rowIndices)
388
+ if (
389
+ label &&
390
+ rowIndices.length &&
391
+ inYBlock(first * rowHeight, offsetY, by)
392
+ ) {
393
+ drawHighlightLabel({
394
+ ctx,
395
+ theme,
396
+ label,
397
+ x: -marginLeft + 2,
398
+ y: first * rowHeight,
399
+ spanWidth: treeAreaWidth,
400
+ })
401
+ }
402
+ }
403
+ }
404
+
352
405
  export function renderTreeCanvas({
353
406
  model,
354
407
  clickMap,
@@ -403,6 +456,9 @@ export function renderTreeCanvas({
403
456
  // every block of every redraw, and `collapsed` is a list
404
457
  const collapsedSet = new Set(model.collapsed)
405
458
 
459
+ renderRowHighlights({ ctx, model, theme, offsetY, blockSizeYOverride })
460
+ setFontSize(ctx, fontSize)
461
+
406
462
  if (!noTree && drawTree) {
407
463
  renderTree({
408
464
  ctx,
@@ -0,0 +1,40 @@
1
+ import { expect, test } from 'vitest'
2
+
3
+ import { exportFileName } from './exportFileName.ts'
4
+
5
+ test('takes the alignment file name and swaps the extension', () => {
6
+ expect(
7
+ exportFileName(
8
+ {
9
+ uri: 'https://example.org/data/PF00069.stock?dl=1',
10
+ locationType: 'UriLocation',
11
+ },
12
+ 'svg',
13
+ ),
14
+ ).toBe('PF00069.svg')
15
+ expect(
16
+ exportFileName(
17
+ {
18
+ localPath: 'C:\\aligns\\hemoglobin.fa',
19
+ locationType: 'LocalPathLocation',
20
+ },
21
+ 'svg',
22
+ ),
23
+ ).toBe('hemoglobin.svg')
24
+ expect(
25
+ exportFileName(
26
+ { blobId: 'b1', name: 'pasted.aln', locationType: 'BlobLocation' },
27
+ 'svg',
28
+ ),
29
+ ).toBe('pasted.svg')
30
+ })
31
+
32
+ test('falls back to a plain name without a file', () => {
33
+ expect(exportFileName(undefined, 'svg')).toBe('image.svg')
34
+ expect(
35
+ exportFileName(
36
+ { uri: 'https://example.org/', locationType: 'UriLocation' },
37
+ 'svg',
38
+ ),
39
+ ).toBe('image.svg')
40
+ })