Search code examples
javaplotjfreechart

CrosshairOverlay throws NullpointerException with CombinedXYPlot (jfreechart)


I wants to add a CrosshairOverlay to my CombinedXYPlot from jFreeCharts library. In a first step i plot a XYPlot and add this to my combinedXYPlot. Like in several demos and tutorials i create the crosshairOverlay and add it to the chartPanel with the corresponding combinedXYPlot.

This is my example code, that produces the NullPointerException:

import org.jfree.chart.ChartMouseEvent;
import org.jfree.chart.ChartMouseListener;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.AxisLocation;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.panel.CrosshairOverlay;
import org.jfree.chart.plot.CombinedDomainXYPlot;
import org.jfree.chart.plot.Crosshair;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.StandardXYItemRenderer;
import org.jfree.chart.renderer.xy.XYItemRenderer;
import org.jfree.chart.title.LegendTitle;
import org.jfree.data.xy.XYDataset;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
import org.jfree.ui.ApplicationFrame;
import org.jfree.ui.RectangleEdge;

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

public class stack implements ChartMouseListener{

    CombinedDomainXYPlot combinedDomainXYPlot;
    XYPlot myPlot1;

    Crosshair xCrosshair;
    Crosshair yCrosshair;

    public static void main(String[] args){

        stack s = new stack();


    }

    public stack(){

        ApplicationFrame app = new ApplicationFrame("Example");
        myPlot1 = createPlot();
        combinedDomainXYPlot = createCombinedXYPlot();
        JFreeChart chart = new JFreeChart("Example", combinedDomainXYPlot);
        ChartPanel chartPanel = new ChartPanel(chart);

        chartPanel.addChartMouseListener(this);

        CrosshairOverlay crosshairOverlay = new CrosshairOverlay();
        this.xCrosshair = new Crosshair(Double.NaN, Color.GRAY, new BasicStroke(0f));
        this.xCrosshair.setLabelVisible(false);
        this.yCrosshair = new Crosshair(Double.NaN, Color.GRAY, new BasicStroke(0f));
        this.yCrosshair.setLabelVisible(false);

        crosshairOverlay.addDomainCrosshair(xCrosshair);
        crosshairOverlay.addRangeCrosshair(yCrosshair);

        //chartPanel.addOverlay(crosshairOverlay);

        LegendTitle legend = chart.getLegend();
        legend.setPosition(RectangleEdge.RIGHT);
        legend.setBackgroundPaint(Color.BLACK);
        legend.setItemPaint(Color.GRAY);
        legend.setItemFont(new Font("Arial", 1, 9));

        app.getContentPane().add(chartPanel);
        app.pack();
        app.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);

        app.setVisible(true);

    }

    private XYPlot createPlot(){
        XYDataset data1 = createDataset();
        XYItemRenderer renderer1 = new StandardXYItemRenderer();
        NumberAxis rangeAxis1 = new NumberAxis("Range 1");
        XYPlot myPlot = new XYPlot(data1,null,rangeAxis1,renderer1);
        myPlot.setRangeAxisLocation(AxisLocation.BOTTOM_OR_LEFT);
        return myPlot;
    }

    private CombinedDomainXYPlot createCombinedXYPlot(){
        CombinedDomainXYPlot combinedDomainXYPlot = new CombinedDomainXYPlot(new NumberAxis());
        combinedDomainXYPlot.setGap(5);
        combinedDomainXYPlot.add(myPlot1,11);
        return combinedDomainXYPlot;
    }

    private XYDataset createDataset() {

        // create dataset 2...
        final XYSeries series2 = new XYSeries("Series 2");

        series2.add(10.0, 16853.2);
        series2.add(20.0, 19642.3);
        series2.add(30.0, 18253.5);
        series2.add(40.0, 15352.3);
        series2.add(50.0, 13532.0);
        series2.add(100.0, 12635.3);
        series2.add(110.0, 13998.2);
        series2.add(120.0, 11943.2);
        series2.add(130.0, 16943.9);
        series2.add(140.0, 17843.2);
        series2.add(150.0, 16495.3);
        series2.add(160.0, 17943.6);
        series2.add(170.0, 18500.7);
        series2.add(180.0, 19595.9);

        return new XYSeriesCollection(series2);

    }

    @Override
    public void chartMouseClicked(ChartMouseEvent event) {
        // would be happy to get this called without exception
        System.out.print("Test");
    }

    @Override
    public void chartMouseMoved(ChartMouseEvent event) {
        // would be happy to get this called  without exception
        System.out.print("Test");
    }
}

