react-msaview 5.4.0 → 5.5.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 (94) hide show
  1. package/LICENSE +21 -0
  2. package/bundle/index.js +465 -138
  3. package/bundle/index.js.map +7 -1
  4. package/dist/colorSchemes.js +11 -7
  5. package/dist/colorSchemes.js.map +1 -1
  6. package/dist/components/Checkbox2.js +3 -1
  7. package/dist/components/Checkbox2.js.map +1 -1
  8. package/dist/components/MSAViewer.d.ts +7 -1
  9. package/dist/components/MSAViewer.js +11 -10
  10. package/dist/components/MSAViewer.js.map +1 -1
  11. package/dist/components/Track.js +8 -5
  12. package/dist/components/Track.js.map +1 -1
  13. package/dist/components/dialogs/FeatureDialog.js +27 -23
  14. package/dist/components/dialogs/FeatureDialog.js.map +1 -1
  15. package/dist/components/dialogs/InterProScanDialog.js +2 -1
  16. package/dist/components/dialogs/InterProScanDialog.js.map +1 -1
  17. package/dist/components/header/MultiAlignmentSelector.js +0 -0
  18. package/dist/components/header/MultiAlignmentSelector.js.map +1 -1
  19. package/dist/components/msa/DomainLegend.d.ts +6 -0
  20. package/dist/components/msa/DomainLegend.js +41 -0
  21. package/dist/components/msa/DomainLegend.js.map +1 -0
  22. package/dist/components/msa/MSACanvasBlock.js +17 -5
  23. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  24. package/dist/components/msa/MSAPanel.js +3 -1
  25. package/dist/components/msa/MSAPanel.js.map +1 -1
  26. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +58 -14
  27. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  28. package/dist/components/msa/renderMSABlock.js +7 -9
  29. package/dist/components/msa/renderMSABlock.js.map +1 -1
  30. package/dist/components/msa/renderMSAMouseover.js +32 -4
  31. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  32. package/dist/components/renderCtx.d.ts +1 -1
  33. package/dist/components/tracks/renderTracksSvg.js +4 -4
  34. package/dist/components/tracks/renderTracksSvg.js.map +1 -1
  35. package/dist/components/tree/renderTreeCanvas.js +3 -0
  36. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  37. package/dist/constants.d.ts +3 -0
  38. package/dist/constants.js +16 -0
  39. package/dist/constants.js.map +1 -1
  40. package/dist/createPaletteMap.d.ts +5 -2
  41. package/dist/createPaletteMap.js +15 -5
  42. package/dist/createPaletteMap.js.map +1 -1
  43. package/dist/hierarchy.js +4 -1
  44. package/dist/hierarchy.js.map +1 -1
  45. package/dist/launchInterProScan.d.ts +2 -0
  46. package/dist/launchInterProScan.js.map +1 -1
  47. package/dist/model/msaModel.js.map +1 -1
  48. package/dist/model.d.ts +100 -3
  49. package/dist/model.js +121 -19
  50. package/dist/model.js.map +1 -1
  51. package/dist/neighborJoining.js +44 -30
  52. package/dist/neighborJoining.js.map +1 -1
  53. package/dist/renderToSvg.js +104 -74
  54. package/dist/renderToSvg.js.map +1 -1
  55. package/dist/{webpack.js → umd.js} +1 -1
  56. package/dist/umd.js.map +1 -0
  57. package/dist/version.d.ts +1 -1
  58. package/dist/version.js +1 -1
  59. package/package.json +8 -7
  60. package/src/colorSchemes.ts +12 -7
  61. package/src/components/Checkbox2.tsx +7 -1
  62. package/src/components/MSAViewer.tsx +31 -21
  63. package/src/components/Track.tsx +8 -5
  64. package/src/components/dialogs/FeatureDialog.tsx +62 -48
  65. package/src/components/dialogs/InterProScanDialog.tsx +2 -1
  66. package/src/components/header/MultiAlignmentSelector.tsx +0 -0
  67. package/src/components/msa/DomainLegend.tsx +74 -0
  68. package/src/components/msa/MSACanvasBlock.tsx +22 -4
  69. package/src/components/msa/MSAPanel.tsx +2 -0
  70. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +73 -17
  71. package/src/components/msa/renderMSABlock.ts +5 -11
  72. package/src/components/msa/renderMSAMouseover.ts +32 -4
  73. package/src/components/renderCtx.ts +1 -0
  74. package/src/components/tracks/renderTracksSvg.ts +4 -4
  75. package/src/components/tree/renderTreeCanvas.ts +5 -1
  76. package/src/constants.ts +18 -0
  77. package/src/createPaletteMap.ts +18 -5
  78. package/src/hierarchy.ts +4 -1
  79. package/src/launchInterProScan.ts +3 -1
  80. package/src/model/msaModel.ts +3 -1
  81. package/src/model.ts +137 -36
  82. package/src/neighborJoining.ts +44 -31
  83. package/src/renderDomainLegendSvg.test.tsx +134 -0
  84. package/src/renderToSvg.tsx +210 -147
  85. package/src/segmentOverlay.test.ts +70 -0
  86. package/src/version.ts +1 -1
  87. package/bundle/index.js.LICENSE.txt +0 -100
  88. package/dist/components/util.d.ts +0 -1
  89. package/dist/components/util.js +0 -7
  90. package/dist/components/util.js.map +0 -1
  91. package/dist/webpack.js.map +0 -1
  92. package/src/components/util.ts +0 -9
  93. /package/dist/{webpack.d.ts → umd.d.ts} +0 -0
  94. /package/src/{webpack.ts → umd.ts} +0 -0
