5858 background-color : # E0BBE4 ;
5959 }
6060
61+ .trimming {
62+ background-color : # ffedef ;
63+ }
64+
6165 .non-typical {
6266 background-color : # D3D3D3 ;
6367 }
6468
69+ .drop-placeholder {
70+ display : inline-block;
71+ width : 60px ;
72+ height : 28px ;
73+ margin : 5px ;
74+ border : 2px dashed # 999 ;
75+ border-radius : 5px ;
76+ background-color : rgba (0 , 0 , 0 , 0.06 );
77+ }
78+
6579 .add-tool-form {
6680 margin-top : 20px ;
6781 display : flex;
86100< body >
87101 < div id ="app " class ="container ">
88102 < h1 style ="font-size: 1.5rem; font-weight: bold; margin-bottom: 1rem; "> RNA-seq Pipeline Builder</ h1 >
89- < label >
103+ <!-- < label>
90104 <input type="checkbox" v-model="colorByFunction"> Color by function
91- </ label >
105+ </label> -->
92106 < div >
93107 < h2 style ="font-size: 1.25rem; font-weight: 600; margin-bottom: 0.5rem; "> Pipeline</ h2 >
94- < div class ="section " @dragover.prevent @drop ="onDrop($event, 'top') ">
95- < div v-for ="tool in topTools " :key ="tool.id " class ="tool " :class ="{ [tool.function]: colorByFunction } "
96- draggable ="true " @dragstart ="onDragStart($event, tool.id) ">
97- {{ tool.name }}
98- </ div >
108+ < div class ="section builtPipeline " @dragover.prevent ="onBuiltPipelineDragOver "
109+ @drop ="onDrop($event, 'top') ">
110+ < template v-for ="(tool, index) in topTools " :key ="tool.id ">
111+ < div v-if ="topInsertIndex === index " class ="drop-placeholder " @dragover.prevent
112+ @drop.stop ="onDropOnPlaceholder($event, index) "> </ div >
113+ < div class ="tool "
114+ :class ="colorByFunction ? (Array.isArray(tool.function) ? tool.function[0] : tool.function) : '' "
115+ draggable ="true " @dragstart ="onDragStart($event, tool.id, 'top') "
116+ @dragover.prevent ="onTopItemDragOver($event, index) "
117+ @drop.stop ="onDropOnTopItem($event, tool.id) ">
118+ {{ tool.name }}
119+ </ div >
120+ </ template >
121+ < div v-if ="topInsertIndex === topTools.length " class ="drop-placeholder " @dragover.prevent
122+ @drop.stop ="onDropOnPlaceholder($event, topTools.length) "> </ div >
99123 </ div >
100124 </ div >
101125 < div >
102126 < h2 style ="font-size: 1.25rem; font-weight: 600; margin-bottom: 0.5rem; "> Available Tools</ h2 >
103- < div class ="section " @dragover.prevent @drop ="onDrop($event, 'bottom') ">
104- < div v-for ="tool in bottomTools " :key ="tool.id " class ="tool "
105- :class ="{ [tool.function]: colorByFunction } " draggable ="true "
106- @dragstart ="onDragStart($event, tool.id) ">
127+ < div style ="margin-bottom: 0.5rem; display: flex; gap: 8px; align-items: center; ">
128+ < label for ="function-filter " style ="font-weight: 600; "> Filter by function:</ label >
129+ < select id ="function-filter " v-model ="functionFilter ">
130+ < option :value ="'All' "> All</ option >
131+ < option v-for ="opt in functionOptions " :key ="opt.value " :value ="opt.value "> {{ opt.label }}</ option >
132+ </ select >
133+ </ div >
134+ < div class ="section tools " @dragover.prevent @drop ="onDrop($event, 'bottom') ">
135+ < div v-for ="tool in filteredBottomTools " :key ="tool.id " class ="tool "
136+ :class ="colorByFunction ? (Array.isArray(tool.function) ? tool.function[0] : tool.function) : '' "
137+ draggable ="true " @dragstart ="onDragStart($event, tool.id, 'bottom') ">
107138 {{ tool.name }}
108139 </ div >
109140 </ div >
@@ -115,89 +146,204 @@ <h2 style="font-size: 1.25rem; font-weight: 600; margin-bottom: 0.5rem;">Availab
115146 </ div >
116147
117148 < script >
118- const { createApp, ref } = Vue ;
149+ const { createApp, ref, computed } = Vue ;
119150
120151 const app = createApp ( {
121152 setup ( ) {
153+ // Mapping of function keys to human-readable labels
154+ const functionLabels = {
155+ 'pre-alignment-qc' : 'Pre-alignment QC' ,
156+ 'trimming' : 'Trimming' ,
157+ 'post-alignment-qc' : 'Post-alignment QC' ,
158+ 'read-counting' : 'Read counting' ,
159+ 'alignment' : 'Alignment' ,
160+ 'general-bam-processing' : 'General BAM processing' ,
161+ 'differential-expression' : 'Differential expression analysis' ,
162+ 'non-typical' : 'Other' ,
163+ } ;
164+
122165 const initialTools = [
123- { id : 'FastQC-pre' , name : 'FastQC → ' , function : 'pre-alignment-qc' } ,
124- { id : 'FastQC-post' , name : 'FastQC → ' , function : 'pre-alignment-qc' } ,
125- { id : 'tool2' , name : 'Trimmomatic → ' , function : 'pre-alignment-qc' } ,
126- { id : 'Degust' , name : 'Degust → ' , function : 'differential-expression' } ,
127- { id : 'tool3' , name : 'HISAT2 → ' , function : 'alignment' } ,
128- { id : 'tool4' , name : 'STAR → ' , function : 'alignment' } ,
129- { id : 'tool5' , name : 'Salmon → ' , function : 'read-counting' } ,
130- { id : 'tool6' , name : 'Kallisto → ' , function : 'read-counting' } ,
131- { id : 'tool7' , name : 'Samtools → ' , function : 'general-bam-processing' } ,
132- { id : 'tool8' , name : 'featureCounts → ' , function : 'read-counting' } ,
133- { id : 'tool9' , name : 'HTSeq → ' , function : 'read-counting' } ,
134- { id : 'tool10' , name : 'DESeq2 → ' , function : 'differential-expression' } ,
135- { id : 'tool11' , name : 'RSeQC → ' , function : 'post-alignment-qc' } ,
136- { id : 'tool12' , name : 'Qualimap → ' , function : 'post-alignment-qc' } ,
137- { id : 'tool13' , name : 'Subread → ' , function : 'alignment' } ,
138- { id : 'tool14' , name : 'Cutadapt → ' , function : 'pre-alignment-qc' } ,
139- { id : 'tool15' , name : 'fastp → ' , function : 'pre-alignment-qc' } ,
140- { id : 'tool16' , name : 'limma → ' , function : 'differential-expression' } ,
141- { id : 'tool17' , name : 'edgeR → ' , function : 'differential-expression' } ,
142- { id : 'tool18' , name : 'sambamba → ' , function : 'general-bam-processing' } ,
143- { id : 'tool19' , name : 'bedtools → ' , function : 'general-bam-processing' } ,
144- { id : 'tool20' , name : 'RSEM → ' , function : 'read-counting' } ,
145- { id : 'tool21' , name : 'UMI-tools → ' , function : 'pre-alignment-qc' } ,
146- { id : 'tool22' , name : 'Trinity → ' , function : 'non-typical' } ,
147- { id : 'tool23' , name : 'StringTie → ' , function : 'non-typical' } ,
148- { id : 'tool24' , name : 'GATK → ' , function : 'non-typical' } ,
149- { id : 'tool25' , name : 'Microsoft Excel → ' , function : 'non-typical' } ,
150- { id : 'tool26' , name : 'Microsoft Word → ' , function : 'non-typical' } ,
151- { id : 'tool27' , name : 'picard MarkDuplicates → ' , function : 'general-bam-processing' } ,
152- { id : 'tool28' , name : 'samtools sort → ' , function : 'general-bam-processing' } ,
153- { id : 'tool29' , name : 'Bowtie2 → ' , function : 'alignment' } ,
154- { id : 'MultiQC' , name : 'MultiQC → ' , function : 'post-alignment-qc' } ,
155- { id : 'Glimma' , name : 'Glimma → ' , function : 'differential-expression' } ,
156- { id : 'DupRadar' , name : 'DupRadar → ' , function : 'post-alignment-qc' } ,
166+ { id : 'FastQC-pre' , name : 'FastQC → ' , function : [ 'pre-alignment-qc' ] } ,
167+ { id : 'FastQC-post' , name : 'FastQC → ' , function : [ 'pre-alignment-qc' ] } ,
168+ { id : 'tool2' , name : 'Trimmomatic → ' , function : [ 'trimming' ] } ,
169+ { id : 'Degust' , name : 'Degust → ' , function : [ 'differential-expression' ] } ,
170+ { id : 'tool3' , name : 'HISAT2 → ' , function : [ 'alignment' ] } ,
171+ { id : 'tool4' , name : 'STAR → ' , function : [ 'alignment' ] } ,
172+ { id : 'tool5' , name : 'Salmon → ' , function : [ 'read-counting' ] } ,
173+ { id : 'tool6' , name : 'Kallisto → ' , function : [ 'read-counting' ] } ,
174+ { id : 'tool7' , name : 'Samtools → ' , function : [ 'general-bam-processing' ] } ,
175+ { id : 'tool8' , name : 'featureCounts → ' , function : [ 'read-counting' ] } ,
176+ { id : 'tool9' , name : 'HTSeq → ' , function : [ 'read-counting' ] } ,
177+ { id : 'tool10' , name : 'DESeq2 → ' , function : [ 'differential-expression' ] } ,
178+ { id : 'tool11' , name : 'RSeQC → ' , function : [ 'post-alignment-qc' ] } ,
179+ { id : 'tool12' , name : 'Qualimap → ' , function : [ 'post-alignment-qc' ] } ,
180+ { id : 'tool13' , name : 'Subread → ' , function : [ 'alignment' ] } ,
181+ { id : 'tool14' , name : 'Cutadapt → ' , function : [ 'trimming' ] } ,
182+ { id : 'tool15' , name : 'fastp → ' , function : [ 'pre-alignment-qc' , 'trimming' ] } ,
183+ { id : 'tool16' , name : 'limma → ' , function : [ 'differential-expression' ] } ,
184+ { id : 'tool17' , name : 'edgeR → ' , function : [ 'differential-expression' ] } ,
185+ { id : 'tool18' , name : 'sambamba → ' , function : [ 'general-bam-processing' ] } ,
186+ { id : 'tool19' , name : 'bedtools → ' , function : [ 'general-bam-processing' ] } ,
187+ { id : 'tool20' , name : 'RSEM → ' , function : [ 'read-counting' ] } ,
188+ { id : 'tool21' , name : 'UMI-tools → ' , function : [ 'trimming' ] } ,
189+ { id : 'tool22' , name : 'Trinity → ' , function : [ 'non-typical' ] } ,
190+ { id : 'tool23' , name : 'StringTie → ' , function : [ 'non-typical' ] } ,
191+ { id : 'tool24' , name : 'GATK → ' , function : [ 'non-typical' ] } ,
192+ { id : 'tool25' , name : 'Microsoft Excel → ' , function : [ 'non-typical' ] } ,
193+ { id : 'tool26' , name : 'Microsoft Word → ' , function : [ 'non-typical' ] } ,
194+ { id : 'tool27' , name : 'picard MarkDuplicates → ' , function : [ 'general-bam-processing' ] } ,
195+ { id : 'tool28' , name : 'samtools sort → ' , function : [ 'general-bam-processing' ] } ,
196+ { id : 'tool29' , name : 'Bowtie2 → ' , function : [ 'alignment' ] } ,
197+ { id : 'MultiQC' , name : 'MultiQC → ' , function : [ 'post-alignment-qc' ] } ,
198+ { id : 'Glimma' , name : 'Glimma → ' , function : [ 'differential-expression' ] } ,
199+ { id : 'DupRadar' , name : 'DupRadar → ' , function : [ 'post-alignment-qc' ] } ,
157200 ] ;
158201
159202 const topTools = ref ( [ ] ) ;
203+ const topInsertIndex = ref ( null ) ;
160204 const bottomTools = ref ( [ ...initialTools ] ) ;
161205 const colorByFunction = ref ( true ) ;
206+ const functionFilter = ref ( 'All' ) ;
162207 const newToolName = ref ( '' ) ;
163208
164- const onDragStart = ( event , id ) => {
209+ const onDragStart = ( event , id , sourceList ) => {
165210 event . dataTransfer . setData ( 'text/plain' , id ) ;
211+ event . dataTransfer . setData ( 'source' , sourceList ) ;
166212 } ;
167213
168214 const onDrop = ( event , targetList ) => {
169- const id = event . dataTransfer . getData ( 'text' ) ;
215+ const id = event . dataTransfer . getData ( 'text/plain' ) || event . dataTransfer . getData ( 'text' ) ;
216+ const sourceList = event . dataTransfer . getData ( 'source' ) ;
170217 const allTools = [ ...topTools . value , ...bottomTools . value ] ;
171218 const sourceTool = allTools . find ( tool => tool . id === id ) ;
172219
173- if ( targetList === 'top' && ! topTools . value . some ( tool => tool . id === id ) ) {
174- topTools . value . push ( sourceTool ) ;
175- bottomTools . value = bottomTools . value . filter ( tool => tool . id !== id ) ;
176- } else if ( targetList === 'bottom' && ! bottomTools . value . some ( tool => tool . id === id ) ) {
177- bottomTools . value . push ( sourceTool ) ;
178- topTools . value = topTools . value . filter ( tool => tool . id !== id ) ;
220+ if ( targetList === 'top' ) {
221+ if ( sourceList === 'bottom' && ! topTools . value . some ( tool => tool . id === id ) ) {
222+ // Move from bottom to end of top or at placeholder index
223+ const insertIndex = ( topInsertIndex . value ?? topTools . value . length ) ;
224+ topTools . value . splice ( insertIndex , 0 , sourceTool ) ;
225+ bottomTools . value = bottomTools . value . filter ( tool => tool . id !== id ) ;
226+ }
227+ topInsertIndex . value = null ;
228+ // If source is top and dropped on the container, treat as no-op (keeps current order)
229+ } else if ( targetList === 'bottom' ) {
230+ if ( sourceList === 'top' && ! bottomTools . value . some ( tool => tool . id === id ) ) {
231+ // Move from top to end of bottom
232+ bottomTools . value . push ( sourceTool ) ;
233+ topTools . value = topTools . value . filter ( tool => tool . id !== id ) ;
234+ }
235+ // If source is bottom and dropped on container, no-op
236+ }
237+ } ;
238+
239+ const onDropOnTopItem = ( event , targetId ) => {
240+ const draggedId = event . dataTransfer . getData ( 'text/plain' ) || event . dataTransfer . getData ( 'text' ) ;
241+ const sourceList = event . dataTransfer . getData ( 'source' ) ;
242+ if ( ! draggedId || draggedId === targetId ) return ;
243+
244+ const targetIndex = topTools . value . findIndex ( t => t . id === targetId ) ;
245+ if ( targetIndex === - 1 ) return ;
246+
247+ if ( sourceList === 'top' ) {
248+ const draggedIndex = topTools . value . findIndex ( t => t . id === draggedId ) ;
249+ if ( draggedIndex === - 1 ) return ;
250+ const [ moved ] = topTools . value . splice ( draggedIndex , 1 ) ;
251+ let insertIndex = targetIndex ;
252+ if ( draggedIndex < targetIndex ) insertIndex -= 1 ;
253+ topTools . value . splice ( insertIndex , 0 , moved ) ;
254+ topInsertIndex . value = null ;
255+ } else if ( sourceList === 'bottom' ) {
256+ const draggedIndexBottom = bottomTools . value . findIndex ( t => t . id === draggedId ) ;
257+ if ( draggedIndexBottom === - 1 ) return ;
258+ const [ moved ] = bottomTools . value . splice ( draggedIndexBottom , 1 ) ;
259+ topTools . value . splice ( targetIndex , 0 , moved ) ;
260+ topInsertIndex . value = null ;
261+ }
262+ } ;
263+
264+ const onDropOnPlaceholder = ( event , insertIndex ) => {
265+ const draggedId = event . dataTransfer . getData ( 'text/plain' ) || event . dataTransfer . getData ( 'text' ) ;
266+ const sourceList = event . dataTransfer . getData ( 'source' ) ;
267+ if ( ! draggedId ) return ;
268+
269+ if ( sourceList === 'top' ) {
270+ const draggedIndex = topTools . value . findIndex ( t => t . id === draggedId ) ;
271+ if ( draggedIndex === - 1 ) return ;
272+ const [ moved ] = topTools . value . splice ( draggedIndex , 1 ) ;
273+ let target = insertIndex ;
274+ if ( draggedIndex < insertIndex ) target -= 1 ;
275+ topTools . value . splice ( target , 0 , moved ) ;
276+ } else if ( sourceList === 'bottom' ) {
277+ const draggedIndexBottom = bottomTools . value . findIndex ( t => t . id === draggedId ) ;
278+ if ( draggedIndexBottom === - 1 ) return ;
279+ const [ moved ] = bottomTools . value . splice ( draggedIndexBottom , 1 ) ;
280+ topTools . value . splice ( insertIndex , 0 , moved ) ;
179281 }
282+ topInsertIndex . value = null ;
283+ } ;
284+
285+ const onTopItemDragOver = ( event , index ) => {
286+ // Compute whether to insert before or after based on cursor horizontal position
287+ const rect = event . currentTarget . getBoundingClientRect ( ) ;
288+ const midpoint = rect . left + rect . width / 2 ;
289+ topInsertIndex . value = event . clientX < midpoint ? index : index + 1 ;
290+ } ;
291+
292+ const onBuiltPipelineDragOver = ( event ) => {
293+ // Show placeholder at end when hovering over container with no specific item
294+ if ( topTools . value . length === 0 ) {
295+ topInsertIndex . value = 0 ;
296+ } else {
297+ topInsertIndex . value = topTools . value . length ;
298+ }
299+ } ;
300+
301+ const onBuiltPipelineDragLeave = ( ) => {
302+ topInsertIndex . value = null ;
180303 } ;
181304
182305 const addTool = ( ) => {
183306 if ( newToolName . value . trim ( ) ) {
184307 const newTool = {
185308 id : `tool${ Date . now ( ) } ` ,
186309 name : `${ newToolName . value . trim ( ) } → ` ,
187- function : 'non-typical'
310+ function : [ 'non-typical' ]
188311 } ;
189312 bottomTools . value . push ( newTool ) ;
190313 newToolName . value = '' ;
191314 }
192315 } ;
193316
317+ const functionOptions = computed ( ( ) => {
318+ return Object . entries ( functionLabels ) . map ( ( [ value , label ] ) => ( { value, label } ) ) ;
319+ } ) ;
320+
321+ const filteredBottomTools = computed ( ( ) => {
322+ if ( functionFilter . value === 'All' ) {
323+ return bottomTools . value ;
324+ }
325+ return bottomTools . value . filter ( t => Array . isArray ( t . function )
326+ ? t . function . includes ( functionFilter . value )
327+ : t . function === functionFilter . value ) ;
328+ } ) ;
329+
194330 return {
331+ functionLabels,
195332 topTools,
196333 bottomTools,
334+ topInsertIndex,
197335 colorByFunction,
336+ functionFilter,
337+ functionOptions,
338+ filteredBottomTools,
198339 newToolName,
199340 onDragStart,
200341 onDrop,
342+ onDropOnTopItem,
343+ onDropOnPlaceholder,
344+ onTopItemDragOver,
345+ onBuiltPipelineDragOver,
346+ onBuiltPipelineDragLeave,
201347 addTool
202348 } ;
203349 }
0 commit comments