Skip to content
This repository was archived by the owner on Aug 15, 2025. It is now read-only.

Commit 9b15ccf

Browse files
committed
Add tool type filter to fantasy pipeline builder
Allow rearrangement of tools once placed
1 parent f0c55f5 commit 9b15ccf

1 file changed

Lines changed: 201 additions & 55 deletions

File tree

files/fantasy-rnaseq-pipeline-builder.html

Lines changed: 201 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,24 @@
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;
@@ -86,24 +100,41 @@
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

Comments
 (0)