Skip to content

Commit

Permalink
Added primer design feature.
Browse files Browse the repository at this point in the history
  • Loading branch information
Nick Rosenau authored and Nick Rosenau committed Nov 15, 2023
1 parent 13fcc68 commit 74efa1a
Show file tree
Hide file tree
Showing 4 changed files with 342 additions and 2 deletions.
8 changes: 7 additions & 1 deletion demo/lib/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
Input,
Menu,
Sidebar,
Modal
} from "semantic-ui-react";
import seqparse from "seqparse";

Expand All @@ -21,6 +22,7 @@ import { AnnotationProp } from "../../src/elements";
import Header from "./Header";
import file from "./file";


const viewerTypeOptions = [
{ key: "both", text: "Both", value: "both" },
{ key: "circular", text: "Circular", value: "circular" },
Expand Down Expand Up @@ -66,7 +68,7 @@ export default class App extends React.Component<any, AppState> {
{ end: 1885, start: 1165 },
],
viewer: "both",
zoom: 50,
zoom: 50
};
linearRef: React.RefObject<HTMLDivElement> = React.createRef();
circularRef: React.RefObject<HTMLDivElement> = React.createRef();
Expand Down Expand Up @@ -100,6 +102,7 @@ export default class App extends React.Component<any, AppState> {
}
};


