Skip to content

Instantly share code, notes, and snippets.

@romainGuiet
Last active February 5, 2026 09:02
Show Gist options
  • Select an option

  • Save romainGuiet/81bf5e601e3ddf414246ccd2765a4f0d to your computer and use it in GitHub Desktop.

Select an option

Save romainGuiet/81bf5e601e3ddf414246ccd2765a4f0d to your computer and use it in GitHub Desktop.
a QuPath GUI to load points from csv file
import javafx.application.Platform
import javafx.scene.Scene
import javafx.scene.control.*
import javafx.scene.layout.*
import javafx.stage.Stage
import javafx.stage.DirectoryChooser
import javafx.geometry.Insets
import javafx.geometry.Pos
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import javafx.scene.control.cell.PropertyValueFactory
import javafx.scene.paint.Color
import java.io.File
// QuPath imports for creating detections
import qupath.lib.objects.PathObjects
import qupath.lib.roi.ROIs
import qupath.lib.regions.ImagePlane
Platform.runLater {
// -------------------- UI SETUP --------------------
def stage = new Stage()
stage.setTitle("QuPath Custom GUI - CSV Loader")
def tabPane = new TabPane()
// ---------- shared state ----------
def topGenesFromTab1 = []
def allGenesFromTab1 = [] // all unique genes
def currentCsvData = [] // List<List<String>>
def currentHeaders = [] // List<String>
def selectedFolder = [null] as File[]
def geneColumnIndex = -1
def xColumnIndex = -1
def yColumnIndex = -1
def zColumnIndex = -1
def dimensionUnit = "micron"
// ---------- Tab‑2 helpers ----------
def geneDropdowns = []
def colorPickers = []
def progressBar = null
// ===================== TAB 1 =====================
def tab1 = new Tab("CSV Loader")
tab1.setClosable(false)
def vbox1 = new VBox(10)
vbox1.setPadding(new Insets(15))
// Title
def label1 = new Label("CSV File Loader")
label1.setStyle("-fx-font-size: 14px; -fx-font-weight: bold;")
// ------ Folder chooser with extension & delimiter ------
def folderHBox = new HBox(10)
folderHBox.setAlignment(Pos.CENTER_LEFT)
def folderLabel = new Label("Folder:")
def folderField = new TextField()
folderField.setPromptText("No folder selected")
folderField.setPrefWidth(200)
folderField.setEditable(false)
def browseButton = new Button("Browse...")
def extensionLabel = new Label("Ext:")
def extensionField = new TextField(".part")
extensionField.setPrefWidth(60)
def delimiterLabel = new Label("Delim:")
def delimiterField = new TextField(",")
delimiterField.setPrefWidth(40)
browseButton.setOnAction({ e ->
def chooser = new DirectoryChooser()
chooser.setTitle("Select Folder Containing CSV Files")
def folder = chooser.showDialog(stage)
if (folder) {
selectedFolder[0] = folder
folderField.setText(folder.absolutePath)
}
})
folderHBox.getChildren().addAll(folderLabel, folderField, browseButton,
extensionLabel, extensionField,
delimiterLabel, delimiterField)
// ------ Load button ------
def loadButton = new Button("Load CSV Files")
loadButton.setPrefWidth(120)
// ------ Table preview ------
def csvTable = new TableView<ObservableList<String>>()
csvTable.setPrefHeight(150)
csvTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY)
// ------ Column‑mapping comboboxes ------
def coordHBox = new HBox(10)
coordHBox.setAlignment(Pos.CENTER_LEFT)
def xCombo = new ComboBox<String>(); xCombo.setPrefWidth(100)
def yCombo = new ComboBox<String>(); yCombo.setPrefWidth(100)
def zCombo = new ComboBox<String>(); zCombo.setPrefWidth(100)
def geneCombo = new ComboBox<String>(); geneCombo.setPrefWidth(100)
def dimensionCombo = new ComboBox<String>()
dimensionCombo.getItems().addAll("micron", "pixel")
dimensionCombo.setValue("micron")
dimensionCombo.setPrefWidth(80)
coordHBox.getChildren().addAll(
new Label("X:"), xCombo,
new Label("Y:"), yCombo,
new Label("Z:"), zCombo,
new Label("Gene:"), geneCombo,
new Label("Dimension:"), dimensionCombo
)
// ------ Analyse abundance button ------
def analyzeButton = new Button("Analyse Abundancy")
analyzeButton.setPrefWidth(150)
// placeholders for abundance results (will be created later)
def abundanceLabel = null
def abundanceTable = null
// ----------------- LOAD BUTTON ACTION (NEW BLOCK) -----------------
loadButton.setOnAction({ e ->
if (selectedFolder[0] == null) {
showAlert("Error", "Please select a folder first.")
return
}
def extension = extensionField.getText()
def delimiter = delimiterField.getText()
if (!extension || !delimiter) {
showAlert("Error", "Please specify both extension and delimiter.")
return
}
try {
// ---- 1️⃣ Find every matching CSV file --------------------
def csvFiles = selectedFolder[0].listFiles().findAll {
it.isFile() && it.name.endsWith(extension)
}
if (csvFiles.isEmpty()) {
showAlert("Warning", "No files found with extension: $extension")
return
}
// ---- 2️⃣ Read all files & concatenate rows ----------------
def allRows = []
def headerFound = false
def headerList = null
csvFiles.each { file ->
def lines = file.readLines()
if (lines.isEmpty()) return // skip empty file
def thisHeader = lines[0].split(delimiter)
.collect { it.trim().replace('"','') }
if (!headerFound) {
headerList = thisHeader
headerFound = true
} else {
if (!thisHeader.equals(headerList)) {
showAlert(
"Error",
"Header mismatch in file '${file.name}'.\nAll CSV files must have identical column headers."
)
throw new IllegalStateException("Header mismatch")
}
}
// collect data rows (skip header)
def dataRows = lines.drop(1).collect { line ->
line.split(delimiter).collect { it.trim().replace('"','') }
}
allRows.addAll(dataRows)
}
// ---- 3️⃣ Store globally ---------------------------------
currentHeaders = headerList
currentCsvData = allRows
// ---- 4️⃣ Populate preview table (first 5 rows) ----------
csvTable.getColumns().clear()
currentHeaders.eachWithIndex { header, idx ->
def col = new TableColumn<ObservableList<String>, String>(header)
col.setCellValueFactory({ param ->
new javafx.beans.property.SimpleStringProperty(
param.getValue().size() > idx ? param.getValue().get(idx) : ""
)
})
csvTable.getColumns().add(col)
}
def preview = FXCollections.observableArrayList()
def maxRows = Math.min(5, currentCsvData.size())
for (int i = 0; i < maxRows; i++) {
preview.add(FXCollections.observableArrayList(currentCsvData[i]))
}
csvTable.setItems(preview)
// ---- 5️⃣ Fill column‑mapping combos --------------------
xCombo.getItems().setAll(currentHeaders)
yCombo.getItems().setAll(currentHeaders)
zCombo.getItems().setAll(currentHeaders)
zCombo.getItems().add("Current annotation Z")
geneCombo.getItems().setAll(currentHeaders)
// optional defaults (same as before)
if (currentHeaders.contains("x_location")) xCombo.setValue("x_location")
if (currentHeaders.contains("y_location")) yCombo.setValue("y_location")
if (currentHeaders.contains("z_location")) zCombo.setValue("z_location")
else zCombo.setValue("Current annotation Z")
if (currentHeaders.contains("feature_name")) geneCombo.setValue("feature_name")
// ---- 6️⃣ Console feedback -----------------------------
println "Loaded ${csvFiles.size()} CSV file(s) from '${selectedFolder[0]}'"
println "Combined rows: ${currentCsvData.size()}"
println "Headers: ${currentHeaders}"
println "First 5 rows shown in the preview table."
} catch (Exception ex) {
showAlert("Error", "Failed to load CSV files: ${ex.message}")
ex.printStackTrace()
}
})
// -----------------------------------------------------------------
// ----------------- ANALYSE ABUNDANCY ACTION (unchanged) -----------------
analyzeButton.setOnAction({ e ->
if (currentCsvData.isEmpty() || currentHeaders.isEmpty()) {
showAlert("Error", "Please load a CSV file first.")
return
}
def geneColumn = geneCombo.getValue()
if (!geneColumn) {
showAlert("Error", "Please select a gene column.")
return
}
def geneIndex = currentHeaders.indexOf(geneColumn)
if (geneIndex == -1) {
showAlert("Error", "Selected gene column not found.")
return
}
// store indices for Tab‑2
geneColumnIndex = geneIndex
xColumnIndex = currentHeaders.indexOf(xCombo.getValue())
yColumnIndex = currentHeaders.indexOf(yCombo.getValue())
zColumnIndex = zCombo.getValue() == "Current annotation Z" ? -1
: currentHeaders.indexOf(zCombo.getValue())
dimensionUnit = dimensionCombo.getValue()
try {
// ---- count gene frequencies ---------------------------------
def geneCounts = [:] as Map<String,Integer>
def allUniqueGenes = [] as Set<String>
currentCsvData.each { row ->
if (row.size() > geneIndex) {
def gene = row[geneIndex]
if (gene?.trim()) {
geneCounts[gene] = (geneCounts[gene] ?: 0) + 1
allUniqueGenes << gene
}
}
}
// ---- top‑10 -----------------------------------------------------------------
def topGenes = geneCounts.entrySet()
.sort{ -it.value }
.take(10)
topGenesFromTab1 = topGenes.collect { [gene: it.key, count: it.value] }
allGenesFromTab1 = allUniqueGenes.sort()
// ---- update Tab‑2 dropdowns -------------------------------------------------
geneDropdowns.each { dd ->
dd.getItems().clear()
dd.getItems().add("none")
dd.getItems().addAll(allGenesFromTab1)
}
// ---- pre‑fill the first 10 dropdowns with top genes ----------------------------
topGenesFromTab1.eachWithIndex { geneData, idx ->
if (idx < geneDropdowns.size()) {
geneDropdowns[idx].setValue(geneData.gene)
// give each a different colour (you can change this)
def colours = [
Color.RED, Color.BLUE, Color.GREEN, Color.ORANGE,
Color.PURPLE, Color.CYAN, Color.MAGENTA,
Color.YELLOW, Color.PINK, Color.BROWN
]
colorPickers[idx].setValue(colours[idx % colours.size()])
}
}
// ---- build the top‑10 abundance table -----------------------------------------
// remove previous if they exist
if (abundanceLabel) vbox1.getChildren().remove(abundanceLabel)
if (abundanceTable) vbox1.getChildren().remove(abundanceTable)
abundanceLabel = new Label("Top 10 Gene Abundance Results:")
abundanceLabel.setStyle("-fx-font-weight: bold;")
abundanceTable = new TableView()
abundanceTable.setPrefHeight(50)
abundanceTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY)
// compact rows
abundanceTable.setRowFactory({ tv ->
def row = new TableRow()
row.setPrefHeight(25)
row
})
// data array (10 columns, 1 row)
def rowData = new String[10]
for (int i = 0; i < topGenes.size(); i++) {
rowData[i] = "${topGenes[i].value}"
}
for (int i = topGenes.size(); i < 10; i++) {
rowData[i] = ""
}
// create columns with gene names as headers
for (int i = 0; i < 10; i++) {
def col = new TableColumn()
col.setPrefWidth(100)
col.setText(i < topGenes.size() ? topGenes[i].key : "")
def idx = i
col.setCellValueFactory({ cd ->
new javafx.beans.property.SimpleStringProperty(
cd.getValue()[idx] ?: ""
)
})
abundanceTable.getColumns().add(col)
}
def tblData = FXCollections.observableArrayList()
tblData.add(rowData)
abundanceTable.setItems(tblData)
abundanceTable.setTableMenuButtonVisible(false)
abundanceTable.setEditable(false)
abundanceTable.setStyle("-fx-table-cell-border-color: transparent;")
vbox1.getChildren().addAll(abundanceLabel, abundanceTable)
println "Gene abundance analysis completed – ${allUniqueGenes.size()} unique genes."
topGenes.eachWithIndex { gene,i -> println "${i+1}. ${gene.key}: ${gene.value}" }
} catch (Exception ex) {
showAlert("Error", "Failed to analyse gene abundance: ${ex.message}")
ex.printStackTrace()
}
})
// -----------------------------------------------------------------
// ---- helper alert ------------------------------------------------
def showAlert = { title, msg ->
Platform.runLater {
def a = new Alert(Alert.AlertType.INFORMATION)
a.title = title
a.headerText = null
a.contentText = msg
a.showAndWait()
}
}
// ---- helper for Z‑plane (unchanged) -------------------------------
def getCurrentAnnotationZPlane = {
def img = getCurrentImageData()
if (!img) return ImagePlane.getDefaultPlane()
def sel = getSelectedObject()
if (sel && sel.getROI()) return sel.getROI().getImagePlane()
ImagePlane.getDefaultPlane()
}
// ------------------- Assemble Tab 1 -------------------------------
vbox1.getChildren().addAll(
label1,
new Separator(),
folderHBox,
loadButton,
new Label("CSV Preview (first 5 data rows):"),
csvTable,
new Label("Column Mapping:"),
coordHBox,
analyzeButton
)
tab1.setContent(new ScrollPane(vbox1))
// ===================== TAB 2 =====================
def tab2 = new Tab("Point Loading")
tab2.setClosable(false)
def vbox2 = new VBox(10)
vbox2.setPadding(new Insets(15))
def label2 = new Label("Point Loading & Gene Selection")
label2.setStyle("-fx-font-size: 14px; -fx-font-weight: bold;")
// ---- Load Points (first N) line ---------------------------------
def firstLineHBox = new HBox(10)
firstLineHBox.setAlignment(Pos.CENTER_LEFT)
def loadPointsButton = new Button("Load Points")
loadPointsButton.setPrefWidth(120)
def pointsLabel = new Label("Points number:")
def pointsField = new TextField("10000")
pointsField.setPrefWidth(80)
firstLineHBox.getChildren().addAll(loadPointsButton, pointsLabel, pointsField)
// ---- 10 gene‑dropdown + colour pickers -------------------------
def geneSelectionVBox = new VBox(5)
geneSelectionVBox.setPadding(new Insets(10,0,10,0))
for (int i = 0; i < 10; i++) {
def line = new HBox(10)
line.setAlignment(Pos.CENTER_LEFT)
def dd = new ComboBox<String>()
dd.setPrefWidth(150)
dd.getItems().add("none")
geneDropdowns << dd
def cp = new ColorPicker(Color.RED)
colorPickers << cp
def lbl = new Label("Gene ${i+1}:")
lbl.setPrefWidth(60)
line.getChildren().addAll(lbl, dd, cp)
geneSelectionVBox.getChildren().add(line)
}
// ---- Load selected genes button --------------------------------
def loadSelectedGenesButton = new Button("Load Selected Genes")
loadSelectedGenesButton.setPrefWidth(180)
// ---- Progress UI ------------------------------------------------
progressBar = new ProgressBar(0)
progressBar.setPrefWidth(300)
progressBar.setVisible(false)
def progressLabel = new Label("")
progressLabel.setVisible(false)
// -----------------------------------------------------------------
// ---- LOAD POINTS (first N) – unchanged except it now works on the combined data
// -----------------------------------------------------------------
loadPointsButton.setOnAction({ e ->
if (currentCsvData.isEmpty() || currentHeaders.isEmpty()) {
showAlert("Error", "Please load CSV data in Tab 1 first.")
return
}
if (geneColumnIndex == -1 || xColumnIndex == -1 || yColumnIndex == -1) {
showAlert("Error", "Please configure column mappings in Tab 1 first.")
return
}
try {
def pointsNumber = Integer.parseInt(pointsField.text)
// pixel size (microns per pixel)
def img = getCurrentImageData()
if (!img) { showAlert("Error","No image open."); return }
def pixX = img.getServer().getPixelCalibration().getAveragedPixelSizeMicrons()
def pixY = img.getServer().getPixelCalibration().getPixelHeight()
// plane – we reuse your helper (or use getSelectedROI())
def plane = ImagePlane.getPlane(getSelectedROI())
progressBar.visible = true
progressLabel.visible = true
progressLabel.text = "Loading first ${pointsNumber} points..."
progressBar.progress = 0.1
def pointsByGene = [:] as Map<String, List<Map>>
def count = 0
currentCsvData.each { row ->
if (count >= pointsNumber) return
if (row.size() > Math.max(geneColumnIndex, Math.max(xColumnIndex, yColumnIndex))) {
try {
def gene = row[geneColumnIndex].trim()
def xRaw = Double.parseDouble(row[xColumnIndex].trim())
def yRaw = Double.parseDouble(row[yColumnIndex].trim())
def x = (dimensionUnit == "micron") ? xRaw / pixX : xRaw
def y = (dimensionUnit == "micron") ? yRaw / pixY : yRaw
pointsByGene.computeIfAbsent(gene){[]}.add([x:x, y:y])
count++
} catch (NumberFormatException ignored) {}
}
}
progressBar.progress = 0.5
if (!pointsByGene) {
progressLabel.text = "No valid points found"
showAlert("Warning","No valid coordinate data found in CSV.")
return
}
// create detections per gene
def totalCreated = 0
def processed = 0
pointsByGene.each { gene, pts ->
def xs = pts.collect{it.x} as double[]
def ys = pts.collect{it.y} as double[]
def roi = ROIs.createPointsROI(xs, ys, plane)
def pc = getPathClass(gene) ?: createPathClass(gene)
def det = PathObjects.createDetectionObject(roi, pc)
det.setName("${gene} (${pts.size()} points)")
addObject(det)
totalCreated += pts.size()
processed++
progressBar.progress = 0.5 + (processed / pointsByGene.size()) * 0.5
}
progressBar.progress = 1.0
progressLabel.text = "Completed: ${totalCreated} points loaded across ${pointsByGene.size()} genes"
fireHierarchyUpdate()
// hide progress after a short pause
Platform.runLater {
Thread.sleep(2000)
progressBar.visible = false
progressLabel.visible = false
}
} catch (NumberFormatException nfe) {
showAlert("Error","Invalid points number.")
progressBar.visible = false
progressLabel.visible = false
} catch (Exception ex) {
showAlert("Error","Failed to load points: ${ex.message}")
ex.printStackTrace()
progressBar.visible = false
progressLabel.visible = false
}
})
// -----------------------------------------------------------------
// ---- LOAD SELECTED GENES (all points) – unchanged except uses combined data
// -----------------------------------------------------------------
loadSelectedGenesButton.setOnAction({ e ->
if (currentCsvData.isEmpty() || currentHeaders.isEmpty()) {
showAlert("Error","Please load CSV data in Tab 1 first."); return
}
if (geneColumnIndex == -1 || xColumnIndex == -1 || yColumnIndex == -1) {
showAlert("Error","Please configure column mappings in Tab 1 first."); return
}
// collect which genes the user actually chose
def selected = []
geneDropdowns.eachWithIndex { dd, i ->
def val = dd.value
if (val && val != "none") {
selected << [gene:val, color:colorPickers[i].value, idx:i]
}
}
if (!selected) {
showAlert("Warning","No genes selected."); return
}
try {
def img = getCurrentImageData()
if (!img) { showAlert("Error","No image open."); return }
def pixX = img.getServer().getPixelCalibration().getAveragedPixelSizeMicrons()
def pixY = img.getServer().getPixelCalibration().getPixelHeight()
def plane = ImagePlane.getPlane(getSelectedROI())
progressBar.visible = true
progressLabel.visible = true
progressBar.progress = 0
selected.eachWithIndex { sel, idx ->
progressBar.progress = idx / selected.size()
progressLabel.text = "Processing ${sel.gene} (${idx+1}/${selected.size()})"
// collect **all** points for this gene
def pts = []
currentCsvData.each { row ->
if (row.size() > Math.max(geneColumnIndex, Math.max(xColumnIndex, yColumnIndex))) {
if (row[geneColumnIndex].trim() == sel.gene) {
try {
def xRaw = Double.parseDouble(row[xColumnIndex].trim())
def yRaw = Double.parseDouble(row[yColumnIndex].trim())
def x = (dimensionUnit == "micron") ? xRaw / pixX : xRaw
def y = (dimensionUnit == "micron") ? yRaw / pixY : yRaw
pts << [x:x, y:y]
} catch (NumberFormatException ignored) {}
}
}
}
if (!pts) {
println "No points found for gene ${sel.gene}"
return
}
def xs = pts.collect{it.x} as double[]
def ys = pts.collect{it.y} as double[]
def roi = ROIs.createPointsROI(xs, ys, plane)
def pc = getPathClass(sel.gene) ?: createPathClass(sel.gene)
// set colour from the colour picker
def r = (int)(sel.color.red * 255)
def g = (int)(sel.color.green * 255)
def b = (int)(sel.color.blue * 255)
pc.setColor(r,g,b)
def det = PathObjects.createDetectionObject(roi, pc)
det.setName("${sel.gene} (${pts.size()} points)")
addObject(det)
println "Created detection for ${sel.gene}: ${pts.size()} points (colour RGB(${r},${g},${b}))"
}
progressBar.progress = 1.0
progressLabel.text = "Completed: ${selected.size()} genes processed"
fireHierarchyUpdate()
Platform.runLater {
Thread.sleep(2000)
progressBar.visible = false
progressLabel.visible = false
}
} catch (Exception ex) {
showAlert("Error","Failed to load gene detections: ${ex.message}")
ex.printStackTrace()
progressBar.visible = false
progressLabel.visible = false
}
})
// ------------------- Assemble Tab 2 ----------------------------
vbox2.getChildren().addAll(
label2,
new Separator(),
firstLineHBox,
new Label("Gene Selection & Colors:"),
geneSelectionVBox,
loadSelectedGenesButton,
new Separator(),
progressLabel,
progressBar
)
tab2.setContent(new ScrollPane(vbox2))
// ------------------- Add tabs & show -------------------------
tabPane.getTabs().addAll(tab1, tab2)
def scene = new Scene(tabPane, 700, 750)
stage.scene = scene
stage.show()
stage.centerOnScreen()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment