Skip to content

Commit

Permalink
wip: sequence display ok. todo: fragment display
Browse files Browse the repository at this point in the history
  • Loading branch information
gaelcartier committed Jun 4, 2024
1 parent 6681b33 commit 04fe712
Show file tree
Hide file tree
Showing 12 changed files with 465 additions and 50 deletions.
17 changes: 17 additions & 0 deletions demo/Fragmentation.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { SVGMassFragmentation } from '../src/components/SVGMassFragmentation';

export default function Fragmentation() {
return (
<>
<h1>Fragmentation</h1>
<div
style={{
border: '1px solid red',
overflow: 'clip',
}}
>
<SVGMassFragmentation />
</div>
</>
);
}
14 changes: 0 additions & 14 deletions demo/Fragmentation.tsx

This file was deleted.

7 changes: 7 additions & 0 deletions demo/main.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from 'react';
import { createRoot } from 'react-dom/client';

import Fragmentation from './Fragmentation';

const root = createRoot(document.getElementById('root'));
root.render(<Fragmentation />);
11 changes: 0 additions & 11 deletions demo/main.tsx

This file was deleted.

4 changes: 2 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
Expand All @@ -7,6 +7,6 @@
</head>
<body>
<div id="root"></div>
<script type="module" src="/demo/main.tsx"></script>
<script type="module" src="/demo/main.jsx"></script>
</body>
</html>
209 changes: 209 additions & 0 deletions src/appendResidues.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import { groupsObject } from 'chemical-groups';
import * as Nucleotide from 'nucleotide';
import * as Peptide from 'peptide';

