JFreeChart X轴标签超出了图表区域 [英] JFreeChart X axis labels are going out of chart area

查看:143
本文介绍了JFreeChart X轴标签超出了图表区域的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个以DateAxis为域的JFreeChart图表.看起来非常不错,但是最后一个轴标签有时会超出图表区域.这是要重现的示例代码:

I have a JFreeChart chart with DateAxis as domain. It looks very nice, however the last axis label sometimes goes out of the chart area. Here is the sample code to reproduce:

public class LineChart_AWT extends ApplicationFrame {

    public LineChart_AWT( String applicationTitle , String chartTitle ) {
          super(applicationTitle);

          ValueAxis timeAxis = new DateAxis("");
          NumberAxis valueAxis = new NumberAxis("Number");
          ((DateAxis)timeAxis).setDateFormatOverride(new SimpleDateFormat("YYYY-MM-dd HH:mm"));
          XYPlot plot = new XYPlot(createDataset(), timeAxis, valueAxis, null);
          XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer(true, false);

          plot.setRenderer(renderer);
          plot.getRangeAxis().setAutoRange(true);
          ((NumberAxis)plot.getRangeAxis()).setAutoRangeIncludesZero(false);
          JFreeChart lineChart = new JFreeChart(chartTitle, plot);
          plot.setBackgroundPaint(Color.lightGray);

          plot.setDomainGridlinesVisible(true);
          plot.setRangeGridlinesVisible(true);
          plot.setDomainGridlinePaint(Color.white);
          plot.setRangeGridlinePaint(Color.white);

          lineChart.setBackgroundPaint(Color.white);
          ChartPanel chartPanel = new ChartPanel( lineChart );
          chartPanel.setPreferredSize( new java.awt.Dimension( 560 , 367 ) );
          setContentPane( chartPanel );
       }

       private TimeSeriesCollection createDataset( ) {
           TimeSeries typeA = new TimeSeries("TypeA");
         TimeSeries typeB = new TimeSeries("TypeB");
         TimeSeriesCollection collection = new TimeSeriesCollection();

         collection.addSeries(typeA);
         collection.addSeries(typeB);
         typeA = collection.getSeries("TypeA");

         typeA.add(new Hour(8, new Day()), 1.0);
         typeA.add(new Hour(10, new Day()), 1.0);
         typeA.add(new Hour(11, new Day()), 1.0);
         typeA.add(new Hour(13, new Day()), 1.0);
         typeA.add(new Hour(16, new Day()), 2.0);
         typeA.add(new Hour(18, new Day()), 2.0);

         typeB.add(new Hour(8, new Day()), 1.0);
         typeB.add(new Hour(19, new Day()), 2.0);
         typeB.add(new Hour(20, new Day()), 5.0);


          return collection;
       }

       public static void main( String[ ] args ) {
          LineChart_AWT chart = new LineChart_AWT(
             "X-axis demo" ,
             "X-axis labels are truncated");

          chart.pack( );
          RefineryUtilities.centerFrameOnScreen( chart );
          chart.setVisible( true );
       }
    }

这是当前的屏幕截图;问题可以在最后一个标签上看到:

Here is the current screenshot; problem can be seen on the last label:

是什么导致最后一个标签显示在当前图表区域之外?另外,如何预防呢?

What causes that last label to be rendered outside of the current chart area? Also, how can I prevent it?

这是一个更全面的示例,其中包含屏幕截图和所有详细信息.

Here is a more comprehensive example with screenshots and all the details.

根据@trashgod的评论,我已经更新到最新的JFreeChart Engine (jfreechart-1.0.19.jar和jcommon-1.0.23.jar) (jfreechart-1.6.0-snapshot.jar).

According to @trashgod's comments, I've updated to the latest JFreeChart Engine (jfreechart-1.0.19.jar and jcommon-1.0.23.jar) (jfreechart-1.6.0-snapshot.jar).

请考虑以下示例(它非常依赖@trashgod的建议-非常感谢):