package/dist/model.d.ts CHANGED
@@ -6,6 +6,14 @@ import type { Instance } from '@jbrowse/mobx-state-tree';
6
6
  import type { Theme } from '@mui/material';
7
7
  /**
8
8
  * #stateModel MsaView
9
+ *
10
+ * The main MSAView state model. Holds the loaded alignment, tree, and optional
11
+ * InterProScan domain annotations, plus all display state (color scheme, zoom,
12
+ * scroll, collapsed clades). It composes in members from `DialogQueueSessionMixin`,
13
+ * `Tree`, and `MSAModel` (see Inherited members below). Data is loaded reactively
14
+ * from the `msaFilehandle` / `treeFilehandle` / `gffFilehandle` properties, or set
15
+ * directly with `setData`. Most state is persisted into the shareable URL.
16
+ *
9
17
  * #example
10
18
  * ```js
11
19
  * import { MSAModelF } from 'react-msaview'
@@ -28,7 +36,7 @@ declare function stateModelFactory(): import("@jbrowse/mobx-state-tree").IModelT
28
36
  bgColor: import("@jbrowse/mobx-state-tree").IType<boolean | undefined, boolean, boolean>;
29
37
  colorSchemeName: import("@jbrowse/mobx-state-tree").IType<string | undefined, string, string>;
30
38
  msaFormat: import("@jbrowse/mobx-state-tree").IMaybe<import("@jbrowse/mobx-state-tree").ISimpleType<import("msa-parsers").MSAFormat>>;
31
- }, "type" | "id" | "data" | "showDomains" | "hideGaps" | "allowedGappyness" | "subFeatureRows" | "drawMsaLetters" | "scrollZoom" | "rowHeight" | "scrollY" | "scrollX" | "colWidth" | "currentAlignment" | "height" | "treeFilehandle" | "msaFilehandle" | "treeMetadataFilehandle" | "gffFilehandle" | "collapsed" | "showOnly" | "turnedOffTracks" | "featureFilters" | "relativeTo"> & {
39
+ }, "type" | "id" | "data" | "showDomains" | "hideGaps" | "allowedGappyness" | "subFeatureRows" | "drawMsaLetters" | "scrollZoom" | "rowHeight" | "scrollY" | "scrollX" | "colWidth" | "currentAlignment" | "height" | "treeFilehandle" | "msaFilehandle" | "treeMetadataFilehandle" | "gffFilehandle" | "collapsed" | "showOnly" | "turnedOffTracks" | "featureFilters" | "relativeTo" | "highlightColumns"> & {
32
40
  id: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<string>, [undefined]>;
33
41
  showDomains: import("@jbrowse/mobx-state-tree").IType<boolean | undefined, boolean, boolean>;
34
42
  hideGaps: import("@jbrowse/mobx-state-tree").IType<boolean | undefined, boolean, boolean>;
@@ -144,6 +152,7 @@ declare function stateModelFactory(): import("@jbrowse/mobx-state-tree").IModelT
144
152
  }>, [undefined]>;
145
153
  featureFilters: import("@jbrowse/mobx-state-tree").IMapType<import("@jbrowse/mobx-state-tree").ISimpleType<boolean>>;
146
154
  relativeTo: import("@jbrowse/mobx-state-tree").IMaybe<import("@jbrowse/mobx-state-tree").ISimpleType<string>>;
155
+ highlightColumns: import("@jbrowse/mobx-state-tree").IType<number[] | undefined, number[] | undefined, number[] | undefined>;
147
156
  }, {
148
157
  queueOfDialogs: [import("@jbrowse/core/util").DialogComponentType, any][];
149
158
  } & {
@@ -262,10 +271,13 @@ declare function stateModelFactory(): import("@jbrowse/mobx-state-tree").IModelT
262
271
  } & {
263
272
  /**
264
273
  * #action
274
+ * draw the alignment with positions numbered relative to the given row's
275
+ * sequence (its node id), instead of in raw MSA-column coordinates
265
276
  */
