JFreeChart X 轴标签超出图表区域
JFreeChart X axis labels are going out of chart area
我有一个 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 );
}
}
这是当前的截图;问题可以在最后一个标签上看到:
是什么原因导致最后一个标签呈现在当前图表区域之外?另外,我该如何预防?
更新
这是一个包含屏幕截图和所有详细信息的更全面的示例。
根据@trashgod的评论,我已经更新到最新的JFreeChart Engine
(jfreechart-1.0.19.jar 和 jcommon-1.0.23.jar)
(jfreechart-1.6.0-snapshot.jar).
考虑这个例子(它非常依赖@trashgod 的建议 - 非常感谢):
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
* @see
*/
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,这里是截图:
但是,如果我将 lot_of_values 更改为 true(这将向集合中添加更多数据 - 您可以在源),将剪切域轴的最后一个标签。这是 lot_of_values=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));
现在我将继续寻找计算此 MaximumDate 的位置。如果有人知道,请告诉我。
该效果是人为减小图表的首选尺寸同时显式增大日期轴刻度标签尺寸造成的。请注意,省略对 setPreferredSize()
的调用会消除效果。或者,您可以按照建议 here 设置轴边距进行补偿。下面的示例将默认上限和下限边距加倍,从报价间隔的 10% 到 20%。
timeAxis.setUpperMargin(DateAxis.DEFAULT_UPPER_MARGIN * 2);
timeAxis.setLowerMargin(DateAxis.DEFAULT_LOWER_MARGIN * 2);
To make it more exact: is it an ultimate solution for these label lengths or just a particular hack for the current situation?
DateAxis
使用标签的计算大小使标签在 tick/gridline 上居中。由于字体大小因平台而异,标签大小因格式和区域设置而异,因此对于给定的封闭组件大小,总会有一些值的组合可能会裁剪标签。随着组件的大小调整,显示的标签数量将发生变化以优化显示。只要让图表随着大小的变化进行调整,讨论here, users will have no trouble. Resize the example's frame or use these built-in 看看效果。
I do not want to hard-wire the dataset; the chart has to look good with all record counts—even if I have only 1 record, or I have 100s.
为此,引导用户 suitable to your use case: This example uses a combo box listener to toggle setVerticalTickLabels()
; you can persist the user's preference as shown . This offers a toolbar of zoom controls. The examples cited here将平移与setMouseWheelEnabled()
相结合。
另一方面,不要忽视提到的其他问题 ,因为它们是常见的陷阱,可能会使其他问题难以隔离。
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
* @see
*/
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);
}
});
}
}
我已经成功调查并解决了这个问题。
当 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);
}
}
这是对 DateAxis class 的覆盖,它完成上述两项任务。
注意,这个子class包含很多从JFreeChart的DateAxis class复制的代码,因为他们定义了estimateMaximumTickLabelWidth 是私有的,所以 subclasses 不能访问它。
你可以修改原来的DateAxisclass来定义这个函数为protected,这样你就可以在这个subclass.
中跳过这个函数
下面是 CorrectedDateAxis 进入图片并更正 DateAxis 范围时的样子:
最后没有更多错误的标签!
我有一个 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 );
}
}
这是当前的截图;问题可以在最后一个标签上看到:
是什么原因导致最后一个标签呈现在当前图表区域之外?另外,我该如何预防?
更新
这是一个包含屏幕截图和所有详细信息的更全面的示例。
根据@trashgod的评论,我已经更新到最新的JFreeChart Engine
(jfreechart-1.0.19.jar 和 jcommon-1.0.23.jar)
(jfreechart-1.6.0-snapshot.jar).
考虑这个例子(它非常依赖@trashgod 的建议 - 非常感谢):
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
* @see
*/
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,这里是截图:
但是,如果我将 lot_of_values 更改为 true(这将向集合中添加更多数据 - 您可以在源),将剪切域轴的最后一个标签。这是 lot_of_values=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));
现在我将继续寻找计算此 MaximumDate 的位置。如果有人知道,请告诉我。
该效果是人为减小图表的首选尺寸同时显式增大日期轴刻度标签尺寸造成的。请注意,省略对 setPreferredSize()
的调用会消除效果。或者,您可以按照建议 here 设置轴边距进行补偿。下面的示例将默认上限和下限边距加倍,从报价间隔的 10% 到 20%。
timeAxis.setUpperMargin(DateAxis.DEFAULT_UPPER_MARGIN * 2);
timeAxis.setLowerMargin(DateAxis.DEFAULT_LOWER_MARGIN * 2);
To make it more exact: is it an ultimate solution for these label lengths or just a particular hack for the current situation?
DateAxis
使用标签的计算大小使标签在 tick/gridline 上居中。由于字体大小因平台而异,标签大小因格式和区域设置而异,因此对于给定的封闭组件大小,总会有一些值的组合可能会裁剪标签。随着组件的大小调整,显示的标签数量将发生变化以优化显示。只要让图表随着大小的变化进行调整,讨论here, users will have no trouble. Resize the example's frame or use these built-in
I do not want to hard-wire the dataset; the chart has to look good with all record counts—even if I have only 1 record, or I have 100s.
为此,引导用户setVerticalTickLabels()
; you can persist the user's preference as shown setMouseWheelEnabled()
相结合。
另一方面,不要忽视提到的其他问题
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
* @see
*/
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);
}
});
}
}
我已经成功调查并解决了这个问题。
当 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);
}
}
这是对 DateAxis class 的覆盖,它完成上述两项任务。
注意,这个子class包含很多从JFreeChart的DateAxis class复制的代码,因为他们定义了estimateMaximumTickLabelWidth 是私有的,所以 subclasses 不能访问它。
你可以修改原来的DateAxisclass来定义这个函数为protected,这样你就可以在这个subclass.
中跳过这个函数下面是 CorrectedDateAxis 进入图片并更正 DateAxis 范围时的样子:
最后没有更多错误的标签!