@teselagen/ove 0.8.2 → 0.8.4

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 (53) 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 +4214 -7859
  12. package/index.es.js +2166 -5811
  13. package/index.umd.js +3745 -7390
  14. package/ove.css +100 -19
  15. package/package.json +2 -2
  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 +10 -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/SelectDialog.js +5 -2
  47. package/src/style.css +2 -2
  48. package/src/utils/editorUtils.js +5 -3
  49. package/src/utils/getAlignedAminoAcidSequenceProps.js +379 -0
  50. package/src/withEditorInteractions/createSequenceInputPopup.js +19 -5
  51. package/src/withEditorInteractions/index.js +9 -3
  52. package/utils/editorUtils.d.ts +2 -1
  53. package/utils/getAlignedAminoAcidSequenceProps.d.ts +49 -0
@@ -0,0 +1,236 @@
1
+ import React, { useCallback, useEffect, useMemo } from "react";
2
+ import "./style.css";
3
+ import {
4
+ calculateAminoAcidFrequency,
5
+ aminoAcidShortNames
6
+ } from "./calculateAminoAcidFrequency";
7
+ import { Button } from "@blueprintjs/core";
8
+
9
+ export default ({ properties, setProperties, style }) => {
10
+ const sidebarRef = React.useRef(null);
11
+ const [mismatchesCount, setMismatchesCount] = React.useState(0);
12
+ const [mismatchesInRange, setMismatchesInRange] = React.useState(0);
13
+
14
+ const { track, isOpen, selection, isPairwise } = properties;
15
+
16
+ const getSequenceInRegion = useCallback(() => {
17
+ const seq = track?.alignmentData?.sequence ?? "";
18
+ if (!selection || selection.start === -1 || selection.end === -1) {
19
+ return seq;
20
+ }
21
+
22
+ const start = Math.max(0, selection.start);
23
+ const end = Math.min(seq.length - 1, selection.end);
24
+ if (start > end) return "";
25
+ return seq.slice(start, end + 1);
26
+ }, [track, selection]);
27
+
28
+ const mismatchKey = isPairwise ? "additionalSelectionLayers" : "mismatches";
29
+
30
+ const trackMismatches = useMemo(() => {
31
+ const tr = track?.[mismatchKey];
32
+ if (!Array.isArray(tr)) return [];
33
+ return isPairwise ? tr.filter(m => m?.color === "red") : tr;
34
+ }, [track, mismatchKey, isPairwise]);
35
+
36
+ useEffect(() => {
37
+ if (!isOpen || sidebarRef.current === null || !track) {
38
+ return;
39
+ }
40
+
41
+ sidebarRef.current.focus();
42
+ let mismatchCount = 0;
43
+
44
+ trackMismatches?.forEach(tm => {
45
+ if (tm === null || tm.start === null || tm.end === null) {
46
+ return;
47
+ }
48
+
49
+ const overlapStart = tm.start;
50
+ const overlapEnd = tm.end;
51
+ if (overlapEnd >= overlapStart) {
52
+ mismatchCount += overlapEnd - overlapStart + 1;
53
+ }
54
+ });
55
+
56
+ setMismatchesCount(mismatchCount);
57
+ setMismatchesInRange(mismatchCount);
58
+
59
+ if (selection && selection.start > -1 && selection.end > -1) {
60
+ let count = 0;
61
+
62
+ trackMismatches?.forEach(tm => {
63
+ if (tm === null || tm.start === null || tm.end === null) {
64
+ return;
65
+ }
66
+
67
+ const overlapStart = Math.max(tm.start, selection.start);
68
+ const overlapEnd = Math.min(tm.end, selection.end);
69
+ if (overlapEnd >= overlapStart) {
70
+ count += overlapEnd - overlapStart + 1;
71
+ }
72
+ });
73
+ setMismatchesInRange(count);
74
+ }
75
+ }, [isOpen, track, selection, trackMismatches]);
76
+
77
+ const aminoFreq = useMemo(() => {
78
+ const seq = getSequenceInRegion();
79
+ return calculateAminoAcidFrequency(
80
+ seq,
81
+ track?.sequenceData?.isProtein ?? false
82
+ );
83
+ }, [getSequenceInRegion, track]);
84
+
85
+ if (!isOpen) {
86
+ return null;
87
+ }
88
+ let trackInner;
89
+
90
+ if (track) {
91
+ const {
92
+ name,
93
+ isProtein,
94
+ proteinSize,
95
+ size,
96
+ molecularWeight,
97
+ isoPoint,
98
+ extinctionCoefficient
99
+ } = track.sequenceData;
100
+
101
+ const frequencyEntries = Object.entries(aminoFreq.frequencies);
102
+ trackInner = (
103
+ <>
104
+ <div
105
+ style={{
106
+ display: "flex",
107
+ padding: 4,
108
+ paddingTop: 11,
109
+ paddingBottom: 11,
110
+ width: "100%"
111
+ }}
112
+ ></div>
113
+ <h5>Track Properties</h5>
114
+
115
+ <div className="bp3-tab-panel">
116
+ <RowItem item={name} title="Name" />
117
+ <RowItem item={isProtein ? proteinSize : size} title="Length" />
118
+ <RowItem
119
+ item={molecularWeight?.toFixed(2)}
120
+ title="Molecular Weight"
121
+ units={isProtein ? "Da" : "g/mol"}
122
+ />
123
+ {name !== "Consensus" && isProtein && (
124
+ <>
125
+ <RowItem item={isoPoint} title="Isoelectric Point" />
126
+ <RowItem
127
+ item={extinctionCoefficient}
128
+ title="Extinction Coefficient"
129
+ />
130
+ </>
131
+ )}
132
+ <RowItem
133
+ item={`${mismatchesInRange}/${mismatchesCount}`}
134
+ title="Mismatches"
135
+ />
136
+ <RowItem
137
+ item={
138
+ selection && selection.start > -1 ? (
139
+ <span>
140
+ {selection.start + 1} - {selection.end + 1}
141
+ </span>
142
+ ) : (
143
+ <span>1 - {isProtein ? proteinSize : size}</span>
144
+ )
145
+ }
146
+ title="Region"
147
+ />
148
+ </div>
149
+ <h5>{isProtein ? "Amino Acid" : "Base Pair"} Frequencies</h5>
150
+ <div className="sidebar-table">
151
+ <div className="sidebar-row">
152
+ <div className="sidebar-cell">Amino Acid</div>
153
+ <div className="sidebar-cell">Count</div>
154
+ <div className="sidebar-cell">Percentage</div>
155
+ </div>
156
+ {frequencyEntries.map(([aa, data], idx) => {
157
+ return (
158
+ <div className={`sidebar-row property-amino-acid-${idx}`}>
159
+ <div className="sidebar-cell">
160
+ {aa} {isProtein ? `(${aminoAcidShortNames[aa]})` : ""}
161
+ </div>
162
+ <div className="sidebar-cell">{data.count}</div>
163
+ <div className="sidebar-cell">
164
+ {data.percentage.toFixed(1)}%
165
+ </div>
166
+ </div>
167
+ );
168
+ })}
169
+ </div>
170
+ </>
171
+ );
172
+ } else {
173
+ trackInner = (
174
+ <div
175
+ style={{
176
+ marginTop: 20,
177
+ fontStyle: "italic",
178
+ fontSize: 16
179
+ }}
180
+ >
181
+ Click on a track to view its properties
182
+ </div>
183
+ );
184
+ }
185
+
186
+ return (
187
+ <div
188
+ ref={sidebarRef}
189
+ style={{
190
+ width: isOpen ? 350 : 0,
191
+ minWidth: isOpen ? 350 : 0,
192
+ maxWidth: isOpen ? 350 : 0,
193
+ paddingLeft: 20,
194
+ paddingRight: 20,
195
+ ...style
196
+ }}
197
+ className="ove-sidebar-container"
198
+ tabIndex={0}
199
+ onKeyDown={e => {
200
+ if (e.key === "Escape") {
201
+ setProperties({ isOpen: false });
202
+ }
203
+ }}
204
+ >
205
+ <Button
206
+ style={{
207
+ position: "absolute",
208
+ top: 5,
209
+ right: 10,
210
+ zIndex: 1,
211
+ cursor: "pointer"
212
+ }}
213
+ minimal
214
+ intent="primary"
215
+ data-tip="Hide Track Properties"
216
+ icon="cross"
217
+ onClick={() => setProperties({ isOpen: false })}
218
+ ></Button>
219
+ {trackInner}
220
+ </div>
221
+ );
222
+ };
223
+
224
+ function RowItem({ item, title, units }) {
225
+ if (!item) return;
226
+
227
+ const propertyClass = title.split(" ").join("-").toLowerCase();
228
+ return (
229
+ <div className={`ve-flex-row property-${propertyClass}`}>
230
+ <div className="ve-column-left">{title}</div>
231
+ <div className="ve-column-right">
232
+ {item} {units ?? ""}
233
+ </div>
234
+ </div>
235
+ );
236
+ }
@@ -0,0 +1,39 @@
1
+ .ove-sidebar-container {
2
+ display: flex;
3
+ flex-direction: column;
4
+ box-shadow: 0 0px 1px var(--reversed);
5
+ overflow-y: scroll;
6
+ }
7
+
8
+ .sidebar-table {
9
+ display: grid;
10
+ /* Define 3 columns with equal width */
11
+ grid-template-columns: repeat(3, 1fr);
12
+ gap: 1px; /* Optional: adds a small gap between cells */
13
+ border: 1px solid #f1f1f1;
14
+ margin: 10px;
15
+ border-radius: 4px;
16
+ }
17
+
18
+ .sidebar-row {
19
+ display: contents; /* Makes children participate in the grid layout of the parent */
20
+ }
21
+
22
+ .sidebar-cell {
23
+ padding: 5px;
24
+ border: 1px solid #f1f1f1;
25
+ text-align: center;
26
+ /* Flexbox can be used within cells for content alignment if needed */
27
+ display: flex;
28
+ align-items: center;
29
+ justify-content: center;
30
+ }
31
+
32
+ h5 {
33
+ margin: 0;
34
+ font-size: 15px;
35
+ font-weight: bold;
36
+ text-align: center;
37
+ padding: 5px 0;
38
+ border-bottom: 1px solid #f1f1f1;
39
+ }
@@ -16,7 +16,8 @@ function Caret({
16
16
  onRightClick,
17
17
  style,
18
18
  selectionMessage,
19
- className = ""
19
+ className = "",
20
+ showAminoAcidUnitAsCodon
20
21
  }) {
21
22
  if (
22
23
  (row.start <= caretPosition && row.end + 1 >= caretPosition) ||
@@ -35,7 +36,12 @@ function Caret({
35
36
  }
36
37
  title={
37
38
  selectionMessage ||
38
- getSelectionMessage({ caretPosition, isProtein, sequenceLength })
39
+ getSelectionMessage({
40
+ caretPosition,
41
+ isProtein,
42
+ sequenceLength,
43
+ showAminoAcidUnitAsCodon
44
+ })
39
45
  }
40
46
  className={classnames(
41
47
  {
@@ -99,7 +99,7 @@ function Labels(props) {
99
99
  annotation = annotationRange;
100
100
  }
101
101
  const annotationLength =
102
- getTextLengthWithCollapseSpace(
102
+ getTextLengthWithCollapseSpace(
103
103
  annotation.name ||
104
104
  (annotation.restrictionEnzyme && annotation.restrictionEnzyme.name) ||
105
105
  ""
@@ -21,6 +21,7 @@ function SelectionLayer(props) {
21
21
  regions,
22
22
  leftMargin = 0,
23
23
  isProtein,
24
+ showAminoAcidUnitAsCodon,
24
25
  getGaps,
25
26
  hideTitle: topLevelHideTitle,
26
27
  customTitle: topLevelCustomTitle,
@@ -62,7 +63,8 @@ function SelectionLayer(props) {
62
63
  selectionLayer,
63
64
  customTitle: customTitle || topLevelCustomTitle,
64
65
  sequenceLength,
65
- isProtein
66
+ isProtein,
67
+ showAminoAcidUnitAsCodon
66
68
  });
67
69
  const onSelectionContextMenu = function (event) {
68
70
  selectionLayerRightClicked &&
@@ -107,6 +109,7 @@ function SelectionLayer(props) {
107
109
  key={key + "caret1"}
108
110
  {...{
109
111
  isProtein,
112
+ showAminoAcidUnitAsCodon,
110
113
  leftMargin,
111
114
  onClick: _onClick || preventDefaultStopPropagation,
112
115
  onRightClick: onSelectionContextMenu,
@@ -130,6 +133,7 @@ function SelectionLayer(props) {
130
133
  key={key + "caret2"}
131
134
  {...{
132
135
  isProtein,
136
+ showAminoAcidUnitAsCodon,
133
137
  leftMargin,
134
138
  onClick: _onClick || preventDefaultStopPropagation,
135
139
  onRightClick: onSelectionContextMenu,
@@ -3,7 +3,27 @@ import { times, map } from "lodash-es";
3
3
  import { view } from "@risingstack/react-easy-state";
4
4
  import { getVisibleStartEnd } from "../utils/getVisibleStartEnd";
5
5
  import { fudge2, realCharWidth } from "./constants";
6
- import dnaToColor, { getDnaColor } from "../constants/dnaToColor";
6
+ import dnaToColor, {
7
+ getDnaColor,
8
+ getSerineThreonineColor,
9
+ getNegativeColor,
10
+ getPositiveColor,
11
+ getChargedColor,
12
+ getHydrophobicity,
13
+ getPolarColor,
14
+ getAliphaticColor,
15
+ getAromaticColor,
16
+ getColorScheme,
17
+ serineThreonineToColor,
18
+ hydrophobicityColor,
19
+ polarColors,
20
+ negativeColors,
21
+ positiveColors,
22
+ chargedColors,
23
+ aliphaticColors,
24
+ aromaticColors,
25
+ colorScheme
26
+ } from "../constants/dnaToColor";
7
27
  import { hoveredAnnEasyStore } from "../helperComponents/withHover";
8
28
  import { getOverlapsOfPotentiallyCircularRanges } from "@teselagen/range-utils";
9
29
  import { partOverhangs } from "./partOverhangs";
@@ -13,6 +33,28 @@ import { isSafari } from "@teselagen/ui";
13
33
  const getChunk = (sequence, chunkSize, chunkNumber) =>
14
34
  sequence.slice(chunkSize * chunkNumber, chunkSize * (chunkNumber + 1));
15
35
 
36
+ function renderColoredLayer(props, fudge, width, toColor) {
37
+ return (
38
+ <svg
39
+ style={{
40
+ left: props.startOffset * props.charWidth,
41
+ height: props.height,
42
+ width,
43
+ position: "absolute"
44
+ }}
45
+ className="rowViewTextContainer"
46
+ height={Math.max(0, Number(props.height))}
47
+ >
48
+ <ColoredSequence
49
+ {...props}
50
+ fudge={fudge}
51
+ totalWidth={width}
52
+ toColor={toColor}
53
+ />
54
+ </svg>
55
+ );
56
+ }
57
+
16
58
  class Sequence extends React.Component {
17
59
  render() {
18
60
  const {
@@ -29,6 +71,15 @@ class Sequence extends React.Component {
29
71
  chunkSize = 100,
30
72
  scrollData,
31
73
  showDnaColors,
74
+ showSerineThreonine,
75
+ showHydrophobicity,
76
+ showPolar,
77
+ showNegative,
78
+ showPositive,
79
+ showCharged,
80
+ showAliphatic,
81
+ showAromatic,
82
+ showColorScheme,
32
83
  fivePrimeThreePrimeHints,
33
84
  alignmentData,
34
85
  sequenceLength,
@@ -79,6 +130,18 @@ class Sequence extends React.Component {
79
130
  }
80
131
  });
81
132
 
133
+ const colorLayers = [
134
+ { show: showSerineThreonine, toColor: "serineThreonine" },
135
+ { show: showHydrophobicity, toColor: "hydrophobicity" },
136
+ { show: showPolar, toColor: "polar" },
137
+ { show: showNegative, toColor: "negative" },
138
+ { show: showPositive, toColor: "positive" },
139
+ { show: showCharged, toColor: "charged" },
140
+ { show: showAliphatic, toColor: "aliphatic" },
141
+ { show: showAromatic, toColor: "aromatic" },
142
+ { show: showColorScheme, toColor: "colorScheme" }
143
+ ];
144
+
82
145
  const style = {
83
146
  position: "relative",
84
147
  height,
@@ -173,6 +236,10 @@ class Sequence extends React.Component {
173
236
  5'
174
237
  </div>
175
238
  )}
239
+ {colorLayers.map(
240
+ ({ show, toColor }) =>
241
+ show && renderColoredLayer(this.props, fudge, width, toColor)
242
+ )}
176
243
  {!hideBps && (
177
244
  <svg
178
245
  style={{
@@ -189,7 +256,8 @@ class Sequence extends React.Component {
189
256
  {...{
190
257
  ...this.props,
191
258
  fudge,
192
- totalWidth: width
259
+ totalWidth: width,
260
+ toColor: "dnaColor"
193
261
  }}
194
262
  />
195
263
  )}
@@ -226,20 +294,46 @@ class ColoredSequence extends React.Component {
226
294
  alignmentData,
227
295
  getGaps,
228
296
  fudge,
229
- totalWidth
297
+ totalWidth,
298
+ toColor
230
299
  } = this.props;
231
300
  if (alignmentData) {
232
301
  sequence = sequence.replace(/^-+/g, "").replace(/-+$/g, "");
233
302
  }
303
+ const colorMap = {
304
+ dnaColor: dnaToColor,
305
+ serineThreonine: serineThreonineToColor,
306
+ hydrophobicity: hydrophobicityColor,
307
+ polar: polarColors,
308
+ negative: negativeColors,
309
+ positive: positiveColors,
310
+ charged: chargedColors,
311
+ aliphatic: aliphaticColors,
312
+ aromatic: aromaticColors,
313
+ colorScheme: colorScheme
314
+ };
315
+
316
+ const colorMethods = {
317
+ dnaColor: getDnaColor,
318
+ serineThreonine: getSerineThreonineColor,
319
+ hydrophobicity: getHydrophobicity,
320
+ polar: getPolarColor,
321
+ negative: getNegativeColor,
322
+ positive: getPositiveColor,
323
+ charged: getChargedColor,
324
+ aliphatic: getAliphaticColor,
325
+ aromatic: getAromaticColor,
326
+ colorScheme: getColorScheme
327
+ };
234
328
  //we use big paths instead of many individual rects to improve performance
235
- const colorPaths = Object.values(dnaToColor).reduce((acc, color) => {
329
+ const colorPaths = Object.values(colorMap[toColor]).reduce((acc, color) => {
236
330
  acc[color] = "";
237
331
  return acc;
238
332
  }, {});
239
333
  const gapsBefore = getGaps ? getGaps({ start: 0, end: 0 }).gapsBefore : 0;
240
334
  sequence.split("").forEach((char, i) => {
241
335
  const width = Number(charWidth);
242
- const color = getDnaColor(char, isReverse);
336
+ const color = colorMethods[toColor](char, isReverse);
243
337
  const x = (i + gapsBefore) * charWidth;
244
338
  const y = 0;
245
339
  colorPaths[color] =
@@ -26,6 +26,7 @@ class Translation extends React.Component {
26
26
  annotationRange,
27
27
  height,
28
28
  showAminoAcidNumbers,
29
+ showAminoAcidUnitAsCodon,
29
30
  charWidth,
30
31
  aminoAcidNumbersHeight,
31
32
  onClick,
@@ -164,10 +165,10 @@ class Translation extends React.Component {
164
165
  aminoAcidSliver.aminoAcidIndex + 1
165
166
  } -- Hydrophobicity: ${aminoAcid.hydrophobicity} -- Mass: ${
166
167
  aminoAcid.mass
167
- }\n
168
+ }\n
168
169
  Part of ${annotation.translationType} Translation from BPs ${
169
170
  annotation.start + 1
170
- } to ${annotation.end + 1} (${aminoAcids.length / 3} AAs)`}
171
+ } to ${annotation.end + 1} (${aminoAcids.length / 3} ${showAminoAcidUnitAsCodon ? "codons" : "AAs"})`}
171
172
  showAminoAcidNumbers={showAminoAcidNumbers}
172
173
  aminoAcidIndex={aminoAcidSliver.aminoAcidIndex}
173
174
  onDoubleClick={function (event) {
@@ -7,6 +7,7 @@ function getExtraInnerCompProps(
7
7
  annotationRange,
8
8
  {
9
9
  showAminoAcidNumbers,
10
+ showAminoAcidUnitAsCodon,
10
11
  getGaps,
11
12
  isProtein,
12
13
  colorType,
@@ -20,6 +21,7 @@ function getExtraInnerCompProps(
20
21
 
21
22
  return {
22
23
  showAminoAcidNumbers,
24
+ showAminoAcidUnitAsCodon,
23
25
  getGaps,
24
26
  height: anotationHeightNoSpace,
25
27
  aminoAcidNumbersHeight,