266
277
  drawRelativeTo(id: string | undefined): void;
267
278
  /**
268
279
  * #action
280
+ * hide columns that are entirely (or mostly, see allowedGappyness) gaps
269
281
  */
270
282
  setHideGaps(arg: boolean): void;
271
283
  /**
@@ -320,6 +332,7 @@ declare function stateModelFactory(): import("@jbrowse/mobx-state-tree").IModelT
320
332
  setHighlightedColumns(columns?: number[]): void;
321
333
  /**
322
334
  * #action
335
+ * toggle the InterProScan protein-domain overlay on the alignment
323
336
  */
324
337
  setShowDomains(arg: boolean): void;
325
338
  /**
@@ -353,14 +366,19 @@ declare function stateModelFactory(): import("@jbrowse/mobx-state-tree").IModelT
353
366
  setCurrentAlignment(n: number): void;
354
367
  /**
355
368
  * #action
369
+ * collapse or un-collapse the subtree rooted at the given tree node id
356
370
  */
357
371
  toggleCollapsed(node: string): void;
358
372
  /**
359
373
  * #action
374
+ * show only the subtree rooted at the given node id (pass undefined to
375
+ * show the whole tree again)
360
376
  */
361
377
  setShowOnly(node?: string): void;
362
378
  /**
363
379
  * #action
380
+ * set the alignment/tree/metadata/domain data directly from strings,
381
+ * bypassing the filehandle loaders
364
382
  */
