Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
270 changes: 188 additions & 82 deletions src/main/java/org/igv/bedpe/ContactMapView.java

Large diffs are not rendered by default.

135 changes: 135 additions & 0 deletions src/main/java/org/igv/bedpe/HicInteractionTrack.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package org.igv.bedpe;

import org.igv.hic.HicFile;
import org.igv.renderer.ContinuousColorScale;
import org.igv.track.RenderContext;
import org.igv.track.TrackClickEvent;
import org.igv.ui.panel.FrameManager;
import org.igv.ui.panel.IGVPopupMenu;
import org.igv.ui.panel.ReferenceFrame;
import org.igv.util.ResourceLocator;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import javax.swing.*;
import java.awt.*;
import java.util.List;

/**
* Subclass of InteractionTrack for HiC format files.
* Handles HiC-specific behavior like normalization, contact map views, and zoom-based filtering.
*/
public class HicInteractionTrack extends InteractionTrack {

public HicInteractionTrack() {
super();
}

public HicInteractionTrack(ResourceLocator locator, HicSource source) {
super(locator, source);
// HiC-specific defaults
maxFeatureCount = 5000;
graphType = GraphType.NESTED_ARC;
useScore = true;
setColor(Color.red);
}

@Override
protected List<BedPE> filterFeaturesForZoom(List<BedPE> features, LoadedInterval interval, ReferenceFrame referenceFrame) {
// In HiC mode we limit interactions to those in view plus a margin of one screen width to either side.
// If zooming in this means we have to filter the features from the previous zoom level that are outside
// of this range. Not doing so leads to inconsistent rendering when loading for the current zoom
// completes and repaints.
if (interval.zoom() < referenceFrame.getZoom()) {
int start = (int) referenceFrame.getOrigin();
int end = (int) referenceFrame.getEnd();
int w = (end - start);
int finalStart = start - w;
int finalEnd = end + w;
return features.stream()
.takeWhile(f -> f.getStart() <= finalEnd)
.filter(f -> f.getEnd() >= finalStart)
.toList();
}
return features;
}

@Override
protected void addFormatSpecificMenuItems(IGVPopupMenu menu, TrackClickEvent te) {
// Add normalization options for HiC tracks
List<String> normalizationTypes = featureSource.getNormalizationTypes();
if (normalizationTypes != null && normalizationTypes.size() > 1) {
menu.addSeparator();
menu.add(new JLabel("<html><b>Normalization</b>"));
ButtonGroup normGroup = new ButtonGroup();
for (String type : normalizationTypes) {
String label = normalizationLabels.getOrDefault(type, type);
JRadioButtonMenuItem normItem = new JRadioButtonMenuItem(label);
normItem.setSelected(type.equals(normalization));
normItem.addActionListener(e -> {
this.normalization = type;
if (contactMapView != null) {
contactMapView.setNormalization(type);
}
this.repaint();
});
normGroup.add(normItem);
menu.add(normItem);
}
}

menu.addSeparator();
JMenuItem mapItem = new JMenuItem("Contact Map View...");
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The menu item text says "Contact Map View..." with an ellipsis, but the old code on line 482 in the InteractionTrack.java said "Open Contact Map View" without ellipsis. The ellipsis typically indicates that clicking the menu item will open a dialog or additional window for more input. In this case, it directly opens the contact map view without additional prompts, so the ellipsis may be misleading. Consider using "Open Contact Map View" or "Contact Map View" without the ellipsis.

Suggested change
JMenuItem mapItem = new JMenuItem("Contact Map View...");
JMenuItem mapItem = new JMenuItem("Contact Map View");

Copilot uses AI. Check for mistakes.
mapItem.setEnabled(contactMapView == null && !FrameManager.isGeneListMode());
mapItem.addActionListener(e -> {
ReferenceFrame frame = te.getFrame() != null ? te.getFrame() : FrameManager.getDefaultFrame();
if (contactMapView == null) {
ContinuousColorScale colorScale = this.getColorScale();
HicFile hicFile = ((HicSource) featureSource).getHicFile();
ContactMapView.showPopup(this, hicFile, normalization, frame, colorScale.getMaxColor());
}
});
menu.add(mapItem);
}

@Override
protected boolean supportsGraphTypeSelection() {
return false;
}

@Override
protected boolean supportsCircularView() {
return false;
}

@Override
protected boolean supportsAutoscaleMenu() {
return false;
}

@Override
protected boolean supportsFeatureWindowMenu() {
return false;
}

@Override
public void marshalXML(Document document, Element element) {
super.marshalXML(document, element);

String nviString = ((HicSource) featureSource).getNVIString();
if (nviString != null) {
element.setAttribute("nvi", nviString);
}
}

@Override
public void unmarshalXML(Element element, Integer version) {
super.unmarshalXML(element, version);

if (element.hasAttribute("nvi")) {
String nviString = element.getAttribute("nvi");
((HicSource) featureSource).setNVIString(nviString);
}
}
}