Consider this example (which deeply relies on @trashgod's suggestions - thank you very much):

import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.text.SimpleDateFormat;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.DateAxis;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.chart.ui.ApplicationFrame;
import org.jfree.data.time.Minute;
import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection;

/**
 * @see https://stackoverflow.com/a/57637615/230513
 * @see https://stackoverflow.com/a/57544811/230513
 */
public class TimeChart extends ApplicationFrame {

    private static boolean lot_of_values = false;

    public TimeChart(String applicationTitle, String chartTitle) {
        super(applicationTitle);

        DateAxis timeAxis = new DateAxis("Timestamp");
        timeAxis.setUpperMargin(DateAxis.DEFAULT_UPPER_MARGIN /* * 2*/); // UPDATED
        timeAxis.setLowerMargin(DateAxis.DEFAULT_LOWER_MARGIN /* * 2*/); // UPDATED
        timeAxis.setDateFormatOverride(new SimpleDateFormat("YYYY-MM-dd HH:mm"));
        NumberAxis numberAxis = new NumberAxis("Number");
        numberAxis.setAutoRangeIncludesZero(false);
        XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer(true, false);
        XYPlot plot = new XYPlot(createDataset(), timeAxis, numberAxis, renderer);
        plot.setBackgroundPaint(Color.lightGray);
        plot.setDomainGridlinePaint(Color.white);
        plot.setRangeGridlinePaint(Color.white);
        JFreeChart lineChart = new JFreeChart(chartTitle, plot);
        lineChart.setBackgroundPaint(Color.white);
        ChartPanel chartPanel = new ChartPanel(lineChart) {
            @Override
            public Dimension getPreferredSize() {
                return new Dimension(1529 , 538);
            }
        };
        add(chartPanel);
    }

    private TimeSeriesCollection createDataset() {

        TimeSeries typeA = new TimeSeries("Temperatures");
        TimeSeriesCollection collection = new TimeSeriesCollection();

        collection.addSeries(typeA);

        if (lot_of_values) {
            typeA.add(Minute.parseMinute("2019-08-25 00:00"), 26.68);
            typeA.add(Minute.parseMinute("2019-08-25 01:00"), 26.75);
            typeA.add(Minute.parseMinute("2019-08-25 02:00"), 25.95);
            typeA.add(Minute.parseMinute("2019-08-25 03:00"), 25.47);
            typeA.add(Minute.parseMinute("2019-08-25 04:00"), 25.19);
            typeA.add(Minute.parseMinute("2019-08-25 05:00"), 24.65);
            typeA.add(Minute.parseMinute("2019-08-25 06:00"), 24.61);
            typeA.add(Minute.parseMinute("2019-08-25 07:00"), 25.58);
            typeA.add(Minute.parseMinute("2019-08-25 08:00"), 26.43);
            typeA.add(Minute.parseMinute("2019-08-25 09:00"), 26.96);
            typeA.add(Minute.parseMinute("2019-08-25 10:00"), 27.81);
            typeA.add(Minute.parseMinute("2019-08-25 11:00"), 28.69);
            typeA.add(Minute.parseMinute("2019-08-25 12:00"), 29.39);
            typeA.add(Minute.parseMinute("2019-08-25 13:00"), 29.89);
            typeA.add(Minute.parseMinute("2019-08-25 14:00"), 30.32);
            typeA.add(Minute.parseMinute("2019-08-25 15:00"), 30.69);
            typeA.add(Minute.parseMinute("2019-08-25 16:00"), 30.83);
            typeA.add(Minute.parseMinute("2019-08-25 17:00"), 30.85);
            typeA.add(Minute.parseMinute("2019-08-25 18:00"), 30.64);
            typeA.add(Minute.parseMinute("2019-08-25 19:00"), 30.04);
            typeA.add(Minute.parseMinute("2019-08-25 20:00"), 29.51);
            typeA.add(Minute.parseMinute("2019-08-25 21:00"), 28.63);
            typeA.add(Minute.parseMinute("2019-08-25 22:00"), 28.48);
            typeA.add(Minute.parseMinute("2019-08-25 23:00"), 27.15);
            typeA.add(Minute.parseMinute("2019-08-26 00:00"), 27.3);
            typeA.add(Minute.parseMinute("2019-08-26 01:00"), 27.05);
            typeA.add(Minute.parseMinute("2019-08-26 02:00"), 26.84);
            typeA.add(Minute.parseMinute("2019-08-26 03:00"), 26.47);
            typeA.add(Minute.parseMinute("2019-08-26 04:00"), 26.34);
            typeA.add(Minute.parseMinute("2019-08-26 05:00"), 25.95);
            typeA.add(Minute.parseMinute("2019-08-26 06:00"), 26.46);
            typeA.add(Minute.parseMinute("2019-08-26 07:00"), 26.75);
            typeA.add(Minute.parseMinute("2019-08-26 08:00"), 26.94);
            typeA.add(Minute.parseMinute("2019-08-26 09:00"), 27.05);
            typeA.add(Minute.parseMinute("2019-08-26 10:00"), 27.35);
            typeA.add(Minute.parseMinute("2019-08-26 11:00"), 27.67);
            typeA.add(Minute.parseMinute("2019-08-26 12:00"), 28.12);
            typeA.add(Minute.parseMinute("2019-08-26 13:00"), 28.41);
            typeA.add(Minute.parseMinute("2019-08-26 14:00"), 28.67);
            typeA.add(Minute.parseMinute("2019-08-26 15:00"), 28.99);
            typeA.add(Minute.parseMinute("2019-08-26 16:00"), 28.99);
            typeA.add(Minute.parseMinute("2019-08-26 17:00"), 29.02);
            typeA.add(Minute.parseMinute("2019-08-26 18:00"), 29.02);
            typeA.add(Minute.parseMinute("2019-08-26 19:00"), 28.43);
            typeA.add(Minute.parseMinute("2019-08-26 20:00"), 27.87);
            typeA.add(Minute.parseMinute("2019-08-26 21:00"), 27.2);
            typeA.add(Minute.parseMinute("2019-08-26 22:00"), 26.88);
            typeA.add(Minute.parseMinute("2019-08-26 23:00"), 26.31);
            typeA.add(Minute.parseMinute("2019-08-27 00:00"), 26.02);
            typeA.add(Minute.parseMinute("2019-08-27 01:00"), 25.51);
            typeA.add(Minute.parseMinute("2019-08-27 02:00"), 25.12);
            typeA.add(Minute.parseMinute("2019-08-27 03:00"), 25.11);
            typeA.add(Minute.parseMinute("2019-08-27 04:00"), 24.97);
            typeA.add(Minute.parseMinute("2019-08-27 05:00"), 24.85);
            typeA.add(Minute.parseMinute("2019-08-27 06:00"), 24.73);
            typeA.add(Minute.parseMinute("2019-08-27 07:00"), 25.04);
            typeA.add(Minute.parseMinute("2019-08-27 08:00"), 25.68);
            typeA.add(Minute.parseMinute("2019-08-27 09:00"), 26.22);
            typeA.add(Minute.parseMinute("2019-08-27 10:00"), 26.69);
            typeA.add(Minute.parseMinute("2019-08-27 11:00"), 27.3);
            typeA.add(Minute.parseMinute("2019-08-27 12:00"), 27.84);
            typeA.add(Minute.parseMinute("2019-08-27 13:00"), 28.26);
            typeA.add(Minute.parseMinute("2019-08-27 14:00"), 28.6);
            typeA.add(Minute.parseMinute("2019-08-27 15:00"), 29.03);
            typeA.add(Minute.parseMinute("2019-08-27 16:00"), 29.38);
            typeA.add(Minute.parseMinute("2019-08-27 17:00"), 29.62);
            typeA.add(Minute.parseMinute("2019-08-27 18:00"), 29.47);
            typeA.add(Minute.parseMinute("2019-08-27 19:00"), 29.01);
            typeA.add(Minute.parseMinute("2019-08-27 20:00"), 28.31);
            typeA.add(Minute.parseMinute("2019-08-27 21:00"), 27.69);
            typeA.add(Minute.parseMinute("2019-08-27 22:00"), 26.93);
            typeA.add(Minute.parseMinute("2019-08-27 23:00"), 26.37);
        }

        typeA.add(Minute.parseMinute("2019-08-28 00:00"), 26.12);
        typeA.add(Minute.parseMinute("2019-08-28 01:00"), 25.77);
        typeA.add(Minute.parseMinute("2019-08-28 02:00"), 25.42);
        typeA.add(Minute.parseMinute("2019-08-28 03:00"), 25.0);
        typeA.add(Minute.parseMinute("2019-08-28 04:00"), 24.57);
        typeA.add(Minute.parseMinute("2019-08-28 05:00"), 24.23);
        typeA.add(Minute.parseMinute("2019-08-28 06:00"), 24.38);
        typeA.add(Minute.parseMinute("2019-08-28 07:00"), 24.99);
        typeA.add(Minute.parseMinute("2019-08-28 08:00"), 25.86);
        typeA.add(Minute.parseMinute("2019-08-28 09:00"), 26.53);
        typeA.add(Minute.parseMinute("2019-08-28 10:00"), 27.32);
        typeA.add(Minute.parseMinute("2019-08-28 11:00"), 27.95);
        typeA.add(Minute.parseMinute("2019-08-28 12:00"), 28.64);
        typeA.add(Minute.parseMinute("2019-08-28 13:00"), 29.38);
        typeA.add(Minute.parseMinute("2019-08-28 14:00"), 29.74);
        typeA.add(Minute.parseMinute("2019-08-28 15:00"), 30.13);
        typeA.add(Minute.parseMinute("2019-08-28 16:00"), 30.42);
        typeA.add(Minute.parseMinute("2019-08-28 17:00"), 30.48);
        typeA.add(Minute.parseMinute("2019-08-28 18:00"), 30.14);
        typeA.add(Minute.parseMinute("2019-08-28 19:00"), 29.41);
        typeA.add(Minute.parseMinute("2019-08-28 20:00"), 28.47);
        typeA.add(Minute.parseMinute("2019-08-28 21:00"), 28.05);



        return collection;
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                TimeChart chart = new TimeChart(
                    "Date axis demo",
                    "Date axis labels are visible");

                chart.pack();
                chart.setVisible(true);
            }
        });
    }
}