365
383
  setData(data: {
366
384
  msa?: string;
@@ -674,6 +692,7 @@ declare function stateModelFactory(): import("@jbrowse/mobx-state-tree").IModelT
674
692
  calculateNeighborJoiningTreeFromMSA(): void;
675
693
  /**
676
694
  * #action
695
+ * restore the default column width and row height
677
696
  */
678
697
  resetZoom(): void;
679
698
  /**
@@ -852,32 +871,40 @@ declare function stateModelFactory(): import("@jbrowse/mobx-state-tree").IModelT
852
871
  name: string;
853
872
  accession: string;
854
873
  description: string;
874
+ featureType: string | undefined;
855
875
  start: number;
856
876
  end: number;
877
+ strand: number | undefined;
857
878
  }>;
858
879
  readonly tidyInterProAnnotations: {
859
880
  id: string;
860
881
  name: string;
861
882
  accession: string;
862
883
  description: string;
884
+ featureType: string | undefined;
863
885
  start: number;
864
886
  end: number;
887
+ strand: number | undefined;
865
888
  }[];
866
889
  readonly tidyFilteredInterProAnnotations: {
867
890
  id: string;
868
891
  name: string;
869
892
  accession: string;
870
893
  description: string;
894
+ featureType: string | undefined;
871
895
  start: number;
872
896
  end: number;
897
+ strand: number | undefined;
873
898
  }[];
874
899
  readonly tidyFilteredGatheredInterProAnnotations: Record<string, {
875
900
  id: string;
876
901
  name: string;
877
902
  accession: string;
878
903
  description: string;
904
+ featureType: string | undefined;
879
905
  start: number;
880
906
  end: number;
907
+ strand: number | undefined;
881
908
  }[]>;
882
909
  } & {
883
910
  /**
@@ -889,12 +916,81 @@ declare function stateModelFactory(): import("@jbrowse/mobx-state-tree").IModelT
889
916
  * #getter
890
917
  */
891
918
  readonly verticalScrollbarWidth: 0 | 20;
919
+ /**
920
+ * #getter
921
+ * ordinal segment types (exons etc.), ordered by sequence position so
922
+ * exon-1..exon-14 read left-to-right; colored by alternating shade and
923
+ * labeled by number rather than each getting a distinct hue + legend row
924
+ */
925
+ readonly segmentDomainTypes: {
926
+ id: string;
927
+ name: string;
928
+ accession: string;
929
+ description: string;
930
+ featureType: string | undefined;
931
+ start: number;
932
+ end: number;
933
+ strand: number | undefined;
934
+ }[];
935
+ /**
936
+ * #getter
937
+ * categorical feature types (InterPro domains and the like) that each get
938
+ * their own color and a legend entry
939
+ */
940
+ readonly categoricalDomainTypes: {
941
+ id: string;
942
+ name: string;
943
+ accession: string;
944
+ description: string;
945
+ featureType: string | undefined;
946
+ start: number;
947
+ end: number;
948
+ strand: number | undefined;
949
+ }[];
892
950
  readonly fillPalette: {
893
- [k: string]: string;
951
+ [x: string]: string;
894
952
  };
895
953
  readonly strokePalette: {
896
954
  [k: string]: string;
897
955
  };
956
+ /**
957
+ * #getter
958
+ * accession -> number drawn on each segment band: the trailing number of
959
+ * the feature name ("exon-3" -> "3"), else its 1-based position
960
+ */
961
+ readonly segmentLabels: Map<string, string>;
962
+ /**
963
+ * #getter
964
+ * the domain types currently drawn on the alignment (filtered-on), shared
965
+ * by the on-screen legend and the SVG export legend. Ordinal segments
966
+ * (exons) are excluded — they read as a numbered gene model, not a color
967
+ * key — so this is the categorical types ordered by sequence position
968
+ */
969
+ readonly visibleDomainTypes: {
970
+ id: string;
971
+ name: string;
972
+ accession: string;
973
+ description: string;
974
+ featureType: string | undefined;
975
+ start: number;
976
+ end: number;
977
+ strand: number | undefined;
978
+ }[];
979
+ /**
980
+ * #getter
981
+ * domain annotations under the mouse, hit-tested against the exact visible
982
+ * column span each box is drawn at (so it matches the overlay across gaps)
983
+ */
984
+ readonly mouseOverDomains: {
985
+ id: string;
986
+ name: string;
987
+ accession: string;
988
+ description: string;
989
+ featureType: string | undefined;
990
+ start: number;
991
+ end: number;
992
+ strand: number | undefined;
993
+ }[];
898
994
  /**
899
995
  * #method
900
996
  */
@@ -955,7 +1051,7 @@ declare function stateModelFactory(): import("@jbrowse/mobx-state-tree").IModelT
955
1051
  bgColor: import("@jbrowse/mobx-state-tree").IType<boolean | undefined, boolean, boolean>;
956
1052
  colorSchemeName: import("@jbrowse/mobx-state-tree").IType<string | undefined, string, string>;
957
1053
  msaFormat: import("@jbrowse/mobx-state-tree").IMaybe<import("@jbrowse/mobx-state-tree").ISimpleType<import("msa-parsers").MSAFormat>>;
958
- }, "type" | "id" | "data" | "showDomains" | "hideGaps" | "allowedGappyness" | "subFeatureRows" | "drawMsaLetters" | "scrollZoom" | "rowHeight" | "scrollY" | "scrollX" | "colWidth" | "currentAlignment" | "height" | "treeFilehandle" | "msaFilehandle" | "treeMetadataFilehandle" | "gffFilehandle" | "collapsed" | "showOnly" | "turnedOffTracks" | "featureFilters" | "relativeTo"> & {
1054
+ }, "type" | "id" | "data" | "showDomains" | "hideGaps" | "allowedGappyness" | "subFeatureRows" | "drawMsaLetters" | "scrollZoom" | "rowHeight" | "scrollY" | "scrollX" | "colWidth" | "currentAlignment" | "height" | "treeFilehandle" | "msaFilehandle" | "treeMetadataFilehandle" | "gffFilehandle" | "collapsed" | "showOnly" | "turnedOffTracks" | "featureFilters" | "relativeTo" | "highlightColumns"> & {
959
1055
  id: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<string>, [undefined]>;
960
1056
  showDomains: import("@jbrowse/mobx-state-tree").IType<boolean | undefined, boolean, boolean>;
961
1057
  hideGaps: import("@jbrowse/mobx-state-tree").IType<boolean | undefined, boolean, boolean>;
@@ -1071,6 +1167,7 @@ declare function stateModelFactory(): import("@jbrowse/mobx-state-tree").IModelT
1071
1167
  }>, [undefined]>;
1072
1168
  featureFilters: import("@jbrowse/mobx-state-tree").IMapType<import("@jbrowse/mobx-state-tree").ISimpleType<boolean>>;
1073
1169
  relativeTo: import("@jbrowse/mobx-state-tree").IMaybe<import("@jbrowse/mobx-state-tree").ISimpleType<string>>;
1170
+ highlightColumns: import("@jbrowse/mobx-state-tree").IType<number[] | undefined, number[] | undefined, number[] | undefined>;
1074
1171
  }>, symbol>>;
