Java使用JFreeChart绘制平滑曲线_目录
转载请注明出处!
前言
其实JFreeChart就没有绘制曲线的功能,绘制折线图是可以的,但是我们可以通过数据量增大的方式,让点足够密集变成曲线,如果没有足够的数据量,可以通过后期算法加工的方式,在两个点之间获取很多的点形成曲线,我们这里使用的是ChartFactory.createLineChart方法绘制。
使用依赖
<!-- https://mvnrepository.com/artifact/org.jfree/jfreechart -->
<dependency>
<groupId>org.jfree</groupId>
<artifactId>jfreechart</artifactId>
<version>1.0.19</version>
</dependency>
简单Demo
下面这是一个简单的Demo,代码不复杂,我也没写全备注,大家先复制下来运行成功,看懂代码,然后继续往下看。
package com.xxx.xx.action;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.block.BlockBorder;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.data.category.CategoryDataset;
import org.jfree.data.category.DefaultCategoryDataset;
import org.jfree.ui.ApplicationFrame;
import org.jfree.ui.RefineryUtilities;
import com.topscomm.dist.util.CreateChartUtil;
import java.awt.*;
public class TestWord extends ApplicationFrame {
public TestWord(String titile){
//程序入口
super(titile);
//获取曲线数据,这里面是我自己造的一条
CategoryDataset dataset=createDataset();
//再用数据获取jfreechart对象,这里使用的是createLineChart折线图
JFreeChart chart=createChart(dataset);
获取panel,可以用它来设置很多样式的参数,这里就先俺默认了
ChartPanel chartPanel=new ChartPanel(chart);
chartPanel.setPreferredSize(new Dimension(800,500));
//这个是jfreechart内部的方法
setContentPane(chartPanel);
}
private CategoryDataset createDataset(){
DefaultCategoryDataset dataset=new DefaultCategoryDataset();
dataset.addValue(212,"Letter","A");
dataset.addValue(504,"Letter","B");
dataset.addValue(1520,"Letter","C");
dataset.addValue(1842,"Letter","D");
dataset.addValue(2991,"Letter","E");
return dataset;
}
private JFreeChart createChart(CategoryDataset dataset){
JFreeChart chart= ChartFactory.createLineChart(
"Line Chart Demo",
"Category Axis",
"Value Axis",
dataset
);
return chart;
}
public static void main(String args[])throws Exception{
//程序执行从构造方法开始
TestWord demo=new TestWord("曲线示例");
//下面这些是弹框展示用的
demo.pack();
RefineryUtilities.centerFrameOnScreen(demo);
demo.setVisible(true);
}
}
运行后会弹出一个窗口来,这就是我们绘制的曲线图,其严格来说还不能称之为曲线图,但又在某种意义上可以,那么如何将其变成一个曲线图呢?请继续往下看
想要平滑曲线,就需要足够多的数据,比如说将上图的A-B切分为1000份,这样代码不用改动,只对数据进行处理,就会得到平滑的曲线,如图:
当然,具体怎么获取很多的数据,完全取决于你的算法,你想让曲线是怎样的一个斜率,都是由你怎么切分数据决定的。
复杂代码
下面再贴一段关于如何设置曲线样式的代码,下面代码无法直接运行,但里面有很详细的注释,设置样式的方式可以参考:
/**
* @description:
* @author: niu
* @date: 2022年4月7日下午4:34:03
* @modify:
*/
package com.xxx.xx.util;
import com.xxx.xx.util.dascore.ChartDto;
import com.xxx.xx.util.dascore.Serie;
import com.xxx.xx.util.dascore.DateStyle;
import com.xxx.xx.util.dascore.DateUtil;
import org.jfree.chart.*;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.block.BlockBorder;
import org.jfree.chart.labels.ItemLabelAnchor;
import org.jfree.chart.labels.ItemLabelPosition;
import org.jfree.chart.labels.StandardCategoryItemLabelGenerator;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.DefaultDrawingSupplier;
import org.jfree.chart.plot.PieLabelLinkStyle;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.renderer.category.LineAndShapeRenderer;
import org.jfree.chart.renderer.category.StandardBarPainter;
import org.jfree.chart.renderer.xy.StandardXYBarPainter;
import org.jfree.data.category.DefaultCategoryDataset;
import org.jfree.ui.RectangleInsets;
import org.jfree.ui.TextAnchor;
import org.springframework.stereotype.Service;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.text.NumberFormat;
import java.util.*;
import java.util.List;
import javax.imageio.ImageIO;
/**
* 说明:
*
* @description:
* @program:
* @author: nwd
* @create: 2022-07-18 18:05
**/
@Service
public class CreateChartUtil{
private static final String NO_DATA_MSG = "数据加载失败";
private static final Font FONT = new Font("simsun", Font.PLAIN, 12);
//颜色
/*public static Color[] CHART_COLORS = {
new Color(253,236,109),
new Color(31,129,188),
new Color(255,117,153),
new Color(158,90,102),
new Color(255,204,102)
};*/
//颜色
public Color[] CHART_COLORS = {
new Color(255,255,0),
new Color(0,255,0),
new Color(255,0,0),
new Color(0,0,255),
new Color(103, 78, 167)
};
/*
* 静态代码块
*/
static{
//setChartTheme();
}
private void setChartTheme(){
// 设置中文主题样式 解决乱码
StandardChartTheme chartTheme = new StandardChartTheme("CN");
// 设置标题字体
chartTheme.setExtraLargeFont(FONT);
// 设置图例的字体
chartTheme.setRegularFont(FONT);
// 设置轴向的字体
chartTheme.setLargeFont(FONT);
chartTheme.setSmallFont(FONT);
chartTheme.setTitlePaint(new Color(51, 51, 51));
chartTheme.setSubtitlePaint(new Color(85, 85, 85));
chartTheme.setLegendBackgroundPaint(Color.WHITE);
chartTheme.setLegendItemPaint(Color.BLACK);
chartTheme.setChartBackgroundPaint(Color.WHITE);
// 绘制颜色绘制颜色.轮廓供应商
// paintSequence,outlinePaintSequence,strokeSequence,outlineStrokeSequence,shapeSequence
Paint[] OUTLINEPAINTSEQUENCE = new Paint[]{Color.WHITE};
// 绘制器颜色源
DefaultDrawingSupplier drawingSupplier = new DefaultDrawingSupplier(CHART_COLORS, CHART_COLORS, OUTLINEPAINTSEQUENCE,
DefaultDrawingSupplier.DEFAULT_STROKE_SEQUENCE, DefaultDrawingSupplier.DEFAULT_OUTLINE_STROKE_SEQUENCE,
DefaultDrawingSupplier.DEFAULT_SHAPE_SEQUENCE);
chartTheme.setDrawingSupplier(drawingSupplier);
chartTheme.setPlotBackgroundPaint(Color.BLACK);// 绘制区域
chartTheme.setPlotOutlinePaint(Color.WHITE);// 绘制区域外边框
chartTheme.setLabelLinkPaint(new Color(8, 55, 114));// 链接标签颜色
chartTheme.setLabelLinkStyle(PieLabelLinkStyle.CUBIC_CURVE);
chartTheme.setAxisOffset(new RectangleInsets(5, 12, 5, 12));
chartTheme.setDomainGridlinePaint(new Color(192, 208, 224));// X坐标轴垂直网格颜色
chartTheme.setRangeGridlinePaint(new Color(0, 0, 0));// 用于标识Y轴刻度的横向线条颜色
chartTheme.setBaselinePaint(Color.WHITE);
chartTheme.setCrosshairPaint(Color.BLUE);// 不确定含义
chartTheme.setAxisLabelPaint(new Color(51, 51, 51));// 坐标轴标题文字颜色
chartTheme.setTickLabelPaint(new Color(67, 67, 72));// 刻度数字
chartTheme.setBarPainter(new StandardBarPainter());// 设置柱状图渲染
chartTheme.setXYBarPainter(new StandardXYBarPainter());// XYBar 渲染
chartTheme.setItemLabelPaint(Color.black);
chartTheme.setThermometerPaint(Color.white);// 温度计
ChartFactory.setChartTheme(chartTheme);
}
/**
* 创建折线图
*
* @param title 折线图标题
* @param xtitle x轴标题
* @param ytitle y轴标题
* @param categorie 横坐标类别
* @param series 数据集
* @return
* @throws Exception
*/
private ChartPanel createChart(String title, String xtitle, String ytitle, java.util.List<String> categorie, List<Map<String,Object>> series) throws Exception{
// 2:创建Chart[创建不同图形]
JFreeChart chart = ChartFactory.createLineChart(title, xtitle, ytitle, createDataset(categorie, series));
// 3:设置抗锯齿,防止字体显示不清楚
CreateChartUtil.setAntiAlias(chart);// 抗锯齿
// 4:对柱子进行渲染[[采用不同渲染]],暂时注释掉这里,如果加上的话,每个节点就会有符号标识,影响曲线效果
//CreateChartUtil.setLineRender(chart.getCategoryPlot(), true, true);
//CategoryPlot plot = chart.getCategoryPlot();
//LineAndShapeRenderer lasp = (LineAndShapeRenderer) plot.getRenderer();
// 5:对其他部分进行渲染
CreateChartUtil.setXAixs(chart.getCategoryPlot());// X坐标轴渲染
CreateChartUtil.setYAixs(chart.getCategoryPlot());// Y坐标轴渲染
// 设置标注无边框
chart.getLegend().setFrame(new BlockBorder(Color.WHITE));
//设置边框可见
chart.setBorderVisible(false);
// 6:使用chartPanel接收
return new ChartPanel(chart);
}
/**
* 必须设置文本抗锯齿
*/
public static void setAntiAlias(JFreeChart chart){
chart.setTextAntiAlias(false);
}
/**
* 设置类别图表(CategoryPlot) X坐标轴线条颜色和样式
*
*/
public static void setXAixs(CategoryPlot plot){
Color lineColor = new Color(31, 121, 170);
plot.getDomainAxis().setAxisLinePaint(lineColor);// X坐标轴颜色
plot.getDomainAxis().setTickMarkPaint(lineColor);// X坐标轴标记|竖线颜色
}
/**
* 设置类别图表(CategoryPlot) Y坐标轴线条颜色和样式 同时防止数据无法显示
*
*/
public static void setYAixs(CategoryPlot plot){
Color lineColor = new Color(192, 208, 224);
ValueAxis axis = plot.getRangeAxis();
axis.setAxisLinePaint(lineColor);// Y坐标轴颜色
axis.setTickMarkPaint(lineColor);// Y坐标轴标记|竖线颜色
// 隐藏Y刻度
axis.setAxisLineVisible(false);
axis.setTickMarksVisible(false);
// 用于标识Y轴刻度的横向线条
plot.setRangeGridlinePaint(new Color(0, 0, 0));
plot.setRangeGridlineStroke(new BasicStroke(1));
// 设置顶部Y坐标轴间距,防止数据无法显示
plot.getRangeAxis().setUpperMargin(0.1);
// 设置底部Y坐标轴间距
plot.getRangeAxis().setLowerMargin(0.1);
}
/**
* 设置折线图样式
*
* @param plot
* @param isShowDataLabels 是否显示数据标签,如果是折线图,调用此方法,如果绘制曲线图则不要调用
*/
@SuppressWarnings("deprecation")
public
static void setLineRender(CategoryPlot plot, boolean isShowDataLabels, boolean isShapesVisible){
plot.setNoDataMessage(NO_DATA_MSG);
plot.setInsets(new RectangleInsets(10, 5, 0, 5), false);
LineAndShapeRenderer renderer = (LineAndShapeRenderer) plot.getRenderer();
//renderer.setStroke(new BasicStroke(1.5F));
if(isShowDataLabels){
renderer.setBaseItemLabelsVisible(false);
renderer.setBaseItemLabelGenerator(new StandardCategoryItemLabelGenerator(StandardCategoryItemLabelGenerator.DEFAULT_LABEL_FORMAT_STRING,
NumberFormat.getInstance()));
renderer.setBasePositiveItemLabelPosition(new ItemLabelPosition(ItemLabelAnchor.OUTSIDE1, TextAnchor.BOTTOM_CENTER));
}
renderer.setBaseShapesVisible(isShapesVisible);
setXAixs(plot);
setYAixs(plot);
}
/**
* 折线图
* <p>
* 创建图表步骤:<br/>
* 1:创建数据集合<br/>
* 2:创建Chart:<br/>
* 3:设置抗锯齿,防止字体显示不清楚<br/>
* 4:对柱子进行渲染,<br/>
* 5:对其他部分进行渲染<br/>
* 6:使用chartPanel接收<br/>
*
* </p>
*/
//创建折线图图表
private DefaultCategoryDataset createDataset(List<String> categorie, List<Map<String,Object>> series){
// 标注类别
String[] categories = categorie.toArray(new String[0]);
// 1:创建数据集合
return CreateChartUtil.createDefaultCategoryDataset(series, categories);
}
/**
* 创建类别数据集合
*/
private static DefaultCategoryDataset createDefaultCategoryDataset(List<Map<String,Object>> series, String[] categories){
DefaultCategoryDataset dataset = new DefaultCategoryDataset();
for(Map<String,Object> serieMap : series){
Serie serie = new Serie().fromMap(serieMap);
String name = serie.getLabel();
List<Object> data = serie.getValue();
if(data != null && categories != null && data.size() == categories.length){
for(int index = 0; index < data.size(); index++){
String value = data.get(index) == null ? "" : data.get(index).toString();
if(isPercent(value)){
value = value.substring(0, value.length() - 1);
}
if(isNumber(value)){
dataset.setValue(Double.parseDouble(value), name, categories[index]);
}
}
}
}
return dataset;
}
/**
* 是不是一个%形式的百分比
*
* @param str
* @return
*/
private static boolean isPercent(String str){
return str != null && (str.endsWith("%") && isNumber(str.substring(0, str.length() - 1)));
}
/**
* 是不是一个数字
*
* @param str
* @return
*/
private static boolean isNumber(String str){
return str != null && str.matches("^[-+]?(([0-9]+)((([.]{0})([0-9]*))|(([.]{1})([0-9]+))))$");
}
public File drawChart(ChartDto chart,String outputPath, Color[] chartColorParam)throws Exception{
CHART_COLORS = chartColorParam;
setChartTheme();
ChartPanel chartPanel = createChart(chart.getTitle(), chart.getXAxis(), chart.getYAxis(), chart.getCategories(), chart.getSeries());
return saveAsFile(chartPanel.getChart(), outputPath);
}
/**
* 将图表保存为PNG、JPEG图片
*
* @param chart 折线图对象
* @param outputPath 文件保存路径, 包含文件名
* @throws Exception
*/
private File saveAsFile(JFreeChart chart, String outputPath) throws Exception{
FileOutputStream out;
try{
File outFile = new File(outputPath);
if(!outFile.getParentFile().exists()){
outFile.getParentFile().mkdirs();
}
out = new FileOutputStream(outputPath);
// 保存为PNG
ChartUtilities.writeChartAsPNG(out, chart, 1800, 500);
out.flush();
out.close();
return outFile;
} catch(IOException e){
// do nothing
e.printStackTrace();
return null;
}
}
/**
* 将几张图片竖向合成一个
* @param folderPath
* @Author: niuwenda
*/
public static boolean mergeVertical(List<File> files, String path){
try {
Integer allWidth = 0; //计算画布总宽
Integer allHeight = 0; //计算画布总高
List<BufferedImage> imgs = new ArrayList<>();
for(int i=0; i<files.size(); i++){
imgs.add(ImageIO.read(files.get(i)));
//因为是竖向合成,拿图片里最大的一个宽度就行
allWidth = Math.max(allWidth,imgs.get(i).getWidth());
allHeight += imgs.get(i).getHeight();
}
BufferedImage combined = new BufferedImage(allWidth, allHeight,BufferedImage.TYPE_INT_RGB);
Graphics g = combined.getGraphics();
//设置画布背景颜色 ,默认黑色
g.setColor(Color.white);
g.fillRect(0, 0, allWidth, allHeight);
Integer height = 0;
for(int i=0; i< imgs.size(); i++){
g.drawImage(imgs.get(i), 0, height,null);
//+10为了设置上下两个图片间距
height += imgs.get(i).getHeight();
}
//关流防止内存溢出
g.dispose();
ImageIO.write(combined, "png", new File(path));
System.out.println("===图片竖向合成成功====");
return true;
} catch (Exception e) {
System.out.println("===图片竖向合成失败====");
e.printStackTrace();
return false;
}
}
//字符串类型图片转换
public static BufferedImage base64StringToImg(String base64String) {
try {
byte[] bytes = Base64.getDecoder().decode(base64String);
ByteArrayInputStream in = new ByteArrayInputStream(bytes);
return ImageIO.read(in);
} catch (final IOException ioe) {
throw new UncheckedIOException(ioe);
}
}
public static void main(String[] args){
try{
ChartDto chartDto = new ChartDto().fromJsonStr("{\"DATE\":\"2020-08-12 19:26:23\",\"SERIES\":[{\"LABEL\":\"A\",\"VALUE\":[\"391.03522\",\"385.38327\",\"386.1623\",\"388.39557\",\"389.19617\",\"386.86725\",\"383.91547\",\"384.25452\",\"379.57465\",\"381.97986\",\"383.04315\",\"383.62997\",\"386.57635\",\"383.42267\",\"382.4391\",\"382.73096\",\"382.76874\",\"384.54282\",\"385.13815\",\"386.9652\",\"385.81213\",\"386.14087\",\"380.2163\",\"380.27798\",\"380.99316\",\"381.51575\",\"381.52173\",\"131.59659\",\"0.6947088\",\"0.6007847\"]},{\"LABEL\":\"B\",\"VALUE\":[\"278.26807\",\"277.34164\",\"282.38678\",\"278.49786\",\"277.28955\",\"277.8999\",\"278.23306\",\"276.3892\",\"277.19434\",\"276.4134\",\"274.7296\",\"276.294\",\"273.78204\",\"275.7772\",\"278.87714\",\"277.34637\",\"274.43817\",\"278.10522\",\"276.18378\",\"277.3299\",\"274.9383\",\"275.388\",\"277.2262\",\"276.70746\",\"273.91785\",\"273.8313\",\"274.09454\",\"82.66885\",\"0.5141127\",\"0.50791365\"]},{\"LABEL\":\"C\",\"VALUE\":[\"227.23041\",\"229.29349\",\"227.69107\",\"227.8791\",\"228.49219\",\"229.2131\",\"227.93292\",\"227.7579\",\"228.23328\",\"228.41928\",\"228.08829\",\"228.5531\",\"226.71812\",\"228.02512\",\"227.48405\",\"226.31848\",\"228.68155\",\"228.44812\",\"227.94458\",\"228.10205\",\"227.5925\",\"227.20189\",\"227.05502\",\"226.98502\",\"225.91888\",\"226.94777\",\"226.73824\",\"82.26399\",\"0.60032165\",\"0.5139754\"]}],\"TITLE\":\"9992 electric intensity\",\"X_AXIS\":\"\",\"CATEGORIES\":[\"19:11\",\"19:12\",\"19:13\",\"19:14\",\"19:15\",\"19:16\",\"19:17\",\"19:18\",\"19:19\",\"19:20\",\"19:21\",\"19:22\",\"19:23\",\"19:24\",\"19:25\",\"19:26\",\"19:27\",\"19:28\",\"19:29\",\"19:30\",\"19:31\",\"19:32\",\"19:33\",\"19:34\",\"19:35\",\"19:36\",\"19:37\",\"19:38\",\"19:39\",\"19:40\"],\"Y_AXIS\":\"Electric Intensity\",\"ADDR\":\"9992\"}");
CreateChartUtil service = new CreateChartUtil();
Collections.replaceAll(chartDto.getCategories(), DateUtil.dateToString(chartDto.getDate(), DateStyle.HH_MM), "★");
Collections.replaceAll(chartDto.getCategories(), "19:37", "☆");
service.drawChart(chartDto,
"d:\\111.png");
/*List<File> files = new ArrayList();
File x = new File("D:\\temp\\testrename\\U.png");
File y = new File("D:\\temp\\testrename\\I.png");
files.add(x);
files.add(y);
String path = "D:\\temp\\testrename\\3.png";
mergeVertical(files,path);*/
}catch(Exception e){
e.printStackTrace();
}
}
}
拓展:如何得到你刚画出来的曲线的导数图?
在数学上,求一个点的导数是有公式的,假如曲线上有两个横坐标无限接近的点(n,w)和(n+x,w+y)
那么(n,w)点的导数就是(w+y-w)/(n+x-n)=y/w,同样的,我们上面已经得到了大量的点坐标,并且已经形成曲线,这就说明我们的点是离的够近的,我们可以用这种算法,将数据都循环计算一遍,从而得到一个曲线的导数的曲线。
注意:这样算是有误差的,但是曲线的形状基本上是对的,如果你追求非常精确的话,请再研究一下别的方法。
存疑:很奇怪的是,数学上是按照我刚才说的那么算,但是公司的算法部有个大佬告诉我,直接用w+y-w就是导数,而且我问他为什么这么做,他也不告诉我,如果有朋友知道的话麻烦告诉我一下,我的曲线是个sin正弦函数,可能是因为这个。