Java JFreeChart X轴标签超出图表区域

Java JFreeChart X轴标签超出图表区域,java,swing,jfreechart,Java,Swing,Jfreechart,我有一个JFreeChart图表,其中DateAxis作为域。它看起来非常漂亮,但是最后一个轴标签有时会超出图表区域。下面是要复制的示例代码: public class LineChart_AWT extends ApplicationFrame { public LineChart_AWT( String applicationTitle , String chartTitle ) { super(applicationTitle); Val

我有一个
JFreeChart
图表,其中
DateAxis
作为域。它看起来非常漂亮,但是最后一个轴标签有时会超出图表区域。下面是要复制的示例代码:

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 );
       }
    }
这是当前的屏幕截图;可以在最后一个标签上看到问题:

是什么导致最后一个标签在当前图表区域之外呈现?还有,我怎样才能预防呢

更新 下面是一个更全面的示例,包括屏幕截图和所有细节

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

考虑一下这个例子(它非常依赖@trashgood的建议-非常感谢):

请注意,我已经将首选图表大小更改为1529x538(我需要生成一个具有此大小的PNG),并且我还引入了一个名为lot\u of\u values的新静态变量。最初,它被设置为false,下面是一个屏幕截图:

但是,如果我将lot_of_值更改为true(这将向集合添加更多数据-您可以在源代码中看到),域轴的最后一个标签将被剪切。下面是一个屏幕截图,其中很多值=true:

更新2 我已经挖掘了JFreeChart的资源,我正在解决这个问题。(我还不得不从上面的源代码中删除一些行,以适应30k字符的限制)

考虑以下屏幕截图:

我认为边距值是在图表当前数据打印之前和之后应用的,而不是应用于当前范围刻度。这就是为什么最后一个记号标签可以被剪掉

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

让我们看看这方面的概念证明。我在数据集中添加了三行:

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
现在结果是:

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

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

现在我将继续查找这个最大日期的计算位置。如果有人知道,请让我知道。

这种效果是人为减小图表的首选大小,同时显式增加日期轴刻度标签大小而造成的伪影。请注意,省略对
setPreferredSize()
的调用会消除这种影响。或者,可以根据建议设置要补偿的轴边距。下面的示例将默认的上下边距加倍,从刻度间隔的10%增加到20%

timeAxis.setUpperMargin(DateAxis.DEFAULT_UPPER_MARGIN * 2);
timeAxis.setLowerMargin(DateAxis.DEFAULT_LOWER_MARGIN * 2);
更确切地说:这是针对这些标签长度的最终解决方案,还是针对当前情况的一种特殊破解

DateAxis
使用标签的计算大小将标签置于刻度线/网格线的中心。由于字体大小因平台而异,标签大小因格式和区域设置而异,因此总有一些值组合可能会根据给定的封闭组件大小剪裁标签。调整组件的大小时,显示的标签数量将更改以优化显示。只要您允许图表随着大小的变化而调整,用户就不会有任何问题。调整示例框架的大小或使用这些内置工具查看效果

我不想硬连接数据集;即使我只有1条记录,或者我有100条记录,图表也必须看起来很好

为此,引导用户选择适合您的用例:这使用组合框侦听器切换
setVerticalTickLabels()
;您可以保留用户的首选项,如图所示。这提供了一个缩放控件工具栏。引用的示例将平移与
setMouseWheelEnabled()
相结合

作为一方,不要忽视提到的其他问题,因为它们是常见的陷阱,可能使其他问题难以孤立


我已经成功地调查并解决了这个问题

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

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

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

  • 检查您的最后一个标签是否被切割
  • 如果是切割,请更正轴的范围
  • 以下是我如何做到这一点的,在不接触JFreeChart的源代码的情况下,尽量做到最小:

    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);
        }
    }
    
    导入java.awt.Font;
    导入java.awt.FontMetrics;
    导入java.awt.Graphics2D;
    导入java.awt.font.FontRenderContext;
    导入java.awt.font.LineMetrics;
    导入java.awt.geom.Rectangle2D;
    导入java.text.DateFormat;
    导入java.util.Date;
    导入java.util.Locale;
    导入java.util.TimeZone;
    导入org.jfree.chart.axis.AxisState;
    导入org.jfree.chart.axis.DateAxis;
    导入org.jfree.chart.axis.DateTickUnit;
    导入org.jfree.chart.plot.PlotRenderingInfo;
    导入org.jfree.chart.ui.RectangleEdge;
    导入org.jfree.chart.ui.rectangleInserts;
    导入org.jfree.data.time.DateRange;
    公共类CorrectedDateAxis扩展了DateAxis{
    /**用于序列化*/
    私有静态最终长serialVersionUID=0L;
    /**
    *创建没有标签的日期轴。
    */
    公共更正日期轴(){
    超级(空);
    }
    /**
    *创建具有指定标签的日期轴。
    *
    *@param label轴标签({@code null}允许)。
    */
    公共更正日期轴(字符串标签){
    超级(标签);
    }
    /**
    *创建一个日期轴。
    *
    *@param label轴标签({@code null}允许)。
    *@param zone时区。
    *@param locale区域设置({@code null}不允许)。
    */
    公共更正日期轴(字符串标签、时区、区域设置){
    超级(标签,
    
    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.chart.ui.UIUtils;
    import org.jfree.data.time.Day;
    import org.jfree.data.time.Hour;
    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 {
    
        public TimeChart(String applicationTitle, String chartTitle) {
            super(applicationTitle);
    
            DateAxis timeAxis = new DateAxis("Timestamp");
            timeAxis.setUpperMargin(DateAxis.DEFAULT_UPPER_MARGIN * 2);
            timeAxis.setLowerMargin(DateAxis.DEFAULT_LOWER_MARGIN * 2);
            timeAxis.setDateFormatOverride(new SimpleDateFormat("YYYY-MM-dd HH:mm"));
            NumberAxis numberAxis = new NumberAxis("Number");
            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(560 , 367);
                }
            };
            add(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) {
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    TimeChart chart = new TimeChart(
                        "Date axis demo",
                        "Date axis labels are visible");
                    chart.pack();
                    UIUtils.centerFrameOnScreen(chart);
                    chart.setVisible(true);
                }
            });
        }
    }
    
    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);
        }
    }