1075
1172
  export default stateModelFactory;
1076
1173
  export type MsaViewStateModel = ReturnType<typeof stateModelFactory>;
package/dist/model.js CHANGED
@@ -10,7 +10,7 @@ import { clustalXColumnColors } from "./clustalX.js";
10
10
  import colorSchemes from "./colorSchemes.js";
11
11
  import ConservationTrack from "./components/ConservationTrack.js";
12
12
  import TextTrack from "./components/TextTrack.js";
13
- import { defaultAllowedGappyness, defaultBgColor, defaultColWidth, defaultColorSchemeName, defaultCurrentAlignment, defaultDrawLabels, defaultDrawMsaLetters, defaultDrawNodeBubbles, defaultDrawTree, defaultHeight, defaultHideGaps, defaultLabelsAlignRight, defaultRowHeight, defaultScrollX, defaultScrollY, defaultScrollZoom, defaultShowBranchLen, defaultShowDomains, defaultSubFeatureRows, defaultTreeAreaWidth, defaultTreeWidth, maxCellSize, minColWidth, minRowHeight, } from "./constants.js";
13
+ import { defaultAllowedGappyness, defaultBgColor, defaultColWidth, defaultColorSchemeName, defaultCurrentAlignment, defaultDrawLabels, defaultDrawMsaLetters, defaultDrawNodeBubbles, defaultDrawTree, defaultHeight, defaultHideGaps, defaultLabelsAlignRight, defaultRowHeight, defaultScrollX, defaultScrollY, defaultScrollZoom, defaultShowBranchLen, defaultShowDomains, defaultSubFeatureRows, defaultTreeAreaWidth, defaultTreeWidth, maxCellSize, minColWidth, minRowHeight, segmentFeatureTypes, segmentShades, } from "./constants.js";
14
14
  import { createPaletteMap } from "./createPaletteMap.js";
15
15
  import { fetchTextWithProgress, isAbortError } from "./fetchUtils.js";
16
16
  import { flatToTree } from "./flatToTree.js";
@@ -34,6 +34,14 @@ function parseTreeText(text) {
34
34
  }
