This is an issue I was seeing in a chart. I used an agent to help me track it down. I'm running 1.5.2, but tested against 1.5.6 and saw the same behavior.
XYPlot.drawAxes stores each axis's drawing state (which carries that axis's tick list) HashMap keyed by the axis object. But for an auto-ranged axis JFreeChart's ValueAxis.equals ignores the current range, and Axis.hashCode hashes only the (here null) label — so two unlabeled, identically-configured, auto-ranged {@code NumberAxis} instances are equal with equal hash codes even if they have different ranges. The range (y) axis's state therefore overwrites the domain (x) axis's entry in the map, and the vertical grid line pass draws lines at the y-axis tick values.
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
import javax.swing.*;
import java.awt.*;
private static JFreeChart buildChart( java.util.function.Supplier<NumberAxis> axisFactory )
{
XYSeries series = new XYSeries( "data" );
// x: 0.5 .. 11.5 (so vertical gridlines belong at 1,2,..,11)
// y: a gentle curve in 0 .. 7 (so the y ticks 0..7 land inside the x range when misused)
for( double x = 0.5; x <= 11.5; x += 0.25 )
series.add( x, 3.5 + 3.0 * Math.sin( x / 2.0 ) );
XYSeriesCollection dataset = new XYSeriesCollection( series );
NumberAxis xAxis = axisFactory.get();
xAxis.setAutoRangeIncludesZero( false );
NumberAxis yAxis = axisFactory.get();
yAxis.setAutoRangeIncludesZero( false );
XYPlot plot = new XYPlot( dataset, xAxis, yAxis, new XYLineAndShapeRenderer( true, false ) );
plot.setOrientation( PlotOrientation.VERTICAL );
plot.setBackgroundPaint( Color.WHITE );
plot.setDomainGridlinesVisible( true );
plot.setDomainGridlinePaint( Color.BLUE );
plot.setDomainGridlineStroke( new BasicStroke( 1f ) );
plot.setRangeGridlinesVisible( true );
plot.setRangeGridlinePaint( new Color( 220, 220, 220 ) );
plot.setDomainZeroBaselineVisible( false );
plot.setRangeZeroBaselineVisible( false );
return new JFreeChart( "", JFreeChart.DEFAULT_TITLE_FONT, plot, false );
}
void main( String[] args )
{
SwingUtilities.invokeLater( () ->
{
JFrame frame = new JFrame( "JFreeChart axis-equals gridline bug" );
frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
frame.setSize( 900, 600 );
ChartPanel chartPanel = new ChartPanel( buildChart( NumberAxis::new ) );
JLabel status = new JLabel();
JButton toggle = new JButton();
// boolean[1] so the lambda can flip it
boolean[] fixed = { false };
Runnable apply = () ->
{
chartPanel.setChart( buildChart( fixed[0] ? IdentityEqualsNumberAxis::new : NumberAxis::new ) );
status.setText( fixed[0]
? " IdentityEqualsNumberAxis — vertical gridlines correct (1..11, full width)"
: " stock NumberAxis — vertical gridlines WRONG (bunched left, drawn at y-tick values)" );
toggle.setText( fixed[0] ? "Show the bug (stock NumberAxis)" : "Apply the fix (identity-equals axis)" );
};
toggle.addActionListener( e ->
{
fixed[0] = !fixed[0];
apply.run();
} );
apply.run();
JPanel top = new JPanel( new BorderLayout() );
top.add( toggle, BorderLayout.WEST );
top.add( status, BorderLayout.CENTER );
frame.setLayout( new BorderLayout() );
frame.add( top, BorderLayout.NORTH );
frame.add( chartPanel, BorderLayout.CENTER );
frame.setLocationRelativeTo( null );
frame.setVisible( true );
} );
}
private static final class IdentityEqualsNumberAxis extends NumberAxis
{
@Override
public boolean equals( Object obj )
{
return obj == this;
}
@Override
public int hashCode()
{
return System.identityHashCode( this );
}
}
}
This is an issue I was seeing in a chart. I used an agent to help me track it down. I'm running 1.5.2, but tested against 1.5.6 and saw the same behavior.
XYPlot.drawAxes stores each axis's drawing state (which carries that axis's tick list) HashMap keyed by the axis object. But for an auto-ranged axis JFreeChart's ValueAxis.equals ignores the current range, and Axis.hashCode hashes only the (here null) label — so two unlabeled, identically-configured, auto-ranged {@code NumberAxis} instances are equal with equal hash codes even if they have different ranges. The range (y) axis's state therefore overwrites the domain (x) axis's entry in the map, and the vertical grid line pass draws lines at the y-axis tick values.