56 changes: 14 additions & 42 deletions src/main/java/org/igv/bedpe/HicSource.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
import org.igv.hic.Region;

import java.io.IOException;
import java.util.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

/**
Expand Down Expand Up @@ -36,7 +39,7 @@ public List<BedPE> getFeatures(String chr, int start, int end, double bpPerPixel

final int binSize = hicFile.getBinSize(chr, bpPerPixel);

List<ContactRecord> records = getRecords(chr, start, end, binSize);
List<ContactRecord> records = getRecords(chr, start, end, binSize, normalization);

if (records.isEmpty()) {
return Collections.emptyList();
Expand All @@ -52,15 +55,15 @@ public List<BedPE> getFeatures(String chr, int start, int end, double bpPerPixel
int b1 = rec.bin1();
int b2 = rec.bin2();
if (Math.abs(b1 - b2) > this.binThreshold) {
values.add(rec.counts());
values.add(rec.normCounts());
}
if (b1 < binMin) binMin = b1;
if (b2 < binMin) binMin = b2;
if (b1 > binMax) binMax = b1;
if (b2 > binMax) binMax = b2;
}

if(values.isEmpty()) {
if (values.isEmpty()) {
return Collections.emptyList();
}

Expand All @@ -78,7 +81,7 @@ public List<BedPE> getFeatures(String chr, int start, int end, double bpPerPixel
int bin2 = rec.bin2();
if (Math.abs(bin1 - bin2) <= this.binThreshold) continue;

float counts = rec.counts();
float counts = rec.normCounts();
if (counts > threshold) {
significantRecords.add(rec);
} else if (counts == threshold) {
Expand Down Expand Up @@ -107,48 +110,17 @@ public List<BedPE> getFeatures(String chr, int start, int end, double bpPerPixel
}
}

double[] normVector = null;
boolean useNormalization = normalization != null && !"NONE".equals(normalization);
if (useNormalization) {
NormalizationVector nv = hicFile.getNormalizationVector(normalization, chr, "BP", binSize);
if (nv == null) {
useNormalization = false;
} else {
normVector = nv.getValues(binMin, binMax);
if (normVector == null) {
useNormalization = false;
}
}
}

// Convert contact records to features
List<BedPE> features = new ArrayList<>();
String c = getChromosomeNameFromGenome(chr);
for (ContactRecord rec : significantRecords) {

int bin1 = rec.bin1();
int bin2 = rec.bin2();
float value = rec.counts();
if (useNormalization) {
double nvnv = 1;
try {
nvnv = normVector[bin1 - binMin] * normVector[bin2 - binMin];
} catch (Exception e) {
throw new RuntimeException(e);
}
if (!Double.isNaN(nvnv)) {
value /= nvnv;
} else {
continue;
}
}

int start1 = bin1 * binSize;
int start1 = rec.bin1() * binSize;
int end1 = start1 + binSize;
int start2 = bin2 * binSize;
int start2 = rec.bin2() * binSize;
int end2 = start2 + binSize;

HicFeature f = new HicFeature(c, start1, end1, c, start2, end2, rec.counts(), value);
HicFeature f = new HicFeature(c, start1, end1, c, start2, end2, rec.counts(), rec.normCounts());
int score = (max > min) ? (int) Math.round(200 + Math.min(Math.max((f.getValue() - min) / (max - min), 0), 1) * 600) : 800;
f.setScore(score);
features.add(f);
Expand Down Expand Up @@ -181,19 +153,19 @@ public boolean hasNormalizationVector(String type, String chr, double bpPerPixel
* @return
* @throws IOException
*/
private List<ContactRecord> getRecords(String chr, int start, int end, int binSize) throws IOException {
private List<ContactRecord> getRecords(String chr, int start, int end, int binSize, String normalization) throws IOException {

final Region region1 = new Region(chr, start, end);
List<ContactRecord> records = hicFile.getContactRecords(
region1,
region1,
"BP",
binSize,
"NONE",
normalization,
false
);

if(start > 0) {
if (start > 0) {
Region adjacent = new Region(chr, Math.max(0, start - (end - start)), start);
List<ContactRecord> adjacentRecords = hicFile.getContactRecords(region1, adjacent, "BP", binSize, "NONE", false);
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation comment states that normalization is applied, but the adjacent region records are fetched with "NONE" normalization on line 170. This seems inconsistent - if normalization is applied to the main region, shouldn't it also be applied to the adjacent region for consistency?

Copilot uses AI. Check for mistakes.
records.addAll(adjacentRecords);
Expand Down
Loading
Loading