35
35
  /**
36
36
  * #stateModel MsaView
37
+ *
38
+ * The main MSAView state model. Holds the loaded alignment, tree, and optional
39
+ * InterProScan domain annotations, plus all display state (color scheme, zoom,
40
+ * scroll, collapsed clades). It composes in members from `DialogQueueSessionMixin`,
41
+ * `Tree`, and `MSAModel` (see Inherited members below). Data is loaded reactively
42
+ * from the `msaFilehandle` / `treeFilehandle` / `gffFilehandle` properties, or set
43
+ * directly with `setData`. Most state is persisted into the shareable URL.
44
+ *
37
45
  * #example
38
46
  * ```js
39
47
  * import { MSAModelF } from 'react-msaview'
@@ -166,6 +174,15 @@ function stateModelFactory() {
166
174
  * #property
167
175
  */
168
176
  relativeTo: types.maybe(types.string),
177
+ /**
178
+ * #property
179
+ * declarative seed for the highlighted-columns overlay (visible column
180
+ * indices). Unlike the volatile `highlightedColumns` (driven by
181
+ * transient genome-hover sync), this persists in the snapshot/URL so a
182
+ * shared link can open with specific columns highlighted. Applied once
183
+ * in afterCreate.
184
+ */
185
+ highlightColumns: types.frozen(),
169
186
  }))
