众所周知,用jfreechart生成图片有很多问题,例如:x轴数据量多一点时,直接显示成了省略号
,y轴描述无法放到y轴正上方等等,另外jfreechart默认的样式也是比较丑的,如下:
jfreechart在画x轴标签时,会根据图片设置的宽度、边距,以及x轴坐标数计算出一个平均值,即是每个坐标占用的最大宽度,超过这个宽度,会继续判断是否设置了参数:
//标签最大换行数
int maximumCategoryLabelLines
默认为0,不设置这个参数的话就不会进行换行,就会直接显示省略号。因此解决思路之一就是设置这个换行参数,或者适当增加图片宽度,可以在一定范围内解决x轴省略号的问题。但是当x轴数量太多,比如一张折线图有几百个数据点,也还是会超出可显示的范围。
对于横坐标出现重叠,也没有显示省略号,也没有换行显示的问题,一般是这个最大显示宽度计算出错的问题。
另外一种解决思路我是在网上找到的,就是对横坐标进行采样,不过经过了我自己的改良,那就是对jfreechart画x轴标签的代码进行修改覆盖,我们写一个类继承jfreechart包里面的
org.jfree.chart.axis.CategoryAxis
并重写refreshTicks方法,以便自己实现添加x轴标签的逻辑,重写的代码类如下:
import org.jfree.chart.axis.*;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.ui.RectangleEdge;
import org.jfree.text.*;
import java.awt.*;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.List;
public class IntervalCategoryAxis extends CategoryAxis {
//抽样得到的数据索引
private List indexes;
//x轴标签允许的换行数
private int line ;
public IntervalCategoryAxis(List indexes,int line) {
this.indexes = indexes;
this.line = line;
}
private float getFloat(CategoryLabelPosition position,Rectangle2D dataArea, RectangleEdge edge){
float l;
if (position.getWidthType() == CategoryLabelWidthType.CATEGORY) {
/*因为我们只显示了采样到的数据,因此这里计算最大宽度时根据样品数计算
否则将导致标签换行不生效*/
l = (float) calculateCategorySize(indexes.size(), dataArea,edge);
}else {
if (RectangleEdge.isLeftOrRight(edge)) {
l = (float) dataArea.getWidth();
} else {
l = (float) dataArea.getHeight();
}
}
return l;
}
/**
* 重写获取横坐标的方法,只显示进行均匀采样得到的坐标
*/
@Override
public List refreshTicks(Graphics2D g2, AxisState state, Rectangle2D dataArea, RectangleEdge edge) {
List ticks = new ArrayList<>();
if (dataArea.getHeight() <= 0.0 || dataArea.getWidth() < 0.0) {
return ticks;
}
CategoryPlot plot = (CategoryPlot) getPlot();
List> categories = plot.getCategoriesForAxis(this);
double max = 0.0;
if (categories != null) {
CategoryLabelPosition position = super.getCategoryLabelPositions().getLabelPosition(edge);
float r = super.getMaximumCategoryLabelWidthRatio();
if (r <= 0.0) {
r = position.getWidthRatio();
}
float l = getFloat(position,dataArea,edge);
//遍历所有数据
for (int i = 0; i < categories.size(); i++) {
Object o = categories.get(i);
Comparable> category = (Comparable>) o;
g2.setFont(getTickLabelFont(category));
TextBlock label;
if(line > 0){
label = createLabel(category, l * r, edge, g2);
}else{
label = new TextBlock();
label.addLine(category.toString(), getTickLabelFont(category), getTickLabelPaint(category));
}
if (edge == RectangleEdge.TOP || edge == RectangleEdge.BOTTOM) {
max = Math.max(max, calculateTextBlockHeight(label, position, g2));
} else if (edge == RectangleEdge.LEFT || edge == RectangleEdge.RIGHT) {
max = Math.max(max, calculateTextBlockWidth(label, position, g2));
}
//只添加采样的标签,对数据渲染不会有影响
if(indexes.contains(i)){
Tick tick = new CategoryTick(category, label, position.getLabelAnchor(), position.getRotationAnchor(), position.getAngle());
ticks.add(tick);
}
}
}
state.setMax(max);
return ticks;
}
}
然后对横坐标标签进行采样展示,我们从大量的数据中只抽取固定数量的数据作为标签,然后添加到图片上。我这里使用的采样是均匀采样,具体想如何采样可以自己去实现,附上我自己写的采样工具类:
package com.zhou.chart.util;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 实现均匀采样(保留首尾)
* @author lang.zhou
* @date 2022/6/23 14:34
*/
public class HeadTailSample extends Sample{
public HeadTailSample(List sampleData, int sampleTotal) {
this.sampleData = sampleData;
this.sampleTotal = sampleTotal;
this.sampleIndex = new ArrayList<>(sampleTotal);
}
public void execute(){
int size = sampleData.size();
float sec = (size+0.0f)/(sampleTotal - 1);
if(size <= sampleTotal){
return;
}
sampleIndex.add(0);
float mark = sec;
for (int i = 1; i < size; i++) {
float cu = i + 0.0f;
float rate = Math.abs(mark - cu);
//补偿因子
if(rate < 1){
sampleIndex.add(i);
//保证采样数量不多于sampleTotal
if(sampleIndex.size()>= sampleTotal){
break;
}
mark+= sec;
}
}
if(!sampleIndex.contains(size - 1)){
if(sampleIndex.size()>= sampleTotal){
sampleIndex.remove(sampleIndex.size() - 1);
}
sampleIndex.add(size - 1);
}
}
public static void main(String[] args) {
//5
test(3,1,2,3,4,5,6,7,8);
test(5,1,2,3,4,5);
test(5,1,2,3,4,5,6);
test(5,1,2,3,4,5,6,7);
test(5,1,2,3,4,5,6,7,8);
test(5,1,2,3,4,5,6,7,8,9);
test(5,1,2,3,4,5,6,7,8,9,10);
test(5,1,2,3,4,5,6,7,8,9,10,11);
test(5,1,2,3,4,5,6,7,8,9,10,11,12);
test(5,1,2,3,4,5,6,7,8,9,10,11,12,13);
test(5,1,2,3,4,5,6,7,8,9,10,11,12,13,14);
test(5,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15);
}
private static void test(int cnt,Integer ... a){
List l = Arrays.asList(a);
Sample sample = new HeadTailSample<>(l, cnt);
sample.sample();
List list = sample.getResult();
System.out.println(Arrays.toString(list.toArray()));
}
}
采样的关键代码如下,list为数据,使用采样工具进行采样:
//对List
这样就能完美解决x轴显示不全的问题了,附上我测试生成的效果图:
注意:设置了x轴标签倾斜后,x轴标签换行就不会生效了,但是我们仍然可以设置最多显示固定数量的标签。
我这里对图片样式进行了美化,去掉了多余的边框,0轴显示实现,还修改了图形颜色和字体。
对于y轴描述无法放到y轴正上方,只能居中的问题,我的解决思路如下:
1. 隐藏原始的y轴标签
2. 自定义画一个标签
我们可以将自带的y轴标签设置为不显示,然后自己创建一个title,放到y轴的正上方,这样便能解决问题:
JFreeChart chart = ChartFactory.createLineChart(
"",
//不设置x轴和y轴描述,就默认不显示描述
"",
"",
data,
PlotOrientation.VERTICAL,
//是否展示图例
isShowLegend,
false,
false);
//图片标题
TextTitle textTitle = new TextTitle(title,titleFont);
textTitle.setPaint(Color.decode(color));
textTitle.setMargin(0,0,0,0);
chart.setTitle(textTitle);
List titles = new ArrayList<>(3);
//自己画一个y轴描述
TextTitle yn = null;
if(StringUtils.isNotBlank(yName)){
yn = new TextTitle(yName,font);
yn.setPaint(Color.decode(color));
//放到左边
yn.setHorizontalAlignment(HorizontalAlignment.LEFT);
titles.add(yn);
}
//设置x轴描述
TextTitle xn = null;
if(StringUtils.isNotBlank(xName)){
xn = new TextTitle(xName,font);
//x轴描述放到右边
xn.setHorizontalAlignment(HorizontalAlignment.RIGHT);
//x轴描述放到底部
xn.setPosition(RectangleEdge.BOTTOM);
xn.setPaint(Color.decode(color));
xn.setMargin(0,10,0,10);
titles.add(xn);
}
if(titles.size() > 0){
//设置额外标题
chart.setSubtitles(titles);
}
注意:多个标题的显示顺序和添加到list中的顺序有关系,如果生成的图片中标签文字先后顺序有问题,可以对添加的顺序进行调整。