@teselagen/ove 0.8.3 → 0.8.5

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 (54) hide show
  1. package/AlignmentView/LabileSitesLayer.d.ts +13 -0
  2. package/AlignmentView/PairwiseAlignmentView.d.ts +1 -9
  3. package/BarPlot/index.d.ts +33 -0
  4. package/LinearView/SequenceName.d.ts +2 -1
  5. package/PropertySidePanel/calculateAminoAcidFrequency.d.ts +46 -0
  6. package/PropertySidePanel/index.d.ts +6 -0
  7. package/RowItem/Caret/index.d.ts +2 -1
  8. package/StatusBar/index.d.ts +2 -1
  9. package/aaprops.svg +2287 -0
  10. package/constants/dnaToColor.d.ts +122 -4
  11. package/index.cjs.js +4184 -2814
  12. package/index.es.js +2137 -767
  13. package/index.umd.js +3715 -2345
  14. package/ove.css +92 -6
  15. package/package.json +3 -3
  16. package/src/AlignmentView/AlignmentVisibilityTool.js +141 -37
  17. package/src/AlignmentView/LabileSitesLayer.js +33 -0
  18. package/src/AlignmentView/Minimap.js +5 -3
  19. package/src/AlignmentView/PairwiseAlignmentView.js +55 -61
  20. package/src/AlignmentView/index.js +476 -257
  21. package/src/AlignmentView/style.css +27 -0
  22. package/src/BarPlot/index.js +156 -0
  23. package/src/CircularView/Caret.js +8 -2
  24. package/src/CircularView/SelectionLayer.js +4 -2
  25. package/src/CircularView/index.js +5 -1
  26. package/src/Editor/darkmode.css +7 -0
  27. package/src/Editor/index.js +3 -0
  28. package/src/Editor/userDefinedHandlersAndOpts.js +2 -1
  29. package/src/FindBar/index.js +2 -3
  30. package/src/LinearView/SequenceName.js +8 -2
  31. package/src/LinearView/index.js +21 -0
  32. package/src/PropertySidePanel/calculateAminoAcidFrequency.js +77 -0
  33. package/src/PropertySidePanel/index.js +236 -0
  34. package/src/PropertySidePanel/style.css +39 -0
  35. package/src/RowItem/Caret/index.js +8 -2
  36. package/src/RowItem/Labels.js +1 -1
  37. package/src/RowItem/SelectionLayer/index.js +5 -1
  38. package/src/RowItem/Sequence.js +99 -5
  39. package/src/RowItem/Translations/Translation.js +3 -2
  40. package/src/RowItem/Translations/index.js +2 -0
  41. package/src/RowItem/index.js +74 -8
  42. package/src/RowItem/style.css +3 -4
  43. package/src/StatusBar/index.js +11 -4
  44. package/src/constants/dnaToColor.js +151 -0
  45. package/src/helperComponents/PinchHelper/PinchHelper.js +5 -1
  46. package/src/helperComponents/PropertiesDialog/index.js +2 -2
  47. package/src/helperComponents/SelectDialog.js +5 -2
  48. package/src/style.css +2 -2
  49. package/src/utils/editorUtils.js +5 -3
  50. package/src/utils/getAlignedAminoAcidSequenceProps.js +379 -0
  51. package/src/withEditorInteractions/createSequenceInputPopup.js +15 -5
  52. package/src/withEditorInteractions/index.js +3 -1
  53. package/utils/editorUtils.d.ts +2 -1
  54. package/utils/getAlignedAminoAcidSequenceProps.d.ts +49 -0
@@ -11,6 +11,12 @@
11
11
  .alignmentViewTrackContainer {
12
12
  position: relative;
13
13
  }
14
+ .alignmentViewTrackContainer.isTrackSelected {
15
+ background-color: #c0e2f5;
16
+ }
17
+ .bp3-dark .alignmentViewTrackContainer.isTrackSelected {
18
+ background-color: #2277a9;
19
+ }
14
20
  .alignmentViewTrackContainer,
15
21
  .alignmentViewTrackContainer .veVectorInteractionWrapper,