render() {
let customChildren = null;
if (this.state.customChildren) {
Expand Down Expand Up @@ -153,6 +156,8 @@ export default class App extends React.Component<any, AppState> {
};
}



return (
<div style={{ height: "100vh" }}>
<Sidebar.Pushable className="sidebar-container">
Expand Down Expand Up @@ -228,6 +233,7 @@ export default class App extends React.Component<any, AppState> {
translations={this.state.translations}
viewer={this.state.viewer as "linear" | "circular"}
zoom={{ linear: this.state.zoom }}
primerDesign={true}
>
{customChildren}
</SeqViz>
Expand Down
263 changes: 263 additions & 0 deletions src/PrimerDesign/PrimerDesign.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
import { useState } from "react";
import React = require("react");
import { Button } from "semantic-ui-react";
import { randomID } from "../sequence";
import { PrimerModal } from "./PrimerModal";

export interface Primer {
seq: any;
temp: number;
GCContent: string;
start: number;
end: number;
rev: boolean
}
export const PrimerDesign = (props) => {

const [openModal, setOpenModal] = useState(false);
const [primers, setPrimers] = useState<Primer[] | null>(null)
const [oldPrimerSelect, setOldPrimerSelect] = useState<string>("")
const [editButton, setEditButton] = useState(false)
const [target, setTarget] = useState<any>(null)

const getPrimerTemp = (sequence: string) => {
let atCount = 0, gcCount = 0;

for (let i = 0; i < sequence.length; i++) {
let nucleotide = sequence[i].toUpperCase();
if (nucleotide === 'A' || nucleotide === 'T') {
atCount++;
} else if (nucleotide === 'G' || nucleotide === 'C') {
gcCount++;
}
}

return 2 * atCount + 4 * gcCount;
}

const getGCContent = (sequence: string) => {
let gcCount = 0;
sequence = sequence.toUpperCase()
const sequenceLength = sequence.length

for(let i=0; i< sequenceLength; i++) {
const nucleotide = sequence[i]
if(nucleotide === 'G' || nucleotide === 'C'){
gcCount++
}
}

return gcCount / sequenceLength * 100;
}

const checkValidSeq = (sequence: string) => {
sequence = sequence.toUpperCase()
return /^[ATCG]*$/i.test(sequence);
}

const reverseComplement = (seq:string) => {
return seq.split('').map(nucleotide => {
switch (nucleotide) {
case 'A': return 'T';
case 'T': return 'A';
case 'C': return 'G';
case 'G': return 'C';
default: return nucleotide;
}
}).reverse().join('').toLowerCase();
};

const getReversePrimer = (shift=0, len=25) => {
const start = props.selection.end
const end = props.selection.end + (len + shift)
let sequence = props.seq.slice(start, end)
sequence = sequence.toUpperCase()

return [reverseComplement(sequence), start, end]
}

const getForwardPrimer = (shift=0, len=25) => {
const start = props.selection.start - (len + shift)
const end = props.selection.start
const forwardPrimer = props.seq.slice(start, end)
return [forwardPrimer, start, end]
}

const shiftPrimers = (temp : number, GCContent: number, remainingBp: string, orientation: string) => {
let count = 0
let len = 25
let seq = 'ATCG'
let start = 0
let end = 0
while((70 <= temp || temp <= 55 || 60 < GCContent || GCContent < 40) && count < remainingBp.length){
if(orientation === 'rev'){
if(len > 18){
const result = getReversePrimer(1, len-1)
seq = result[0]
start = result[1]
end = result[2]
len -= 1
}
else{
const result = getReversePrimer(1)
seq = result[0]
start = result[1]
end = result[2]
}
}
else{
if(len > 18){
const result = getForwardPrimer(1, len-1)
seq = result[0]
start = result[1]
end = result[2]
len -= 1
}
else{
const result = getForwardPrimer(1)
seq = result[0]
start = result[1]
end = result[2]
}
}

temp = getPrimerTemp(seq)
GCContent = getGCContent(seq)
count += 1
}

return [seq, temp, GCContent, start, end]
}

const handlePrimerDesign = (sequence:string) => {

if(sequence !== oldPrimerSelect){
if(primers != null){
removePrimers()
}
setPrimers(null)
const validate = checkValidSeq(sequence)

if(validate){

let forward = getForwardPrimer()
let rev = getReversePrimer()

let GCContentFwd = getGCContent(forward[0])
let GCContentRev = getGCContent(rev[0])

let fwdTemp = getPrimerTemp(forward[0])
let revTemp = getPrimerTemp(rev[0])

let startFwd = 0
let endFwd = 0
let startRev = 0
let endRev = 0

const fwdBackwards = props.seq.slice(0, props.selection.start - 25)
const revOnwards = props.seq.slice(props.selection.end + 25, props.seq.length - 1)


if((70 <= fwdTemp || fwdTemp <= 55 || 60 < GCContentFwd || GCContentFwd < 40 || Math.abs(fwdTemp - revTemp) > 5)){
const result:any = shiftPrimers(fwdTemp, GCContentFwd, fwdBackwards, 'fwd')
forward = result[0]
fwdTemp = result[1]
GCContentFwd = result[2]
startFwd = result[3]
endFwd = result[4]
}
if((70 <= revTemp || revTemp <= 55 || 60 < GCContentRev || GCContentRev < 40 || Math.abs(fwdTemp - revTemp) > 5)){
const result:any = shiftPrimers(revTemp, GCContentRev, revOnwards, 'rev')
rev = result[0]
revTemp = result[1]
GCContentRev = result[2]
startRev = result[3]
endRev = result[4]
}

setOldPrimerSelect(sequence)
setPrimers([{'seq': forward, 'temp': fwdTemp, 'GCContent': Math.round(GCContentFwd).toString() + "%", 'start': startFwd, 'end': endFwd, rev: false},
{'seq': rev, 'temp': revTemp, 'GCContent': Math.round(GCContentRev).toString() + "%", 'start': startRev, 'end': endRev, rev: true}])
setOpenModal(true)
return
}
throw 'Invalid DNA sequence'
}
else{
setOpenModal(true)
}


}

const addPrimers = (primers:Primer[]) => {

let annotations = [...props.annotations]

primers.forEach((primer:Primer) => {
const primerAnnotation = {
id: randomID(),
color: primer.rev ? 'blue' : 'red',
direction: primer.rev ? -1 : 1,
end: primer.end,
name: primer.rev ? 'primer-rev' : 'primer-fwd',
start: primer.start
}
annotations = [...annotations, primerAnnotation]
})

const targetAnnotation = {
id: randomID(),
color: 'green',
direction: 1,
end: props.selection.end,
name: 'target',
start: props.selection.start
}
if(!annotations.find((annotation:any) => annotation.id === target?.id)){
setTarget(targetAnnotation)
annotations = [...annotations, targetAnnotation]
}


props.setAnnotations(annotations)
}

const removePrimers = () => {

const annotations = props.annotations.filter((annotation:any) => !annotation.name.includes('primer') && !annotation.name.includes('target'))
setEditButton(false)
props.setAnnotations(annotations)
}

React.useEffect(() => {
if(props.selection?.start && primers){
const findOne = primers.find((primer:Primer) => primer.start === props.selection?.start && primer.end === props.selection?.end)
if(findOne || (target?.start === props.selection?.start && target?.end === props.selection?.end)){
setEditButton(true)
}
else{
setEditButton(false)
}
}
}, [props.selection])

return(
<div style={{position: 'absolute', top: 14, left: 250, zIndex: 10, width: '200px'}}>
{props.primerSelect !== "" && !editButton && (
<Button color='blue' onClick={() => {
handlePrimerDesign(props.primerSelect);
setOpenModal(true)
}}>Create Primers</Button>
)}
{props.primerSelect !== "" && editButton && (
<Button color='green' onClick={() => {
setOpenModal(true)
}}>Edit Primers</Button>
)}
{primers && (
<PrimerModal open={openModal} closeModal={() => setOpenModal(false)} data={primers} addPrimers={(data:Primer[]) => addPrimers(data)} removePrimers={removePrimers} />
)}
</div>
)
}
63 changes: 63 additions & 0 deletions src/PrimerDesign/PrimerModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useEffect, useState } from "react";
import React = require("react");
import { Button, Modal, SemanticCOLORS } from "semantic-ui-react";

export const PrimerModal = (props) => {
const [buttonRev, setButtonRev] = useState("Add Primers")
const [buttonRevColor, setButtonRevColor] = useState("blue" as SemanticCOLORS)


const handleButtonChangeRev = () => {
if(buttonRev === 'Add Primers'){
setButtonRevColor('red')
setButtonRev('Remove Primers')
}
else{
setButtonRevColor('blue')
setButtonRev('Add Primers')
}
}

useEffect(() => {

setButtonRev('Add Primers');
setButtonRevColor('blue');

}, [props.data]);


return (
<div>
<Modal
open={props.open}
>
<Modal.Header>Primer Information</Modal.Header>
<Modal.Content>
<p>Forward Primer Sequence: {props.data[0].seq}</p>
<p>Forward Primer Length: {props.data[0].seq.length} bp</p>
<p>Forward Primer GC Content: {props.data[0].GCContent}</p>
<p>Forward Primer Temp: {props.data[0].temp} °C</p>

<br></br>
<p>Reverse Primer Sequence: {props.data[1].seq}</p>
<p>Reverse Primer Length: {props.data[1].seq.length} bp</p>
<p>Reverse Primer GC Content: {props.data[1].GCContent}</p>
<p>Reverse Primer Temp: {props.data[1].temp} °C</p>
<Button color={buttonRevColor} onClick={() => {
if(buttonRev === 'Remove Primers'){
props.removePrimers();
}
else{
props.addPrimers([props.data[0], props.data[1]]);
}
handleButtonChangeRev()}}>{buttonRev}</Button>
</Modal.Content>
<Modal.Actions>
<Button color='red' onClick={props.closeModal}>
Close
</Button>
</Modal.Actions>
</Modal>
</div>
);
};
Loading

0 comments on commit 74efa1a

Please sign in to comment.