From 74efa1aa9627ef89962272c5e85fe63336abb2c0 Mon Sep 17 00:00:00 2001 From: Nick Rosenau Date: Wed, 15 Nov 2023 12:54:23 -0500 Subject: [PATCH] Added primer design feature. --- demo/lib/App.tsx | 8 +- src/PrimerDesign/PrimerDesign.tsx | 263 ++++++++++++++++++++++++++++++ src/PrimerDesign/PrimerModal.tsx | 63 +++++++ src/SeqViz.tsx | 10 +- 4 files changed, 342 insertions(+), 2 deletions(-) create mode 100644 src/PrimerDesign/PrimerDesign.tsx create mode 100644 src/PrimerDesign/PrimerModal.tsx diff --git a/demo/lib/App.tsx b/demo/lib/App.tsx index b10b7dcfd..79860902d 100644 --- a/demo/lib/App.tsx +++ b/demo/lib/App.tsx @@ -11,6 +11,7 @@ import { Input, Menu, Sidebar, + Modal } from "semantic-ui-react"; import seqparse from "seqparse"; @@ -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" }, @@ -66,7 +68,7 @@ export default class App extends React.Component { { end: 1885, start: 1165 }, ], viewer: "both", - zoom: 50, + zoom: 50 }; linearRef: React.RefObject = React.createRef(); circularRef: React.RefObject = React.createRef(); @@ -100,6 +102,7 @@ export default class App extends React.Component { } }; + render() { let customChildren = null; if (this.state.customChildren) { @@ -153,6 +156,8 @@ export default class App extends React.Component { }; } + + return (
@@ -228,6 +233,7 @@ export default class App extends React.Component { translations={this.state.translations} viewer={this.state.viewer as "linear" | "circular"} zoom={{ linear: this.state.zoom }} + primerDesign={true} > {customChildren} diff --git a/src/PrimerDesign/PrimerDesign.tsx b/src/PrimerDesign/PrimerDesign.tsx new file mode 100644 index 000000000..a08e132c1 --- /dev/null +++ b/src/PrimerDesign/PrimerDesign.tsx @@ -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(null) + const [oldPrimerSelect, setOldPrimerSelect] = useState("") + const [editButton, setEditButton] = useState(false) + const [target, setTarget] = useState(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( +
+ {props.primerSelect !== "" && !editButton && ( + + )} + {props.primerSelect !== "" && editButton && ( + + )} + {primers && ( + setOpenModal(false)} data={primers} addPrimers={(data:Primer[]) => addPrimers(data)} removePrimers={removePrimers} /> + )} +
+ ) +} \ No newline at end of file diff --git a/src/PrimerDesign/PrimerModal.tsx b/src/PrimerDesign/PrimerModal.tsx new file mode 100644 index 000000000..4e3c50649 --- /dev/null +++ b/src/PrimerDesign/PrimerModal.tsx @@ -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 ( +
+ + Primer Information + +

Forward Primer Sequence: {props.data[0].seq}

+

Forward Primer Length: {props.data[0].seq.length} bp

+

Forward Primer GC Content: {props.data[0].GCContent}

+

Forward Primer Temp: {props.data[0].temp} °C

+ +

+

Reverse Primer Sequence: {props.data[1].seq}

+

Reverse Primer Length: {props.data[1].seq.length} bp

+

Reverse Primer GC Content: {props.data[1].GCContent}

+

Reverse Primer Temp: {props.data[1].temp} °C

+ +
+ + + +
+
+ ); +}; \ No newline at end of file diff --git a/src/SeqViz.tsx b/src/SeqViz.tsx index 2b1af072f..d5e0fd49d 100644 --- a/src/SeqViz.tsx +++ b/src/SeqViz.tsx @@ -19,6 +19,7 @@ import { isEqual } from "./isEqual"; import search from "./search"; import { Selection } from "./selectionContext"; import { complement, directionality, guessType, randomID } from "./sequence"; +import { PrimerDesign } from "./PrimerDesign/PrimerDesign"; /** `SeqViz` props. See the README for more details. One of `seq`, `file` or `accession` is required. */ export interface SeqVizProps { @@ -163,6 +164,7 @@ export interface SeqVizProps { /** how zoomed to make the linear viewer. default: 50 */ linear?: number; }; + primerDesign?: boolean; } export interface SeqVizState { @@ -203,6 +205,7 @@ export default class SeqViz extends React.Component { translations: [], viewer: "both", zoom: { circular: 0, linear: 50 }, + primerDesign: false }; constructor(props: SeqVizProps) { @@ -233,7 +236,7 @@ export default class SeqViz extends React.Component { }); } } - + // Check if an accession was passed, we'll query it here if so const { accession } = this.props; if (!accession || !accession.length) { @@ -459,10 +462,15 @@ export default class SeqViz extends React.Component { circular: typeof zoom?.circular == "number" ? Math.min(Math.max(zoom.circular, 0), 100) : 0, linear: typeof zoom?.linear == "number" ? Math.min(Math.max(zoom.linear, 0), 100) : 50, }, + primerDesign: false }; return (
+ {this.props.primerDesign && this.props.selection?.start && { + this.setState({annotations: data}) + }} /> }
);