const ALTERNATIVES = ['', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹'];
const SYMBOLS = ['Θ', 'Δ', 'Λ', 'Φ', 'Ω', 'Γ', 'Χ'];

let currentSymbol = 0;

/**
* Code that allows to split a sequence of amino acids or nucleotides natural or non natural
* @param {string} [sequence]
* @param {object} [options={}]
* @param {string} [options.kind] - peptide, rna, ds-dna or dna. Default if contains U: rna, otherwise ds-dna
* @param {string} [options.fivePrime=monophosphate] - alcohol, monophosphate, diphosphate, triphosphate
* @param {string} [options.circular=false]
*/

export function appendResidues(data, sequence, options = {}) {
const { kind = 'peptide' } = options;

currentSymbol = 0;
// we normalize the sequence to 3 letter codes

if (kind === 'peptide') {
sequence = Peptide.sequenceToMF(sequence);
} else {
sequence = Nucleotide.sequenceToMF(sequence, options);
}

const result = {
begin: '',
end: '',
residues: [],
};

const STATE_BEGIN = 0;
const STATE_MIDDLE = 1;
const STATE_END = 2;

let parenthesisLevel = 0;
let state = STATE_BEGIN; // as long as we don't have an uppercase followed by 2 lowercases
for (let i = 0; i < sequence.length; i++) {
let currentChar = sequence.charAt(i);
let nextChar = i < sequence.length - 1 ? sequence.charAt(i + 1) : '';
let nextNextChar = i < sequence.length - 2 ? sequence.charAt(i + 2) : '';

if (
state === STATE_BEGIN &&
currentChar.match(/[A-Z]/) &&
nextChar.match(/[a-z]/) &&
nextNextChar.match(/[a-z]/) &&
parenthesisLevel === 0
) {
state = STATE_MIDDLE;
}

if (
state === STATE_MIDDLE &&
!sequence.substring(i).match(/[A-Z][a-z][a-z]/) &&
!currentChar.match(/[a-z]/) &&
parenthesisLevel === 0
) {
state = STATE_END;
} else if (
currentChar.match(/[A-Z]/) &&
nextChar.match(/[a-z]/) &&
nextNextChar.match(/[a-z]/) &&
parenthesisLevel === 0
) {
result.residues.push('');
}

switch (state) {
case STATE_BEGIN:
result.begin = result.begin + currentChar;
break;
case STATE_MIDDLE:
result.residues[result.residues.length - 1] =
result.residues[result.residues.length - 1] + currentChar;
break;
case STATE_END:
result.end = result.end + currentChar;
break;
default:
}

if (currentChar === '(') {
parenthesisLevel++;
} else if (currentChar === ')') {
parenthesisLevel--;
}
}

// we process all the residues
let alternatives = {};
let replacements = {};
for (let i = 0; i < result.residues.length; i++) {
let label = result.residues[i];
let residue = {
value: label,
results: {
begin: [],
end: [],
},
};
residue.fromBegin = i + 1;
residue.fromEnd = result.residues.length - i;
residue.kind = 'residue';
if (label.includes('(')) {
getModifiedReplacement(label, residue, alternatives, replacements);
} else if (groupsObject[label] && groupsObject[label].oneLetter) {
residue.label = groupsObject[label].oneLetter;
} else {
getUnknownReplacement(label, residue, replacements);
}
result.residues[i] = residue;
}
result.begin = removeStartEndParenthesis(result.begin);
result.end = removeStartEndParenthesis(result.end);
if (result.begin.length > 2) {
let label = options.kind === 'peptide' ? 'Nter' : "5'";
replacements[result.begin] = {
label,
};
result.begin = label;
}
if (result.end.length > 2) {
let label = options.kind === 'peptide' ? 'Cter' : "3'";
replacements[result.end] = {
label,
};
result.end = label;
}

result.begin = { label: result.begin, kind: 'begin' };
result.end = { label: result.end, kind: 'end' };
result.alternatives = alternatives;
result.replacements = replacements;

result.all = [result.begin].concat(result.residues, [result.end]);

result.all.forEach((entry) => {
entry.info = {
nbOver: 0,
nbUnder: 0,
};
});

data.residues = result;
}

function getUnknownReplacement(unknownResidue, residue, replacements) {
if (!replacements[unknownResidue]) {
replacements[unknownResidue] = {
label: SYMBOLS[currentSymbol] || '?',
id: unknownResidue,
};
}
currentSymbol++;
residue.replaced = true;
residue.label = replacements[unknownResidue].label;
}

function getModifiedReplacement(
modifiedResidue,
residue,
alternatives,
replacements,
) {
if (!replacements[modifiedResidue]) {
let position = modifiedResidue.indexOf('(');
let residueCode = modifiedResidue.substring(0, position);
let modification = removeStartEndParenthesis(
modifiedResidue.substring(position),
);

if (
groupsObject[residueCode] &&
groupsObject[residueCode].alternativeOneLetter
) {
let alternativeOneLetter = groupsObject[residueCode].alternativeOneLetter;

if (!alternatives[alternativeOneLetter]) {
alternatives[alternativeOneLetter] = { count: 1 };
} else {
alternatives[alternativeOneLetter].count++;
}
replacements[modifiedResidue] = {
label:
ALTERNATIVES[alternatives[alternativeOneLetter].count - 1] +
alternativeOneLetter,
residue: residueCode,
modification,
};
} else {
getUnknownReplacement(modifiedResidue, residue, replacements);
}
}
residue.replaced = true;
residue.label = replacements[modifiedResidue].label;
}

function removeStartEndParenthesis(mf) {
if (mf[0] === '(' && mf[mf.length - 1] === ')') {
return mf.substring(1, mf.length - 1);
}
return mf;
}
120 changes: 120 additions & 0 deletions src/appendResults.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
export function appendResults(data, analysisResult, options = {}) {
const numberResidues = data.residues.residues.length;
const { merge = {}, filter = {} } = options;

let results = JSON.parse(JSON.stringify(analysisResult));
results = results.filter((result) => !result.type.match(/^-B[0-9]$/));
// we calculate all the lines based on the results
for (let result of results) {
let parts = result.type.split(/:|(?=[a-z])/); // we may have ':' but not mandatory
if (parts.length === 2) {
result.internal = true;
if (parts[1].match(/^[abcd][1-9]/)) {
[parts[0], parts[1]] = [parts[1], parts[0]];
}
result.to = getNumber(parts[0]) - 1;
result.from = numberResidues - getNumber(parts[1]);
} else {
if (parts[0].match(/^[abcd][1-9]/)) {
result.fromBegin = true;
result.position = getNumber(parts[0]) - 1;
}
if (parts[0].match(/^[wxyz][1-9]/)) {
result.fromEnd = true;
result.position = numberResidues - 1 - getNumber(parts[0]);
}
}

if (result.fromEnd) result.color = 'red';
if (result.fromBegin) result.color = 'blue';
if (result.internal) {
switch (result.type.substring(0, 1)) {
case 'a':
result.color = 'green';
break;
case 'b':
result.color = 'orange';
break;
case 'c':
result.color = 'cyan';
break;
default:
result.color = 'green';
}
}
}

if (merge.charge) {
const unique = {};
for (let result of results) {
if (!unique[result.type]) {
unique[result.type] = [];
}
unique[result.type].push(result);
}
results = [];
for (let key in unique) {
let current = unique[key][0];
current.similarity = unique[key].reduce(
(previous, item) => previous + item.similarity,
0,
);
current.similarity = current.similarity / unique[key].length;
results.push(current);
current.charge = '';
}
}

for (let result of results) {
if (result.similarity > 0.95) {
result.textColor = 'black';
} else if (result.similarity > 0.9) {
result.textColor = '#333';
} else if (result.similariy > 0.8) {
result.textColor = '#666';
} else {
result.textColor = '#999';
}
}

results = filterResults(results, filter);

// sort by residue length
results.sort((a, b) => a.length - b.length);
data.results = results;
}

function getNumber(text) {
return Number(text.replace(/^.([0-9]+).*$/, '$1'));

Check warning on line 88 in src/appendResults.js

View workflow job for this annotation

GitHub Actions / nodejs / lint-eslint

Capture group '([0-9]+)' should be converted to a named or non-capturing group
}

function filterResults(results, filter) {
if (!filter) return;
let {
minRelativeQuantity = 0,
minSimilarity = 0,
minQuantity = 0,
showInternals = true,
} = filter;

if (minRelativeQuantity) {
minQuantity =
Math.max(...results.map((entry) => entry.quantity)) * minRelativeQuantity;
}

if (minSimilarity) {
results = results.filter(
(result) => !result.similarity || result.similarity >= minSimilarity,
);
}
if (minQuantity) {
results = results.filter(
(result) => !result.quantity || result.quantity >= minQuantity,
);
}

if (!showInternals) {
results = results.filter((result) => !result.internal);
}
return results;
}
Loading

0 comments on commit 04fe712

Please sign in to comment.