/* * The MIT License (MIT) * * Copyright (c) 2007-2015 Broad Institute * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.broad.igv.ui; import org.broad.igv.feature.IExon; import org.broad.igv.prefs.Constants; import org.broad.igv.prefs.PreferencesManager; import org.broad.igv.renderer.SashimiJunctionRenderer; import org.broad.igv.sam.*; import org.broad.igv.track.*; import org.broad.igv.ui.color.ColorPalette; import org.broad.igv.ui.color.ColorUtilities; import org.broad.igv.event.IGVEventBus; import org.broad.igv.event.IGVEventObserver; import org.broad.igv.event.ViewChange; import org.broad.igv.ui.panel.*; import org.broad.igv.ui.util.UIUtilities; import javax.swing.*; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.geom.Rectangle2D; import java.io.File; import java.util.*; import java.util.List; /** * Window for displaying sashimi style junction plot * See http://genes.mit.edu/burgelab/miso/docs/sashimi.html * <p/> * User: jacob * Date: 2013-Jan-11 */ public class SashimiPlot extends JFrame implements IGVEventObserver { private List<SpliceJunctionTrack> spliceJunctionTracks; private ReferenceFrame frame; private IGVEventBus eventBus; /** * The minimum allowed origin of the frame. We set scrolling * limits based on initialization */ private final double minOrigin; /** * The maximum allow end of the frame. We set scrolling * limits based on initialization */ private final double maxEnd; private static final List<Color> plotColors; static { ColorPalette palette = ColorUtilities.getDefaultPalette(); plotColors = Arrays.asList(palette.getColors()); } public SashimiPlot(ReferenceFrame iframe, Collection<? extends AlignmentTrack> alignmentTracks, FeatureTrack geneTrack) { getGlassPane().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); int minJunctionCoverage = PreferencesManager.getPreferences().getAsInt(Constants.SAM_JUNCTION_MIN_COVERAGE); this.eventBus = new IGVEventBus(); this.frame = new ReferenceFrame(iframe, eventBus); minOrigin = this.frame.getOrigin(); maxEnd = this.frame.getEnd(); initSize(frame.getWidthInPixels()); BoxLayout boxLayout = new BoxLayout(getContentPane(), BoxLayout.Y_AXIS); getContentPane().setLayout(boxLayout); //Add control elements to the top getContentPane().add(generateControlPanel(this.frame)); spliceJunctionTracks = new ArrayList<>(alignmentTracks.size()); int colorInd = 0; eventBus.subscribe(ViewChange.class, this); for (AlignmentTrack alignmentTrack : alignmentTracks) { //AlignmentDataManager oldDataManager = alignmentTrack.getDataManager(); AlignmentDataManager dataManager = alignmentTrack.getDataManager(); SpliceJunctionTrack spliceJunctionTrack = new SpliceJunctionTrack(alignmentTrack.getResourceLocator(), alignmentTrack.getName(), dataManager, null, SpliceJunctionTrack.StrandOption.COMBINE); // Override expand/collpase setting -- expanded sashimi plots make no sense spliceJunctionTrack.setDisplayMode(Track.DisplayMode.COLLAPSED); spliceJunctionTrack.setRendererClass(SashimiJunctionRenderer.class); Color color = plotColors.get(colorInd); colorInd = (colorInd + 1) % plotColors.size(); spliceJunctionTrack.setColor(color); TrackComponent<SpliceJunctionTrack> trackComponent = new TrackComponent<SpliceJunctionTrack>(frame, spliceJunctionTrack); trackComponent.originalFrame = iframe; initSpliceJunctionComponent(trackComponent, dataManager,dataManager.getCoverageTrack(), minJunctionCoverage); getContentPane().add(trackComponent); spliceJunctionTracks.add(spliceJunctionTrack); spliceJunctionTrack.load(iframe); // <= Must "load" tracks with frame of alignment track (actually just fetches from cache) } Axis axis = createAxis(frame); getContentPane().add(axis); SelectableFeatureTrack geneTrackClone = new SelectableFeatureTrack(geneTrack); TrackComponent<SelectableFeatureTrack> geneComponent = new TrackComponent<SelectableFeatureTrack>(frame, geneTrackClone); getContentPane().add(geneComponent); initGeneComponent(frame.getWidthInPixels(), geneComponent, geneTrackClone); validate(); } private Component generateControlPanel(ReferenceFrame frame) { JPanel controlPanel = new JPanel(); ZoomSliderPanel zoomSliderPanel = new ZoomSliderPanel(frame); zoomSliderPanel.setMinZoomLevel(frame.getZoom()); zoomSliderPanel.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { SashimiPlot.this.repaint(); } }); Dimension controlSize = new Dimension(200, 30); controlPanel.add(zoomSliderPanel); setFixedSize(zoomSliderPanel, controlSize); Dimension panelSize = controlSize; setFixedSize(controlPanel, panelSize); BoxLayout layout = new BoxLayout(controlPanel, BoxLayout.X_AXIS); controlPanel.setLayout(layout); return controlPanel; } private static void setFixedSize(Component component, Dimension dimension) { component.setPreferredSize(dimension); component.setMinimumSize(dimension); component.setMaximumSize(dimension); } private void initSize(int width) { setSize(width, 500); } private Axis createAxis(ReferenceFrame frame) { Axis axis = new Axis(frame); Dimension maxDim = new Dimension(Integer.MAX_VALUE, 25); axis.setMaximumSize(maxDim); Dimension prefDim = new Dimension(maxDim); prefDim.setSize(frame.getWidthInPixels(), prefDim.height); axis.setPreferredSize(prefDim); return axis; } private void initGeneComponent(int prefWidth, TrackComponent<SelectableFeatureTrack> geneComponent, FeatureTrack geneTrack) { geneTrack.setDisplayMode(Track.DisplayMode.SQUISHED); geneTrack.clearPackedFeatures(); RenderContext context = new RenderContext(geneComponent, null, frame, null); geneTrack.load(context.getReferenceFrame()); Dimension maxGeneDim = new Dimension(Integer.MAX_VALUE, geneTrack.getNumberOfFeatureLevels() * geneTrack.getSquishedRowHeight() + 10); geneComponent.setMaximumSize(maxGeneDim); Dimension prefGeneDim = new Dimension(maxGeneDim); prefGeneDim.setSize(prefWidth, prefGeneDim.height); geneComponent.setPreferredSize(prefGeneDim); GeneTrackMouseAdapter ad2 = new GeneTrackMouseAdapter(geneComponent); geneComponent.addMouseListener(ad2); geneComponent.addMouseMotionListener(ad2); } private void initSpliceJunctionComponent(TrackComponent<SpliceJunctionTrack> trackComponent, AlignmentDataManager dataManager, CoverageTrack coverageTrack, int minJunctionCoverage) { JunctionTrackMouseAdapter ad1 = new JunctionTrackMouseAdapter(trackComponent); trackComponent.addMouseListener(ad1); trackComponent.addMouseMotionListener(ad1); getRenderer(trackComponent.track).setDataManager(dataManager); getRenderer(trackComponent.track).setCoverageTrack(coverageTrack); getRenderer(trackComponent.track).getCoverageTrack().rescale(trackComponent.originalFrame); dataManager.setMinJunctionCoverage(minJunctionCoverage); getRenderer(trackComponent.track).setBackground(getBackground()); } private SashimiJunctionRenderer getRenderer(SpliceJunctionTrack spliceJunctionTrack) { return (SashimiJunctionRenderer) spliceJunctionTrack.getRenderer(); } @Override public void receiveEvent(Object event) { repaint(); } /** * Should consider using this elsewhere. Single component * which contains a single track */ private static class TrackComponent<T extends Track> extends JComponent { private T track; private ReferenceFrame frame; private String toolTipText = null; public ReferenceFrame originalFrame; public TrackComponent(ReferenceFrame frame, T track) { this.frame = frame; this.track = track; } public void updateToolTipText(TrackClickEvent tce) { toolTipText = track.getValueStringAt(tce.getFrame().getChrName(), tce.getChromosomePosition(), tce.getMouseEvent().getX(), tce.getMouseEvent().getY(), tce.getFrame()); toolTipText = "<html>" + toolTipText; setToolTipText(toolTipText); } @Override public void paintComponent(Graphics g) { super.paintComponent(g); Rectangle visibleRect = getVisibleRect(); RenderContext context = new RenderContext(this, (Graphics2D) g, frame, visibleRect); track.render(context, visibleRect); } } /** * Set the minimum junction coverage, per trac,k and is not persistent * <p/> * Our "Set Max Junction Coverage Range" just changes the view scaling, it doesn't * filter anything, which is different behavior than the minimum. This might be confusing. * * @param trackComponent * @param newMinJunctionCoverage */ private void setMinJunctionCoverage(TrackComponent<SpliceJunctionTrack> trackComponent, int newMinJunctionCoverage) { AlignmentDataManager dataManager = getRenderer(trackComponent.track).getDataManager(); dataManager.setMinJunctionCoverage(newMinJunctionCoverage); trackComponent.track.clear(); trackComponent.repaint(); } /** * Set the max coverage depth, which is a graphical scaling parameter for determining how * thick the junction arcs will be * * @param trackComponent * @param newMaxDepth */ private void setMaxCoverageDepth(TrackComponent<SpliceJunctionTrack> trackComponent, int newMaxDepth) { getRenderer(trackComponent.track).setMaxDepth(newMaxDepth); repaint(); } private class JunctionTrackMouseAdapter extends TrackComponentMouseAdapter<SpliceJunctionTrack> { JunctionTrackMouseAdapter(TrackComponent<SpliceJunctionTrack> trackComponent) { super(trackComponent); } @Override protected void handleDataClick(MouseEvent e) { //Show data of some sort? } @Override protected IGVPopupMenu getPopupMenu(MouseEvent e) { IGVPopupMenu menu = new IGVPopupMenu(); final JCheckBoxMenuItem showCoverageData = new JCheckBoxMenuItem("Show Exon Coverage Data"); showCoverageData.setSelected(PreferencesManager.getPreferences().getAsBoolean(Constants.SASHIMI_SHOW_COVERAGE)); showCoverageData.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { PreferencesManager.getPreferences().put(Constants.SASHIMI_SHOW_COVERAGE, showCoverageData.isSelected()); SashimiPlot.this.repaint(); } }); CoverageTrack covTrack = getRenderer(this.trackComponent.track).getCoverageTrack(); JMenuItem setCoverageDataRange = CoverageTrack.addDataRangeItem(SashimiPlot.this, null, Arrays.asList(covTrack)); setCoverageDataRange.setText("Set Exon Coverage Max"); JMenuItem maxJunctionCoverageRange = new JMenuItem("Set Junction Coverage Max"); maxJunctionCoverageRange.setToolTipText("The thickness of each line will be proportional to the coverage, up until this value"); maxJunctionCoverageRange.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { String input = JOptionPane.showInputDialog("Set Max Junction Coverage", getRenderer(trackComponent.track).getMaxDepth()); if (input == null || input.length() == 0) return; try { int newMaxDepth = Integer.parseInt(input); setMaxCoverageDepth(JunctionTrackMouseAdapter.this.trackComponent, newMaxDepth); } catch (NumberFormatException ex) { JOptionPane.showMessageDialog(SashimiPlot.this, input + " is not an integer"); } } }); JMenuItem minJunctionCoverage = new JMenuItem("Set Junction Coverage Min"); minJunctionCoverage.setToolTipText("Junctions below this threshold will be removed from view"); minJunctionCoverage.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { AlignmentDataManager dataManager = getRenderer(trackComponent.track).getDataManager(); SpliceJunctionHelper.LoadOptions loadOptions = dataManager.getSpliceJunctionLoadOptions(); String input = JOptionPane.showInputDialog("Set Minimum Junction Coverage", loadOptions.minJunctionCoverage); if (input == null || input.length() == 0) return; try { int newMinJunctionCoverage = Integer.parseInt(input); setMinJunctionCoverage(JunctionTrackMouseAdapter.this.trackComponent, newMinJunctionCoverage); } catch (NumberFormatException ex) { JOptionPane.showMessageDialog(SashimiPlot.this, input + " is not an integer"); } } }); JMenuItem colorItem = new JMenuItem("Set Color"); colorItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { Color color = UIUtilities.showColorChooserDialog( "Select Track Color", trackComponent.track.getColor()); SashimiPlot.this.toFront(); if (color == null) return; trackComponent.track.setColor(color); trackComponent.repaint(); } }); ButtonGroup shapeGroup = new ButtonGroup(); JRadioButtonMenuItem textShape = getJunctionCoverageRadioButton("Text", SashimiJunctionRenderer.ShapeType.TEXT); textShape.setToolTipText("Show junction coverage as text"); shapeGroup.add(textShape); JRadioButtonMenuItem circleShape = getJunctionCoverageRadioButton("Circle", SashimiJunctionRenderer.ShapeType.CIRCLE); circleShape.setToolTipText("Show junction coverage as a circle"); shapeGroup.add(circleShape); JRadioButtonMenuItem ellipseShape = getJunctionCoverageRadioButton("Ellipse", SashimiJunctionRenderer.ShapeType.ELLIPSE); ellipseShape.setToolTipText("Show junction coverage as an ellipse"); shapeGroup.add(ellipseShape); JRadioButtonMenuItem noShape = getJunctionCoverageRadioButton("None", SashimiJunctionRenderer.ShapeType.NONE); ellipseShape.setToolTipText("Show junction coverage as an ellipse"); shapeGroup.add(ellipseShape); ButtonGroup strandGroup = new ButtonGroup(); JRadioButtonMenuItem combineStrands = getStrandRadioButton("Combine Strands", SpliceJunctionTrack.StrandOption.COMBINE); combineStrands.setToolTipText("Combine junctions from both strands -- best for non-strand preserving libraries."); strandGroup.add(combineStrands); // JRadioButtonMenuItem bothStrands = getStrandRadioButton("Both Strands", SpliceJunctionTrack.StrandOption.BOTH); // strandGroup.add(bothStrands); // menu.add(bothStrands); JRadioButtonMenuItem plusStrand = getStrandRadioButton("Forward Strand", SpliceJunctionTrack.StrandOption.FORWARD); plusStrand.setToolTipText("Show only junctions on the forward read strand (of first-in-pair for paired reads)"); strandGroup.add(plusStrand); JRadioButtonMenuItem minusStrand = getStrandRadioButton("Reverse Strand", SpliceJunctionTrack.StrandOption.REVERSE); plusStrand.setToolTipText("Show only junctions on the reverse read strand (of first-in-pair for paired reads)"); strandGroup.add(minusStrand); JMenuItem saveImageItem = new JMenuItem("Save Image..."); saveImageItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { File defaultFile = new File("Sashimi.png"); IGV.getInstance().createSnapshot(SashimiPlot.this.getContentPane(), defaultFile); } }); // Coverage ranges -- these apply to current plot only menu.add(new JLabel("Junction Coverage Display")); menu.add(setCoverageDataRange); menu.add(minJunctionCoverage); menu.add(maxJunctionCoverageRange); menu.add(colorItem); // Coverage data -- applies to all plots menu.add(showCoverageData); // Shape options -- all plots menu.addSeparator(); menu.add(textShape); menu.add(circleShape); // menu.add(ellipseShape); menu.add(noShape); // Strand options -- applies to all plots menu.addSeparator(); menu.add(combineStrands); menu.add(plusStrand); menu.add(minusStrand); menu.addSeparator(); menu.add(saveImageItem); return menu; } private JRadioButtonMenuItem getJunctionCoverageRadioButton(String label, final SashimiJunctionRenderer.ShapeType shapeType) { JRadioButtonMenuItem item = new JRadioButtonMenuItem(label); item.setSelected(SashimiJunctionRenderer.getShapeType() == shapeType); item.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { SashimiJunctionRenderer.setShapeType(shapeType); SashimiPlot.this.repaint(); } }); return item; } private JRadioButtonMenuItem getStrandRadioButton(String label, final SpliceJunctionTrack.StrandOption option) { JRadioButtonMenuItem item = new JRadioButtonMenuItem(label); item.setSelected(SpliceJunctionTrack.getStrandOption() == option); item.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { SpliceJunctionTrack.setStrandOption(option); for (SpliceJunctionTrack t : spliceJunctionTracks) { t.clear(); } repaint(); } }); return item; } } private class GeneTrackMouseAdapter extends TrackComponentMouseAdapter<SelectableFeatureTrack> { GeneTrackMouseAdapter(TrackComponent<SelectableFeatureTrack> trackComponent) { super(trackComponent); } @Override protected void handleDataClick(MouseEvent e) { trackComponent.track.handleDataClick(createTrackClickEvent(e)); Set<IExon> selectedExon = trackComponent.track.getSelectedExons(); for (SpliceJunctionTrack spliceTrack : spliceJunctionTracks) { getRenderer(spliceTrack).setSelectedExons(selectedExon); } repaint(); } @Override public void mouseMoved(MouseEvent e) { trackComponent.updateToolTipText(createTrackClickEvent(e)); } @Override protected IGVPopupMenu getPopupMenu(MouseEvent e) { IGVPopupMenu menu = new IGVPopupMenu(); TrackMenuUtils.addDisplayModeItems(Arrays.<Track>asList(trackComponent.track), menu); menu.addPopupMenuListener(new RepaintPopupMenuListener(SashimiPlot.this)); return menu; } } private abstract class TrackComponentMouseAdapter<T extends Track> extends MouseAdapter { protected TrackComponent<T> trackComponent; protected PanTool currentTool; TrackComponentMouseAdapter(TrackComponent<T> trackComponent) { this.trackComponent = trackComponent; currentTool = new PanTool(null); currentTool.setReferenceFrame(this.trackComponent.frame); } @Override public void mouseDragged(MouseEvent e) { if (currentTool.getLastMousePoint() == null) { //This shouldn't happen, but does occasionally return; } double diff = e.getX() - currentTool.getLastMousePoint().getX(); // diff > 0 means moving mouse to the right, which drags the frame towards the negative direction boolean hitBounds = SashimiPlot.this.frame.getOrigin() <= minOrigin && diff > 0; hitBounds |= SashimiPlot.this.frame.getEnd() >= maxEnd && diff < 0; if (!hitBounds) { currentTool.mouseDragged(e); repaint(); } } @Override public void mouseReleased(MouseEvent e) { if (e.isPopupTrigger()) { doPopupMenu(e); } else { currentTool.mouseReleased(e); } } @Override public void mousePressed(MouseEvent e) { if (e.isPopupTrigger()) { doPopupMenu(e); } else { currentTool.mousePressed(e); super.mousePressed(e); } } protected void doPopupMenu(MouseEvent e) { IGVPopupMenu menu = getPopupMenu(e); if (menu != null) menu.show(trackComponent, e.getX(), e.getY()); } protected TrackClickEvent createTrackClickEvent(MouseEvent e) { return new TrackClickEvent(e, trackComponent.frame); } @Override public void mouseClicked(MouseEvent e) { if (e.isPopupTrigger()) { doPopupMenu(e); return; } currentTool.mouseClicked(e); handleDataClick(e); } /** * Essentially left click * * @param e */ protected abstract void handleDataClick(MouseEvent e); /** * Essentially right click * * @param e * @return */ protected abstract IGVPopupMenu getPopupMenu(MouseEvent e); } /** * Show SashimiPlot window, or change settings of {@code currentWindow} * * @param sashimiPlot */ public static void getSashimiPlot(SashimiPlot sashimiPlot) { if (sashimiPlot == null) { FeatureTrack geneTrack = null; List<FeatureTrack> nonJunctionTracks = new ArrayList(IGV.getInstance().getFeatureTracks()); Iterator iter = nonJunctionTracks.iterator(); while(iter.hasNext()) { if(iter.next() instanceof SpliceJunctionTrack) iter.remove(); } if (nonJunctionTracks.size() == 1) { geneTrack = nonJunctionTracks.get(0); } else { FeatureTrackSelectionDialog dlg = new FeatureTrackSelectionDialog(IGV.getMainFrame(), nonJunctionTracks); dlg.setTitle("Select Gene Track"); dlg.setVisible(true); if (dlg.getIsCancelled()) return; geneTrack = dlg.getSelectedTrack(); } Collection<AlignmentTrack> alignmentTracks = new ArrayList<AlignmentTrack>(); for (Track track : IGV.getInstance().getAllTracks()) { if (track instanceof AlignmentTrack) { alignmentTracks.add((AlignmentTrack) track); } } if (alignmentTracks.size() > 1) { TrackSelectionDialog<AlignmentTrack> alDlg = new TrackSelectionDialog<AlignmentTrack>(IGV.getMainFrame(), TrackSelectionDialog.SelectionMode.MULTIPLE, alignmentTracks); alDlg.setTitle("Select Alignment Tracks"); alDlg.setVisible(true); if (alDlg.getIsCancelled()) return; alignmentTracks = alDlg.getSelectedTracks(); } sashimiPlot = new SashimiPlot(FrameManager.getDefaultFrame(), alignmentTracks, geneTrack); //sashimiPlot.setShapeType(shapeType); sashimiPlot.setVisible(true); } else { //sashimiPlot.setShapeType(shapeType); } } private static class RepaintPopupMenuListener implements PopupMenuListener { Component component; RepaintPopupMenuListener(Component component) { this.component = component; } @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) { component.repaint(); } @Override public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { component.repaint(); } @Override public void popupMenuCanceled(PopupMenuEvent e) { component.repaint(); } } private static class Axis extends JComponent { private ReferenceFrame frame; Axis(ReferenceFrame frame) { this.frame = frame; } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); Rectangle visibleRect = getVisibleRect(); RenderContext context = new RenderContext(this, (Graphics2D) g, frame, visibleRect); drawGenomicAxis(context, visibleRect); } /** * Draw axis displaying genomic coordinates * * @param context * @param trackRectangle */ private void drawGenomicAxis(RenderContext context, Rectangle trackRectangle) { int numTicks = 4; int ticHeight = 5; double pixelPadding = trackRectangle.getWidth() / 20; int yLoc = ticHeight + 1; double origin = context.getOrigin(); double locScale = context.getScale(); //Pixel start/end positions of ruler double startPix = trackRectangle.getX() + pixelPadding; double endPix = trackRectangle.getMaxX() - pixelPadding; double ticIntervalPix = (endPix - startPix) / (numTicks - 1); double ticIntervalCoord = locScale * ticIntervalPix; int startCoord = (int) (origin + (locScale * startPix)); Graphics2D g2D = context.getGraphic2DForColor(Color.black); g2D.drawLine((int) startPix, yLoc, (int) endPix, yLoc); for (int tic = 0; tic < numTicks; tic++) { int xLoc = (int) (startPix + tic * ticIntervalPix); g2D.drawLine(xLoc, yLoc, xLoc, yLoc - ticHeight); int ticCoord = (int) (startCoord + tic * ticIntervalCoord); String text = "" + ticCoord; Rectangle2D textBounds = g2D.getFontMetrics().getStringBounds(text, g2D); g2D.drawString(text, (int) (xLoc - textBounds.getWidth() / 2), (int) (yLoc + textBounds.getHeight())); } } } }