请注意,我已将首选图表尺寸更改为 1529 x 538 (我将需要生成具有此尺寸的PNG),并且还引入了一个名为 lot_of_values .最初,它设置为 false ,这是此屏幕截图:

Please note that I've changed the preferred diagram size to 1529 x 538 (I will need to generate a PNG with this size), and also I have introduced a new static variable called lot_of_values. Initially, it is set to false, here is a screenshot of this:

但是,如果我将 lot_of_values 更改为 true (将会向集合中添加更多数据-您可以在源代码中看到),则域轴的最后一个标签将被削减.这是 lot_of_values = true 的屏幕截图:

However if I change lot_of_values to true (which will add more data to the collection - you can see in the source), the last label of the domain axis will be cut. Here is the screenshot with lot_of_values=true:

我已经深入研究JFreeChart的资源,并且正在解决问题. (同样,我不得不从上面的源代码中删除一些行以适应30k个字符的限制)

I have digged myself into JFreeChart's sources and I'm on the way of solving the problem. (also I had to remove some lines from the source above to fit into the 30k characters limit)

请考虑以下屏幕截图:

Consider the following screenshot:

我认为在图表的当前数据绘制前后会应用边距值,而不是在当前的范围刻度线处应用边距值.这就是为什么可以剪切最后一个刻度标签的原因.