170
187
  .volatile(() => ({
171
188
  /**
@@ -260,12 +277,15 @@ function stateModelFactory() {
260
277
  .actions(self => ({
261
278
  /**
262
279
  * #action
280
+ * draw the alignment with positions numbered relative to the given row's
281
+ * sequence (its node id), instead of in raw MSA-column coordinates
263
282
  */
264
283
  drawRelativeTo(id) {
265
284
  self.relativeTo = id;
266
285
  },
267
286
  /**
268
287
  * #action
288
+ * hide columns that are entirely (or mostly, see allowedGappyness) gaps
269
289
  */
270
290
  setHideGaps(arg) {
271
291
  self.hideGaps = arg;
@@ -341,6 +361,7 @@ function stateModelFactory() {
341
361
  },
342
362
  /**
343
363
  * #action
364
+ * toggle the InterProScan protein-domain overlay on the alignment
344
365
  */
345
366
  setShowDomains(arg) {
346
367
  self.showDomains = arg;
@@ -389,6 +410,7 @@ function stateModelFactory() {
389
410
  },
390
411
  /**
391
412
  * #action
413
+ * collapse or un-collapse the subtree rooted at the given tree node id
392
414
  */
393
415
  toggleCollapsed(node) {
394
416
  if (self.collapsed.includes(node)) {
@@ -400,12 +422,16 @@ function stateModelFactory() {
400
422
  },
401
423
  /**
402
424
  * #action
425
+ * show only the subtree rooted at the given node id (pass undefined to
426
+ * show the whole tree again)
403
427
  */
404
428
  setShowOnly(node) {
405
429
  self.showOnly = node;
406
430
  },
407
431
  /**
408
432
  * #action
433
+ * set the alignment/tree/metadata/domain data directly from strings,
434
+ * bypassing the filehandle loaders
409
435
  */
410
436
  setData(data) {
411
437
  self.data = cast(data);
@@ -794,6 +820,9 @@ function stateModelFactory() {
794
820
  const { colStats, colStatsSums } = this;
795
821
  return colStats.map((stats, i) => {
796
822
  const total = colStatsSums[i];
823
+ if (!total) {
824
+ return { letter: '', color: undefined };
825
+ }
797
826
  let maxCount = 0;
798
827
  let letter = '';
799
828
  for (const [key, val] of Object.entries(stats)) {
@@ -1044,6 +1073,7 @@ function stateModelFactory() {
1044
1073
  },
1045
1074
  /**
1046
1075
  * #action
1076
+ * restore the default column width and row height
1047
1077
  */
1048
1078
  resetZoom() {
1049
1079
  self.setColWidth(defaultColWidth);
@@ -1080,9 +1110,8 @@ function stateModelFactory() {
1080
1110
  */
1081
1111
  zoomIn() {
1082
1112
  transaction(() => {
1083
- self.colWidth = Math.min(maxCellSize, Math.ceil(self.colWidth * 1.5));
1084
- self.rowHeight = Math.min(maxCellSize, Math.ceil(self.rowHeight * 1.5));
1085
- self.scrollX = clamp(self.scrollX, self.maxScrollX, 0);
1113
+ this.zoomInHorizontal();
1114
+ this.zoomInVertical();
1086
1115
  });
1087
1116
  },
1088
1117
  /**
@@ -1090,9 +1119,8 @@ function stateModelFactory() {
1090
1119
  */
1091
1120
  zoomOut() {
1092
1121
  transaction(() => {
1093
- self.colWidth = Math.max(minColWidth, Math.floor(self.colWidth * 0.75));
1094
- self.rowHeight = Math.max(minRowHeight, Math.floor(self.rowHeight * 0.75));
1095
- self.scrollX = clamp(self.scrollX, self.maxScrollX, 0);
1122
+ this.zoomOutHorizontal();
1123
+ this.zoomOutVertical();
1096
1124
  });
1097
1125
  },
1098
1126
  /**
@@ -1113,8 +1141,12 @@ function stateModelFactory() {
1113
1141
  self.rowHeight = clamp(self.rowHeight * scaleFactor, minRowHeight, maxCellSize);
1114
1142
  self.scrollX = clamp(offsetX - colInView * self.colWidth, self.maxScrollX, 0);
1115
1143
  const anchoredScrollY = offsetY - rowInView * self.rowHeight;
1116
- const overflow = Math.max(0, self.totalHeight - self.height);
1117
- const topBias = self.height > 0 ? clamp(1 - overflow / self.height, 0, 1) : 0;
1144
+ // maxScrollY is -(totalHeight - visibleMsaHeight) when the alignment
1145
+ // overflows, so -maxScrollY is exactly that overflow past the
1146
+ // scrollable MSA viewport (0 when it fits)
1147
+ const overflow = Math.max(0, -self.maxScrollY);
1148
+ const visibleHeight = self.totalHeight - overflow;
1149
+ const topBias = visibleHeight > 0 ? clamp(1 - overflow / visibleHeight, 0, 1) : 1;
1118
1150
  self.scrollY = clamp(anchoredScrollY * (1 - topBias), self.maxScrollY, 0);
1119
1151
  });
1120
1152
  },
@@ -1356,13 +1388,15 @@ function stateModelFactory() {
1356
1388
  .flatMap(([id, val]) => val.matches.flatMap(({ signature, locations }) => {
1357
1389
  const { entry } = signature;
1358
1390
  return entry
1359
- ? locations.map(({ start, end }) => ({
1391
+ ? locations.map(({ start, end, strand }) => ({
1360
1392
  id,
1361
1393
  name: entry.name,
1362
1394
  accession: entry.accession,
1363
1395
  description: entry.description,
1396
+ featureType: entry.featureType,
1364
1397
  start,
1365
1398
  end,
1399
+ strand,
1366
1400
  }))
1367
1401
  : [];
1368
1402
  }))
@@ -1391,8 +1425,32 @@ function stateModelFactory() {
1391
1425
  get verticalScrollbarWidth() {
1392
1426
  return self.showVerticalScrollbar ? 20 : 0;
1393
1427
  },
1428
+ /**
1429
+ * #getter
1430
+ * ordinal segment types (exons etc.), ordered by sequence position so
1431
+ * exon-1..exon-14 read left-to-right; colored by alternating shade and
1432
+ * labeled by number rather than each getting a distinct hue + legend row
1433
+ */
1434
+ get segmentDomainTypes() {
1435
+ return [...self.tidyInterProAnnotationTypes.values()]
1436
+ .filter(d => segmentFeatureTypes.has(d.featureType ?? ''))
1437
+ .toSorted((a, b) => a.start - b.start);
1438
+ },
1439
+ /**
1440
+ * #getter
1441
+ * categorical feature types (InterPro domains and the like) that each get
1442
+ * their own color and a legend entry
1443
+ */
1444
+ get categoricalDomainTypes() {
1445
+ return [...self.tidyInterProAnnotationTypes.values()].filter(d => !segmentFeatureTypes.has(d.featureType ?? ''));
1446
+ },
1394
1447
  get fillPalette() {
1395
- return createPaletteMap([...self.tidyInterProAnnotationTypes.keys()]);
1448
+ const segments = Object.fromEntries(this.segmentDomainTypes.map((d, i) => [
1449
+ d.accession,
1450
+ segmentShades[i % segmentShades.length],
1451
+ ]));
1452
+ const categorical = createPaletteMap(this.categoricalDomainTypes.map(d => d.accession));
1453
+ return { ...segments, ...categorical };
1396
1454
  },
1397
1455
  get strokePalette() {
1398
1456
  return transform(this.fillPalette, ([key, val]) => [
@@ -1400,6 +1458,50 @@ function stateModelFactory() {
1400
1458
  colord(val).darken(0.1).toHex(),
1401
1459
  ]);
1402
1460
  },
1461
+ /**
1462
+ * #getter
1463
+ * accession -> number drawn on each segment band: the trailing number of
1464
+ * the feature name ("exon-3" -> "3"), else its 1-based position
1465
+ */
1466
+ get segmentLabels() {
1467
+ return new Map(this.segmentDomainTypes.map((d, i) => {
1468
+ const m = /(\d+)\s*$/.exec(d.name);
1469
+ return [d.accession, m ? m[1] : `${i + 1}`];
1470
+ }));
1471
+ },
1472
+ /**
1473
+ * #getter
1474
+ * the domain types currently drawn on the alignment (filtered-on), shared
1475
+ * by the on-screen legend and the SVG export legend. Ordinal segments
1476
+ * (exons) are excluded — they read as a numbered gene model, not a color
1477
+ * key — so this is the categorical types ordered by sequence position
1478
+ */
1479
+ get visibleDomainTypes() {
1480
+ return this.categoricalDomainTypes
1481
+ .filter(d => self.featureFilters.get(d.accession))
1482
+ .toSorted((a, b) => a.start - b.start);
1483
+ },
1484
+ /**
1485
+ * #getter
1486
+ * domain annotations under the mouse, hit-tested against the exact visible
1487
+ * column span each box is drawn at (so it matches the overlay across gaps)
1488
+ */
1489
+ get mouseOverDomains() {
1490
+ const { mouseCol } = self;
1491
+ const name = self.mouseOverRowName;
1492
+ if (name !== undefined && mouseCol !== undefined) {
1493
+ const entry = self.tidyFilteredGatheredInterProAnnotations[name] ?? [];
1494
+ return entry.filter(d => {
1495
+ const m1 = self.seqPosToVisibleCol(name, d.start - 1);
1496
+ const m2 = self.seqPosToVisibleCol(name, d.end);
1497
+ return (m1 !== undefined &&
1498
+ m2 !== undefined &&
1499
+ mouseCol >= m1 &&
1500
+ mouseCol < m2);
1501
+ });
1502
+ }
1503
+ return [];
1504
+ },
1403
1505
  /**
1404
1506
  * #method
1405
1507
  */
@@ -1462,14 +1564,8 @@ function stateModelFactory() {
1462
1564
  * #action
1463
1565
  */
1464
1566
  fit() {
1465
- if (self.numRows > 0) {
1466
- self.rowHeight = clamp(self.msaAreaHeight / self.numRows, minRowHeight, maxCellSize);
1467
- }
1468
- if (self.numColumns > 0) {
1469
- self.colWidth = clamp(self.msaAreaWidth / self.numColumns, minColWidth, maxCellSize);
1470
- }
1471
- self.scrollX = 0;
1472
- self.scrollY = 0;
1567
+ this.fitVertically();
1568
+ this.fitHorizontally();
1473
1569
  },
1474
1570
  /**
1475
1571
  * #action
@@ -1490,6 +1586,12 @@ function stateModelFactory() {
1490
1586
  self.scrollX = 0;
1491
1587
  },
1492
1588
  afterCreate() {
1589
+ // seed the highlighted-columns overlay from the declarative property so
1590
+ // a shared snapshot/URL opens with those columns highlighted (the
1591
+ // volatile highlightedColumns can later be driven by genome-hover sync)
1592
+ if (self.highlightColumns?.length) {
1593
+ self.setHighlightedColumns(self.highlightColumns);
1594
+ }
1493
1595
  addDisposer(self, autorun(() => {
1494
1596
  for (const key of self.tidyInterProAnnotationTypes.keys()) {
1495
1597
  this.initFilter(key);