The Exception:

Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException
    at org.jfree.chart.panel.CrosshairOverlay.paintOverlay(CrosshairOverlay.java:257)
    at org.jfree.chart.ChartPanel.paintComponent(ChartPanel.java:1658)
    at javax.swing.JComponent.paint(JComponent.java:1056)
    at javax.swing.JComponent.paintChildren(JComponent.java:889)
    at javax.swing.JComponent.paint(JComponent.java:1065)
    at javax.swing.JComponent.paintChildren(JComponent.java:889)
    at javax.swing.JComponent.paint(JComponent.java:1065)
    at javax.swing.JLayeredPane.paint(JLayeredPane.java:586)
    at javax.swing.JComponent.paintChildren(JComponent.java:889)
    at javax.swing.JComponent.paintToOffscreen(JComponent.java:5217)
    at javax.swing.RepaintManager$PaintManager.paintDoubleBuffered(RepaintManager.java:1579)
    at javax.swing.RepaintManager$PaintManager.paint(RepaintManager.java:1502)
    at javax.swing.RepaintManager.paint(RepaintManager.java:1272)
    at javax.swing.JComponent.paint(JComponent.java:1042)
    at java.awt.GraphicsCallback$PaintCallback.run(GraphicsCallback.java:39)
    at sun.awt.SunGraphicsCallback.runOneComponent(SunGraphicsCallback.java:79)
    at sun.awt.SunGraphicsCallback.runComponents(SunGraphicsCallback.java:116)
    at java.awt.Container.paint(Container.java:1975)
    at java.awt.Window.paint(Window.java:3904)
    at javax.swing.RepaintManager$4.run(RepaintManager.java:842)
    ...

I could find out that in the statement (in CrosshairOverlay.java)

double yy = yAxis.valueToJava2D(y, dataArea, yAxisEdge);

throws the NullPointerException because the yAxis is null. This variable is assigned above in the statement:

ValueAxis yAxis = plot.getRangeAxis();

The function RangeAxis() from XYPlot.java:

public ValueAxis getRangeAxis(int index) {
    ValueAxis result = this.rangeAxes.get(index);
    if (result == null) {
        Plot parent = getParent();
        if (parent instanceof XYPlot) {
            XYPlot xy = (XYPlot) parent;
            result = xy.getRangeAxis(index);
        }
    }
    return result;
}

Here parent is not instance of XYPlot, but null. The comment of the getParent() function says:

Returns the parent plot (or null if this plot is not part of a combined plot).

At this point i do not understand why it returns null (Maybe because i am in the CombinedXYPlot and it has no parent?). Can anybody help me and explain why the parent plot is null, and how to solve my problem?


Solution

  • I found an answer to my question. I had to write my own CrosshairOverlay by just modifing the paintOverlay function. Now the crosshair is painted (just) for the first plot added to the CombinedXYPlot

    I had to downcast to CombinedXYPlot to reach a function that gives me a list of the XYPlot subplots. From that on i choose the first subplot and let the crosshair painting do his calculations with this.

        @Override
        public void paintOverlay(Graphics2D g2, ChartPanel chartPanel) {
            Shape savedClip = g2.getClip();
            Rectangle2D dataArea = chartPanel.getScreenDataArea();
            g2.clip(dataArea);
            JFreeChart chart = chartPanel.getChart();
            XYPlot plot = (XYPlot) chart.getPlot();
    
            ValueAxis xAxis = plot.getDomainAxis();
            RectangleEdge xAxisEdge = plot.getDomainAxisEdge();
            Iterator iterator = getDomainCrosshairs().iterator();
            while (iterator.hasNext()) {
                Crosshair ch = (Crosshair) iterator.next();
                if (ch.isVisible()) {
                    double x = ch.getValue();
                    double xx = xAxis.valueToJava2D(x, dataArea, xAxisEdge);
                    if (plot.getOrientation() == PlotOrientation.VERTICAL) {
                        drawVerticalCrosshair(g2, dataArea, xx, ch);
                    }
                    else {
                        drawHorizontalCrosshair(g2, dataArea, xx, ch);
                    }
                }
            }
    
            // get subplots
            List<XYPlot> subplots = ((CombinedDomainXYPlot) plot).getSubplots();
    
            //take yAxis from first plot
            ValueAxis yAxis = subplots.get(0).getRangeAxis();
    
            RectangleEdge yAxisEdge = plot.getRangeAxisEdge();
            iterator = this.getRangeCrosshairs().iterator();
            while (iterator.hasNext()) {
                Crosshair ch = (Crosshair) iterator.next();
                if (ch.isVisible()) {
                    double y = ch.getValue();
                    double yy = yAxis.valueToJava2D(y, dataArea, yAxisEdge);
                    if (plot.getOrientation() == PlotOrientation.VERTICAL) {
                        drawHorizontalCrosshair(g2, dataArea, yy, ch);
                    }
                    else {
                        drawVerticalCrosshair(g2, dataArea, yy, ch);
                    }
                }
            }
            g2.setClip(savedClip);
        }
    

    The problem with that solution is, if i add subblots to the CombinedXYPlot, the crosshair position will be shiftet by the size of each subplot. I have to find a way to get the size of a subplot (should be the same for each plot) and add it to the calculation of the yy position.

    This problem does not have anything to do with my original question, so i will wait help or for potentiall better answers a while and will mark this post as solution.