I think margin values are applied before and after the chart's current data plotting and not to the current range ticks. That's why the last tick label can be cut.

如果数据要填充到最后一个刻度(当前为2019-08-29 00:00),这将不是问题,因为在这种情况下,页边空白将允许正确打印该值.

It won't be a problem if the data would fill up until the last tick (currently 2019-08-29 00:00) because in that case the margin would allow that value to be printed correctly.

让我们看看这一点的概念证明.我在数据集中添加了三行:

Let's see a proof-of-concept for this. I added three lines to the dataset:

typeA.add(Minute.parseMinute("2019-08-28 21:00"), 28.05); //original line
typeA.add(Minute.parseMinute("2019-08-28 22:00"), 28.05); //new line
typeA.add(Minute.parseMinute("2019-08-28 23:00"), 28.05); //new line
typeA.add(Minute.parseMinute("2019-08-29 00:00"), 28.05); //new line

现在结果是:

这也可以通过调用以下命令来修改轴的最大日期来实现:

This can be achieved also by modifying the axis's maximum date by calling:

timeAxis.setMaximumDate(new Date(119,7,29,4,36));

现在,我将继续搜寻此 MaximumDate 的计算位置.如果有人知道,请告诉我.

Now I will go forward to hunt down where this MaximumDate calculated. If someone know, please let me know.

推荐答案

我已成功调查并解决了此问题.

I have successfully investigated and solved this issue.

当JFreeChart决定其轴上的默认刻度时,它将计算标签的宽度并检查它们是否在刻度之间,然后增加刻度直到标签适合为止.

When JFreeChart decides about the default ticks on its axis, it calculates the width of the labels and checks if they fit in between the ticks, then increases the ticks until the labels will fit.

很好,但是在此过程中,JFreeChart不会检查天气,最后一个标签是否适合图表的绘图区域.

That's good but during this procedure JFreeChart does not check weather the last label will fit or not into the chart's drawing area.

要克服这种情况,您将有两个任务:

To overcome this situation you will have two tasks:

  1. 检查天气,您的上一个标签是否被剪掉
  2. 如果已切割,请更正轴的范围

这是我这样做的方法,在不涉及JFreeChart的源代码的情况下,尝试将其最小化:

Here is how I have done this, trying to be as minimal as possible while not touching JFreeChart's source at all:

import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.font.FontRenderContext;
import java.awt.font.LineMetrics;
import java.awt.geom.Rectangle2D;
import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

import org.jfree.chart.axis.AxisState;
import org.jfree.chart.axis.DateAxis;
import org.jfree.chart.axis.DateTickUnit;
import org.jfree.chart.plot.PlotRenderingInfo;
import org.jfree.chart.ui.RectangleEdge;
import org.jfree.chart.ui.RectangleInsets;
import org.jfree.data.time.DateRange;