16
22
  .alignmentViewTrackContainer .veVectorInteractionWrapper > div {
@@ -91,6 +97,11 @@
91
97
  .minimapLane.lane-hovered .miniBluePath {
92
98
  fill: rgb(169, 169, 245) !important;
93
99
  }
100
+ .minimapLane.isTrackSelected .miniBluePath {
101
+ fill: rgb(137, 168, 255) !important;
102
+ stroke: rgb(0, 0, 0);
103
+ stroke-width: 0.5px;
104
+ }
94
105
 
95
106
  .veAlignmentName {
96
107
  padding-top: 1px;
@@ -105,3 +116,19 @@
105
116
  text-overflow: ellipsis;
106
117
  white-space: nowrap;
107
118
  }
119
+
120
+ .veLabileSites {
121
+ position: absolute;
122
+ top: 0;
123
+ height: 100%;
124
+ z-index: 10;
125
+ }
126
+
127
+ .veAlignmentViewLabileSiteLine {
128
+ position: absolute;
129
+ height: 100%;
130
+ width: 4px;
131
+ background: #1585c5;
132
+ opacity: 0.5;
133
+ top: 0;
134
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Minimal SVG BarPlot component
3
+ * @param {Object} props
4
+ * @param {number[]} props.data - Array of numbers to plot
5
+ * @param {number} [props.width=300]
6
+ * @param {number} [props.height=150]
7
+ * @param {string[]} [props.barColors]
8
+ */
9
+ export function BarPlot({
10
+ data,
11
+ width = 300,
12
+ height = 30,
13
+ barColors,
14
+ className
15
+ }) {
16
+ if (!data || data.length === 0) return null;
17
+ const maxVal = Math.max(...data);
18
+ const barWidth = width / data.length;
19
+
20
+ return (
21
+ <svg width={width} height={height} className={className}>
22
+ {data.map((val, i) => {
23
+ const barHeight = (val / maxVal) * (height - 2);
24
+ return (
25
+ <rect
26
+ data-tip={`${val?.toFixed(1)}%`}
27
+ key={i}
28
+ x={i * barWidth + 1}
29
+ y={height - barHeight}
30
+ width={barWidth - 2}
31
+ height={barHeight}
32
+ fill={barColors ? barColors[i % barColors.length] : "#3498db"}
33
+ rx={2}
34
+ />
35
+ );
36
+ })}
37
+ {/* 50% horizontal line */}
38
+ <line
39
+ x1={1}
40
+ y1={height - 0.5 * (height - 2)}
41
+ x2={width}
42
+ y2={height - 0.5 * (height - 2)}
43
+ stroke="white"
44
+ strokeDasharray="4,2"
45
+ strokeWidth={1}
46
+ />
47
+ <line
48
+ x1={0}
49
+ y1={height}
50
+ x2={width}
51
+ y2={height}
52
+ stroke="#333"
53
+ strokeWidth={1}
54
+ />
55
+ </svg>
56
+ );
57
+ }
58
+
59
+ export function AminoAcidCirclePlot({ data, width, className }) {
60
+ // width: total SVG width to fit all circles
61
+ const n = data.length;
62
+ if (n === 0) return null;
63
+
64
+ // Calculate spacing and radius so circles fit the width
65
+ const padding = 5; // space at each end
66
+ const availableWidth = width - 3 * padding;
67
+ const spacing = n > 1 ? availableWidth / (n - 1) : 0;
68
+ // Make radius as large as possible without overlap
69
+ const maxRadius = spacing / 2 - 2 > 0 ? spacing / 2 - 2 : 8;
70
+ const radius = Math.max(8, Math.min(maxRadius, 20));
71
+ const svgHeight = radius * 2 + 10;
72
+
73
+ return (
74
+ <svg width={width} height={svgHeight} className={className}>
75
+ {/* Circles */}
76
+ {data.map((d, idx) => (
77
+ <g key={idx}>
78
+ <circle
79
+ data-tip={d.group}
80
+ cx={idx * spacing + radius}
81
+ cy={radius + 2}
82
+ r={radius}
83
+ fill={d.color}
84
+ stroke="#333"
85
+ strokeWidth={1}
86
+ />
87
+ </g>
88
+ ))}
89
+ </svg>
90
+ );
91
+ }
92
+
93
+ /**
94
+ * Stacked SVG BarPlot component for multiple properties per bar
95
+ * @param {Object} props
96
+ * @param {number[][]} props.data - Array of arrays, each inner array is the values for that position
97
+ * @param {number} [props.width=300]
98
+ * @param {number} [props.height=30]
99
+ * @param {string[]} [props.barColors]
100
+ */
101
+ export function StackedBarPlot({ data, width = 300, height = 30, barColors }) {
102
+ if (!data || data.length === 0) return null;
103
+
104
+ const _data = data.map(vals => Object.values(vals));
105
+ const legends = data.map(vals => Object.keys(vals));
106
+
107
+ // Find the max sum for stacking
108
+ const maxVal = Math.max(
109
+ ..._data.map(vals => vals.reduce((a, b) => a + b, 0))
110
+ );
111
+ const barWidth = width / _data.length;
112
+
113
+ return (
114
+ <svg width={width} height={height}>
115
+ {_data.map((vals, i) => {
116
+ let yOffset = height;
117
+ return vals.map((val, j) => {
118
+ const barHeight = (val / maxVal) * (height - 2);
119
+ yOffset -= barHeight;
120
+ return (
121
+ <rect
122
+ data-tip={legends[i][j]}
123
+ key={j}
124
+ x={i * barWidth + 1}
125
+ y={yOffset}
126
+ width={barWidth - 2}
127
+ height={barHeight}
128
+ fill={barColors ? barColors[j % barColors.length] : colorMap[j]}
129
+ rx={2}
130
+ />
131
+ );
132
+ });
133
+ })}
134
+ <line
135
+ x1={0}
136
+ y1={height}
137
+ x2={width}
138
+ y2={height}
139
+ stroke="#333"
140
+ strokeWidth={1}
141
+ />
142
+ </svg>
143
+ );
144
+ }
145
+
146
+ const colorMap = [
147
+ "#1f77b4", // blue
148
+ "#ff7f0e", // orange
149
+ "#2ca02c", // green
150
+ "#d62728", // red
151
+ "#9467bd", // purple
152
+ "#8c564b", // brown
153
+ "#e377c2", // pink
154
+ "#7f7f7f", // gray
155
+ "#bcbd22" // olive
156
+ ];
@@ -15,7 +15,8 @@ function Caret({
15
15
  innerRadius,
16
16
  outerRadius,
17
17
  isProtein,
18
- selectionMessage
18
+ selectionMessage,
19
+ showAminoAcidUnitAsCodon
19
20
  }) {
20
21
  const { startAngle, endAngle } = getRangeAngles(
21
22
  { start: caretPosition, end: caretPosition },
@@ -37,7 +38,12 @@ function Caret({
37
38
  >
38
39
  <title>
39
40
  {selectionMessage ||
40
- getSelectionMessage({ caretPosition, isProtein, sequenceLength })}
41
+ getSelectionMessage({
42
+ caretPosition,
43
+ isProtein,
44
+ sequenceLength,
45
+ showAminoAcidUnitAsCodon
46
+ })}
41
47
  </title>
42
48
  <line
43
49
  strokeWidth="1.5px"
@@ -21,7 +21,8 @@ function SelectionLayer({
21
21
  onRightClicked,
22
22
  onClick,
23
23
  index,
24
- isProtein
24
+ isProtein,
25
+ showAminoAcidUnitAsCodon
25
26
  }) {
26
27
  const {
27
28
  color,
@@ -47,7 +48,8 @@ function SelectionLayer({
47
48
  const selectionMessage = getSelectionMessage({
48
49
  sequenceLength,
49
50
  selectionLayer,
50
- isProtein
51
+ isProtein,
52
+ showAminoAcidUnitAsCodon
51
53
  });
52
54
  // let section2 = sector({
53
55
  // center: [0, 0], //the center is always 0,0 for our annotations :) we rotate later!
@@ -168,6 +168,7 @@ export function CircularView(props) {
168
168
  readOnly,
169
169
  hideName = false,
170
170
  editorName,
171
+ showAminoAcidUnitAsCodon,
171
172
  smartCircViewLabelRender,
172
173
  showCicularViewInternalLabels,
173
174
  withRotateCircularView: _withRotateCircularView,
@@ -600,6 +601,7 @@ export function CircularView(props) {
600
601
  key={"veCircularViewSelectionLayer" + index}
601
602
  {...{
602
603
  index,
604
+ showAminoAcidUnitAsCodon,
603
605
  isDraggable: true,
604
606
  isProtein,
605
607
  selectionLayer,
@@ -637,6 +639,7 @@ export function CircularView(props) {
637
639
  key="veCircularViewCaret"
638
640
  {...{
639
641
  caretPosition,
642
+ showAminoAcidUnitAsCodon,
640
643
  sequenceLength,
641
644
  isProtein,
642
645
  innerRadius,
@@ -651,8 +654,9 @@ export function CircularView(props) {
651
654
  if (radius < 150) radius = 150;
652
655
  const widthToUse = Math.max(Number(width) || 300);
653
656
  const heightToUse = Math.max(Number(height) || 300);
657
+ const proteinUnits = showAminoAcidUnitAsCodon ? "codons" : "AAs";
654
658
  const bpTitle = isProtein
655
- ? `${Math.floor(sequenceLength / 3)} AAs`
659
+ ? `${Math.floor(sequenceLength / 3)} ${proteinUnits}`
656
660
  : `${sequenceLength} bps`;
657
661
  const nameEl = (
658
662
  <div
@@ -65,6 +65,13 @@
65
65
  fill: #f5f8fa !important;
66
66
  }
67
67
 
68
+ .bp3-dark .rowViewTextContainer *::selection {
69
+ fill: #f5f8fa !important;
70
+ }
71
+ .bp3-dark .veRowViewAxis *::selection {
72
+ fill: #f5f8fa !important;
73
+ }
74
+
68
75
  /* .bp3-dark .veRowViewAxis text {
69
76
  stroke: #f5f8fa !important;
70
77
  } */
@@ -361,6 +361,7 @@ export class Editor extends React.Component {
361
361
  hoveredId,
362
362
  isFullscreen,
363
363
  maxInsertSize,
364
+ showAminoAcidUnitAsCodon,
364
365
  maxAnnotationsToDisplay,
365
366
  minHeight = 400,
366
367
  onlyShowLabelsThatDoNotFit = true,
@@ -594,6 +595,7 @@ export class Editor extends React.Component {
594
595
  {...panelPropsToSpread}
595
596
  editorName={editorName}
596
597
  maxInsertSize={maxInsertSize}
598
+ showAminoAcidUnitAsCodon={showAminoAcidUnitAsCodon}
597
599
  isProtein={sequenceData.isProtein}
598
600
  onlyShowLabelsThatDoNotFit={onlyShowLabelsThatDoNotFit}
599
601
  tabHeight={tabHeight}
@@ -961,6 +963,7 @@ export class Editor extends React.Component {
961
963
  )
962
964
  }
963
965
  editorName={editorName}
966
+ showAminoAcidUnitAsCodon={showAminoAcidUnitAsCodon}
964
967
  {...StatusBarProps}
965
968
  />
966
969
  )}
@@ -57,5 +57,6 @@ export const userDefinedHandlersAndOpts = [
57
57
  "onCreateNewFromSubsequence",
58
58
  "onPreviewModeFullscreenClose",
59
59
  "onPaste",
60
- "menuFilter"
60
+ "menuFilter",
61
+ "showAminoAcidUnitAsCodon"
61
62
  ];
@@ -269,9 +269,8 @@ export class FindBar extends React.Component {
269
269
  minWidth: 300
270
270
  }
271
271
  : {
272
- position: "fixed",
273
- top: 120,
274
- right: 25,
272
+ position: "relative",
273
+ top: 100,
275
274
  padding: 10,
276
275
  display: "flex",
277
276
  alignItems: "start",
@@ -1,13 +1,19 @@
1
1
  import React from "react";
2
2
 
3
- export function SequenceName({ sequenceName, sequenceLength, isProtein }) {
3
+ export function SequenceName({
4
+ sequenceName,
5
+ sequenceLength,
6
+ isProtein,
7
+ showAminoAcidUnitAsCodon
8
+ }) {
9
+ const proteinUnits = showAminoAcidUnitAsCodon ? "codons" : "AAs";
4
10
  return (
5
11
  <div key="sequenceNameText" className="sequenceNameText">
6
12
  <span>{sequenceName} </span>
7
13
  <br />
8
14
  <span>
9
15
  {isProtein
10
- ? `${Math.floor(sequenceLength / 3)} AAs`
16
+ ? `${Math.floor(sequenceLength / 3)} ${proteinUnits}`
11
17
  : `${sequenceLength} bps`}
12
18
  </span>
13
19
  </div>
@@ -28,6 +28,7 @@ import {
28
28
  editorDragStopped
29
29
  } from "../withEditorInteractions/clickAndDragUtils";
30
30
  import { store } from "@risingstack/react-easy-state";
31
+ import { BarPlot, AminoAcidCirclePlot } from "../BarPlot";
31
32
 
32
33
  const defaultMarginWidth = 10;
33
34
 
@@ -173,6 +174,7 @@ class _LinearView extends React.Component {
173
174
  annotationVisibilityOverrides,
174
175
  isProtein,
175
176
  noWarnings,
177
+ showAminoAcidUnitAsCodon,
176
178
  ...rest
177
179
  } = this.props;
178
180
 
@@ -291,6 +293,7 @@ class _LinearView extends React.Component {
291
293
  <SequenceName
292
294
  {...{
293
295
  isProtein,
296
+ showAminoAcidUnitAsCodon,
294
297
  sequenceName,
295
298
  sequenceLength: sequenceData.sequence
296
299
  ? sequenceData.sequence.length
@@ -302,6 +305,24 @@ class _LinearView extends React.Component {
302
305
  <VeTopRightContainer>{this.paredDownMessages}</VeTopRightContainer>
303
306
  )}
304
307
  <PinchHelperToUse {...(linearZoomEnabled && pinchHandler)}>
308
+ {sequenceData.isProtein && sequenceData.name === "Consensus" && (
309
+ <>
310
+ {annotationVisibilityOverrides.conservation && (
311
+ <BarPlot
312
+ className="ve-linear-view-conservation-plot"
313
+ data={sequenceData?.aminoAcidProperties?.frequencies}
314
+ width={width}
315
+ />
316
+ )}
317
+ {annotationVisibilityOverrides.properties && (
318
+ <AminoAcidCirclePlot
319
+ className="ve-linear-view-property-analysis-plot"
320
+ data={sequenceData?.aminoAcidProperties?.propertyAnalysis}
321
+ width={width}
322
+ />
323
+ )}
324
+ </>
325
+ )}
305
326
  <RowItem
306
327
  {...{
307
328
  ...rest,
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Calculates detailed amino acid frequency, including counts and percentages for
3
+ * all 20 standard amino acids.
4
+ *
5
+ * @param {string} sequence The protein sequence.
6
+ * @returns {{
7
+ * totalCount: number,
8
+ * frequencies: Object<string, {count: number, percentage: number}>,
9
+ * nonStandard: Object<string, number>
10
+ * }} A comprehensive analysis object.
11
+ */
12
+ export function calculateAminoAcidFrequency(sequence, isProtein) {
13
+ // 1. Validate input
14
+ if (!sequence || typeof sequence !== "string") {
15
+ console.warn("Invalid or empty amino acid sequence provided.");
16
+ }
17
+
18
+ const standards = isProtein
19
+ ? "ACDEFGHIKLMNPQRSTVWY-".split("")
20
+ : "ATCG-".split("");
21
+ const frequencies = {};
22
+ standards.forEach(a => {
23
+ frequencies[a] = { count: 0, percentage: 0 };
24
+ });
25
+
26
+ const nonStandard = {}; // For gaps '-', 'X', 'B', 'Z', etc.
27
+ let totalCount = 0;
28
+
29
+ // 2. Iterate and count
30
+ for (const char of sequence.toUpperCase()) {
31
+ if (frequencies[char]) {
32
+ frequencies[char].count++;
33
+ totalCount++;
34
+ } else {
35
+ // It's a non-standard character (like a gap or 'X')
36
+ nonStandard[char] = (nonStandard[char] || 0) + 1;
37
+ }
38
+ }
39
+
40
+ // 3. Calculate percentages
41
+ if (totalCount > 0) {
42
+ for (const a of standards) {
43
+ frequencies[a].percentage = (frequencies[a].count / totalCount) * 100;
44
+ }
45
+ }
46
+
47
+ return {
48
+ totalStandards: totalCount, // Total count of elements
49
+ totalLength: sequence.length, // Total length including non-standard
50
+ frequencies,
51
+ nonStandard
52
+ };
53
+ }
54
+
55
+ export const aminoAcidShortNames = {
56
+ A: "Ala",
57
+ C: "Cys",
58
+ D: "Asp",
59
+ E: "Glu",
60
+ F: "Phe",
61
+ G: "Gly",
62
+ H: "His",
63
+ I: "Ile",
64
+ K: "Lys",
65
+ L: "Leu",
66
+ M: "Met",
67
+ N: "Asn",
68
+ P: "Pro",
69
+ Q: "Gln",
70
+ R: "Arg",
71
+ S: "Ser",
72
+ T: "Thr",
73
+ V: "Val",
74
+ W: "Trp",
75
+ Y: "Tyr",
76
+ "-": "Gaps"
77
+ };