maven
<dependency>
<groupId>org.jfree</groupId>
<artifactId>jfreechart</artifactId>
<version>1.5.2</version>
</dependency>
图表数据封装
import lombok.Data;
import java.awt.*;
import java.util.Map;
/**
* 图表数据封装
* @author liaozesong
*/
@Data
public class ChartData {
/**
* 分组
*/
private String group;
/**
* 折线名称
*/
private String title;
/**
* 最小值
*/
private Double min;
/**
* 最大值
*/
private Double max;
/**
* 折线颜色
*/
private Color color;
/**
* 折线数据
* key->x轴
* value->y轴
*/
private Map<Long, Double> data;
/**
* 除data外所有属性复制
*/
public ChartData copyBase() {
ChartData chartData = new ChartData();
chartData.setGroup(group);
chartData.setTitle(title);
chartData.setColor(color);
chartData.setMax(max);
chartData.setMin(min);
return chartData;
}
}
扩展 ChartFactory
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.plot.CombinedDomainXYPlot;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.chart.title.TextTitle;
import org.jfree.chart.title.Title;
import org.jfree.chart.ui.RectangleInsets;
import org.jfree.data.xy.CategoryTableXYDataset;
import java.awt.*;
import java.awt.geom.Ellipse2D;
import java.text.FieldPosition;
import java.text.NumberFormat;
import java.text.ParsePosition;
import java.util.Comparator;
import java.util.List;
/**
* 扩展 ChartFactory
* @author liaozesong
*/
@Slf4j
public class GtChartFactory extends ChartFactory {
public static JFreeChart createLineChart(String title, List<List<ChartData>> data, Long minValues, Long maxValues) {
NumberAxis xAxis = new NumberAxis();
setX(xAxis, minValues, maxValues);
CombinedDomainXYPlot plots = new CombinedDomainXYPlot(xAxis);
setCombinedPlot(plots);
for (int groupIndex = 0; groupIndex < data.size(); groupIndex++) {
CategoryTableXYDataset dataset = new CategoryTableXYDataset();
XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer();
List<ChartData> datum = data.get(groupIndex);
//扩展的y轴 支持 \n 换行 label
LineNumberAxis yAxis = new LineNumberAxis(datum.get(0).getGroup());
setY(yAxis, datum);
for (int i = 0; i < datum.size(); i++) {
ChartData chartData = datum.get(i);
chartData.getData().forEach((key, value) -> dataset.add(key, value, chartData.getTitle()));
setRenderer(renderer, i, chartData.getColor());
}
XYPlot xyPlot = new XYPlot(dataset, groupIndex == 0 ? xAxis : null, yAxis, renderer);
setPlot(xyPlot);
plots.add(xyPlot);
}
JFreeChart chart = new JFreeChart(null, JFreeChart.DEFAULT_TITLE_FONT,
plots, true);
setChart(title, chart);
return chart;
}
/**
* 设置折线线段
*/
private static void setRenderer(XYLineAndShapeRenderer renderer, int index, Color color) {
//要求单点也要显示
renderer.setSeriesShape(index, new Ellipse2D.Double(-0.25, -0.25, 0.5, 0.5));
//设置折线颜色
if (null != color) {
renderer.setSeriesPaint(index, color);
}
}
/**
* 设置画图数据区域
*/
private static void setPlot(XYPlot xyPlot) {
//要求不显示边框
xyPlot.setOutlineVisible(false);
}
/**
* 设置多图合并区域
*/
private static void setCombinedPlot(CombinedDomainXYPlot plots) {
//图表无间隔
plots.setGap(0);
}
/**
* 设置x轴
*/
private static void setX(NumberAxis numberAxis, Long minValues, Long maxValues) {
numberAxis.setAutoRange(false);
numberAxis.setRange(minValues, maxValues);
//格式化刻度
numberAxis.setNumberFormatOverride(new ChartKmFormat());
}
/**
* 设置y轴
*/
private static void setY(NumberAxis numberAxis, List<ChartData> data) {
//y轴 label 由默认纵向转为横向
numberAxis.setLabelAngle(Math.PI / 2.0);
numberAxis.setLabelFont(new Font("宋体", Font.PLAIN, 8));
if (CollUtil.isEmpty(data)) {
return;
}
//根据配置 设置范围;无配置时 自适应
try {
Double min = data.stream().min(Comparator.comparing(ChartData::getMin)).get().getMin();
Double max = data.stream().max(Comparator.comparing(ChartData::getMax)).get().getMax();
numberAxis.setAutoRange(false);
numberAxis.setRange(min, max);
} catch (Exception e) {
}
}
/**
* 设置图表样式
*/
private static void setChart(String title, JFreeChart chart) {
chart.setBackgroundPaint(Color.white);
chart.getLegend().setItemFont(new Font("宋体", Font.PLAIN, 8));
//防止A4打印时,底部无墨
chart.getLegend().setPadding(new RectangleInsets(1, 1, 20, 1));
chart.setTitle(new TextTitle(
title,
new Font("宋体", Font.PLAIN, 12),
TextTitle.DEFAULT_TEXT_PAINT, Title.DEFAULT_POSITION,
Title.DEFAULT_HORIZONTAL_ALIGNMENT,
Title.DEFAULT_VERTICAL_ALIGNMENT,
//防止A4打印时,顶部无墨
new RectangleInsets(20, 1, 1, 1)
));
}
/**
* 自定义x轴 刻度 转换
* 23311000 -> K23+311
*/
static class ChartKmFormat extends NumberFormat {
@Override
public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) {
number = number / 1000;
int v = (int) number / 1000;
int v1 = (int) number % 1000;
return toAppendTo.append(StrUtil.format("K{}+{}", v, v1));
}
@Override
public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) {
number = number / 1000;
int v = (int) number / 1000;
int v1 = (int) number % 1000;
return toAppendTo.append(StrUtil.format("K{}+{}", v, v1));
}
@Override
public Number parse(String source, ParsePosition parsePosition) {
return source.length();
}
}
}
扩展NumberAxis label 换行
import cn.hutool.core.util.StrUtil;
import org.jfree.chart.axis.AxisLabelLocation;
import org.jfree.chart.axis.AxisState;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.text.TextUtils;
import org.jfree.chart.ui.RectangleEdge;
import org.jfree.chart.ui.RectangleInsets;
import org.jfree.chart.ui.TextAnchor;
import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.font.LineMetrics;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
/**
* 扩展NumberAxis
* y轴 label 换行 \n
* @author liaozesong
*/
public class LineNumberAxis extends NumberAxis {
public LineNumberAxis(String label) {
super(label);
}
/**
* 重写Axis drawLabel方法
*/
@Override
protected AxisState drawLabel(String label, Graphics2D g2,
Rectangle2D plotArea, Rectangle2D dataArea, RectangleEdge edge,
AxisState state) {
if (StrUtil.isEmpty(label)) {
return state;
}
Font font = getLabelFont();
RectangleInsets insets = getLabelInsets();
g2.setFont(font);
g2.setPaint(getLabelPaint());
FontMetrics fm = g2.getFontMetrics();
Rectangle2D labelBounds = TextUtils.getTextBounds(label, g2, fm);
AffineTransform t = AffineTransform.getRotateInstance(
getLabelAngle() - Math.PI / 2.0, labelBounds.getCenterX(),
labelBounds.getCenterY());
Shape rotatedLabelBounds = t.createTransformedShape(labelBounds);
labelBounds = rotatedLabelBounds.getBounds2D();
double labelx = state.getCursor()
- insets.getRight() - labelBounds.getWidth() / 2.0;
double labely = labelLocationY(AxisLabelLocation.MIDDLE, dataArea);
TextAnchor anchor = labelAnchorV(AxisLabelLocation.MIDDLE);
drawAlignedString(label, g2, (float) labelx,
(float) labely,
anchor);
state.cursorLeft(insets.getLeft() + labelBounds.getWidth()
+ insets.getRight());
return state;
}
public static void drawAlignedString(String text, Graphics2D g2,
float x, float y, TextAnchor anchor) {
Rectangle2D textBounds = new Rectangle2D.Double();
float[] adjust = deriveTextBoundsAnchorOffsets(g2, text, anchor,
textBounds);
textBounds.setRect(x + adjust[0], y + adjust[1] + adjust[2],
textBounds.getWidth(), textBounds.getHeight());
//根据 \n 换行显示
String[] split = StrUtil.split(text, "\n");
for (int i = 0; i < split.length; i++) {
g2.drawString(split[i], x + adjust[0], y + adjust[1] + 10 * i);
}
}
/**
* 无修改
*/
private static float[] deriveTextBoundsAnchorOffsets(Graphics2D g2,
String text, TextAnchor anchor, Rectangle2D textBounds) {
float[] result = new float[3];
FontRenderContext frc = g2.getFontRenderContext();
Font f = g2.getFont();
FontMetrics fm = g2.getFontMetrics(f);
Rectangle2D bounds = TextUtils.getTextBounds(text, g2, fm);
LineMetrics metrics = f.getLineMetrics(text, frc);
float ascent = metrics.getAscent();
result[2] = -ascent;
float halfAscent = ascent / 2.0f;
float descent = metrics.getDescent();
float leading = metrics.getLeading();
float xAdj = 0.0f;
float yAdj = 0.0f;
if (anchor.isHorizontalCenter()) {
xAdj = (float) -bounds.getWidth() / 2.0f;
} else if (anchor.isRight()) {
xAdj = (float) -bounds.getWidth();
}
if (anchor.isTop()) {
yAdj = -descent - leading + (float) bounds.getHeight();
} else if (anchor.isHalfAscent()) {
yAdj = halfAscent;
} else if (anchor.isVerticalCenter()) {
yAdj = -descent - leading + (float) (bounds.getHeight() / 2.0);
} else if (anchor.isBaseline()) {
yAdj = 0.0f;
} else if (anchor.isBottom()) {
yAdj = -metrics.getDescent() - metrics.getLeading();
}
if (textBounds != null) {
textBounds.setRect(bounds);
}
result[0] = xAdj;
result[1] = yAdj;
return result;
}
}
调用
/**
* @author liaozesong
*/
public class ChartUtil {
public static byte[] getChart(String title, List<List<ChartData>> data, Long min, Long max) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
JFreeChart chart = GtChartFactory.createLineChart(title, data, min, max);
//2的倍数保存指定大小图片,防止图片模糊
ChartUtils.writeScaledChartAsPNG(outputStream, chart, 842, 595, 2, 2);
return outputStream.toByteArray();
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
IoUtil.close(outputStream);
}
}
}