public class CorrectedDateAxis extends DateAxis {

    /** For serialization. */
    private static final long serialVersionUID = 0L;


    /**
     * Creates a date axis with no label.
     */
    public CorrectedDateAxis() {
        super(null);
    }

    /**
     * Creates a date axis with the specified label.
     *
     * @param label  the axis label ({@code null} permitted).
     */
    public CorrectedDateAxis(String label) {
        super(label);
    }

    /**
     * Creates a date axis.
     *
     * @param label  the axis label ({@code null} permitted).
     * @param zone  the time zone.
     * @param locale  the locale ({@code null} not permitted).
     */
    public CorrectedDateAxis(String label, TimeZone zone, Locale locale) {
        super(label, zone, locale);
    }

    /**
     * Estimates the maximum width of the tick labels, assuming the specified
     * tick unit is used.
     * <P>
     * Rather than computing the string bounds of every tick on the axis, we
     * just look at two values: the lower bound and the upper bound for the
     * axis.  These two values will usually be representative.
     *
     * @param g2  the graphics device.
     * @param unit  the tick unit to use for calculation.
     *
     * @return The estimated maximum width of the tick labels.
     */
    private double estimateMaximumTickLabelWidth(Graphics2D g2, 
            DateTickUnit unit) {

        RectangleInsets tickLabelInsets = getTickLabelInsets();
        double result = tickLabelInsets.getLeft() + tickLabelInsets.getRight();

        Font tickLabelFont = getTickLabelFont();
        FontRenderContext frc = g2.getFontRenderContext();
        LineMetrics lm = tickLabelFont.getLineMetrics("ABCxyz", frc);
        if (isVerticalTickLabels()) {
            // all tick labels have the same width (equal to the height of
            // the font)...
            result += lm.getHeight();
        }
        else {
            // look at lower and upper bounds...
            DateRange range = (DateRange) getRange();
            Date lower = range.getLowerDate();
            Date upper = range.getUpperDate();
            String lowerStr, upperStr;
            DateFormat formatter = getDateFormatOverride();
            if (formatter != null) {
                lowerStr = formatter.format(lower);
                upperStr = formatter.format(upper);
            }
            else {
                lowerStr = unit.dateToString(lower);
                upperStr = unit.dateToString(upper);
            }
            FontMetrics fm = g2.getFontMetrics(tickLabelFont);
            double w1 = fm.stringWidth(lowerStr);
            double w2 = fm.stringWidth(upperStr);
            result += Math.max(w1, w2);
        }

        return result;
    }   

    @Override
    public AxisState draw(Graphics2D g2, double cursor, Rectangle2D plotArea, Rectangle2D dataArea, RectangleEdge edge,
            PlotRenderingInfo plotState) {

        double labelWidth = estimateMaximumTickLabelWidth(g2, getTickUnit());

        double lastLabelPosition = dateToJava2D(calculateHighestVisibleTickValue(getTickUnit()),
                plotArea, edge);

        if (lastLabelPosition + labelWidth / 2 > plotArea.getMaxX()) {
            double plottingWidthCorrection = plotArea.getX() + (lastLabelPosition + labelWidth / 2) - plotArea.getMaxX();

            // Calculate and set the new corrected maximum date
            setMaximumDate(new Date((long)(getMaximumDate().getTime() + java2DToValue(plottingWidthCorrection, plotArea, edge) - getMinimumDate().getTime())));
        }

        return super.draw(g2, cursor, plotArea, dataArea, edge, plotState);
    }
}

这是对 DateAxis 类的替代,它完成了上述两个任务.

This is an override to the DateAxis class and it does both of the tasks mentioned above.

注意,该子类包含从JFreeChart的 DateAxis 类复制的许多代码,因为它们已将 estimateMaximumTickLabelWidth 定义为私有,因此子类无法访问它.

Note, this subclass contains a lot of codes copied from JFreeChart's DateAxis class because they have defined estimateMaximumTickLabelWidth as private so subclasses are not accessing it.

您可以修改原始的DateAxis类以将该函数定义为受保护的,这样就可以在此子类中跳过此函数.

You can modify the original DateAxis class to define this function as protected, in this way you could skip this function in this subclass.

这是 CorrectedDateAxis 进入图片并校正DateAxis范围时的外观:

Here is how it looks like when CorrectedDateAxis steps into the picture and corrects the DateAxis's range:

最后没有错误的标签了!

No more wrong labels at the end!

这篇关于JFreeChart X轴标签超出了图表区域的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