Search code examples
javajavafxjfreechart-fx

JFreeChart-FX time series vertical tick labels overlap


When using to render a time series chart with vertical tick labels, the labels unexpectedly overlap the domain axis and sometimes change on resize. I can't reproduce this with Swing or pure Java2D, shown here. I'd welcome any guidance.

image

import java.awt.BasicStroke;
import java.awt.Color;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.DateAxis;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.fx.ChartViewer;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYSplineRenderer;
import org.jfree.data.time.Day;
import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection;

/**
 * @see https://stackoverflow.com/q/70021577/230513
 */
public class SplineTest extends Application {

    @Override
    public void start(Stage stage) throws ParseException {
        SimpleDateFormat inFormat = new SimpleDateFormat("yyyy-MM-dd");
        SimpleDateFormat outFormat = new SimpleDateFormat("dd-MM-yyyy");
        // data
        double[] values = new double[]{0.67, 0.67, 0.69, 0.70, 0.70, 0.71, 0.71};
        String[] dates = new String[]{"2021-11-09", "2021-11-10", "2021-11-11",
            "2021-11-12", "2021-11-15", "2021-11-16", "2021-11-17"};
        TimeSeries series = new TimeSeries("Time Series");
        for (int i = 0; i < values.length; i++) {
            Date date = inFormat.parse(dates[i]);
            series.add(new Day(date), values[i]);
        }
        TimeSeriesCollection dataset = new TimeSeriesCollection(series);
        // axes
        NumberAxis rangeAxis = new NumberAxis("Value");
        NumberFormat numberFormat = NumberFormat.getInstance();
        numberFormat.setMinimumFractionDigits(2);
        numberFormat.setMaximumFractionDigits(2);
        rangeAxis.setNumberFormatOverride(numberFormat);
        rangeAxis.setLowerMargin(0.08); // 8% lower margin
        rangeAxis.setAutoRangeIncludesZero(false);
        DateAxis domainAxis = new DateAxis("Date");
        domainAxis.setDateFormatOverride(outFormat);
        domainAxis.setVerticalTickLabels(true);
        // renderer, plot, chart
        XYSplineRenderer r = new XYSplineRenderer(15);
        XYPlot xyplot = new XYPlot(dataset, domainAxis, rangeAxis, r);
        JFreeChart chart = new JFreeChart(null, xyplot);
        r.setSeriesPaint(0, new Color(255, 152, 0));
        r.setSeriesStroke(0, new BasicStroke(2.0f));
        // display
        Scene scene = new Scene(new ChartViewer(chart));
        stage.setTitle("SplineTest");
        stage.setScene(scene);
        stage.setWidth(600);
        stage.setHeight(400);
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Solution

  • The example cited uses a renderer for which no ChartFactory exists. These factories illustrate typical use cases; all end by applying a ChartTheme to the chart. The variation below applies a StandardChartTheme, which establishes coherent defaults for elements that affect geometry, such as font metrics and axis offsets; the defaults can be customized as shown.

    The issue does not arise in pure Java2D, which runs on the initial thread. A similar Swing program is typically scheduled on the EventQueue. A program relies on a custom GraphicsContext; applying a ChartTheme ensures that the required defaults are present on the first update.

    In exceptional cases, the chart can be made to redraw itself explicitly via fireChartChanged(). The Canvas held by the ChartViewer can do the same via its chartChanged() method.

    image

    import java.awt.BasicStroke;
    import java.awt.Color;
    import java.text.NumberFormat;
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import javafx.application.Application;
    import javafx.scene.Scene;
    import javafx.stage.Stage;
    import org.jfree.chart.JFreeChart;
    import org.jfree.chart.StandardChartTheme;
    import org.jfree.chart.axis.DateAxis;
    import org.jfree.chart.axis.NumberAxis;
    import org.jfree.chart.fx.ChartViewer;
    import org.jfree.chart.plot.XYPlot;
    import org.jfree.chart.renderer.xy.XYSplineRenderer;
    import org.jfree.data.time.Day;
    import org.jfree.data.time.TimeSeries;
    import org.jfree.data.time.TimeSeriesCollection;
    
    /**
     * @see https://stackoverflow.com/a/70058016/230513
     * @see https://stackoverflow.com/q/70021577/230513
     */
    public class SplineTest extends Application {
    
        @Override
        public void start(Stage stage) throws ParseException {
            SimpleDateFormat inFormat = new SimpleDateFormat("yyyy-MM-dd");
            SimpleDateFormat outFormat = new SimpleDateFormat("dd-MM-yyyy");
            // data
            double[] values = new double[]{0.67, 0.67, 0.69, 0.70, 0.70, 0.71, 0.71};
            String[] dates = new String[]{"2021-11-09", "2021-11-10", "2021-11-11",
                "2021-11-12", "2021-11-15", "2021-11-16", "2021-11-17"};
            TimeSeries series = new TimeSeries("Time Series");
            for (int i = 0; i < values.length; i++) {
                Date date = inFormat.parse(dates[i]);
                series.add(new Day(date), values[i]);
            }
            TimeSeriesCollection dataset = new TimeSeriesCollection(series);
            // axes
            NumberAxis rangeAxis = new NumberAxis("Value");
            NumberFormat numberFormat = NumberFormat.getInstance();
            numberFormat.setMinimumFractionDigits(2);
            numberFormat.setMaximumFractionDigits(2);
            rangeAxis.setNumberFormatOverride(numberFormat);
            rangeAxis.setLowerMargin(0.08); // 8% lower margin
            rangeAxis.setAutoRangeIncludesZero(false);
            DateAxis domainAxis = new DateAxis("Date");
            domainAxis.setDateFormatOverride(outFormat);
            domainAxis.setVerticalTickLabels(true);
            // renderer, plot, chart
            XYSplineRenderer r = new XYSplineRenderer(15);
            XYPlot xyplot = new XYPlot(dataset, domainAxis, rangeAxis, r);
            JFreeChart chart = new JFreeChart(null, xyplot);
            StandardChartTheme theme = new StandardChartTheme("Custom");
            theme.setPlotBackgroundPaint(Color.WHITE);
            theme.setDomainGridlinePaint(Color.LIGHT_GRAY);
            theme.setRangeGridlinePaint(Color.LIGHT_GRAY);
            theme.apply(chart);
            r.setSeriesPaint(0, new Color(255, 152, 0));
            r.setSeriesStroke(0, new BasicStroke(2.0f));
            // display
            Scene scene = new Scene(new ChartViewer(chart));
            stage.setTitle("SplineTest");
            stage.setScene(scene);
            stage.setWidth(600);
            stage.setHeight(400);
            stage.show();
        }
    
        public static void main(String[] args) {
            launch(args);
        }
    }