根据实际业务需要,有时可能需要在同一个页面中显示多个图形,例如下图:
这个时候怎么去处理,一种比较简单粗暴但却很LOW的方法是:在前端页面复制同样的代码,通过多个ajax请求去获取数据并渲染视图。一两个图形还行,七八个的时候就会显得代码臃肿不堪,让人不忍直视。针对这个问题,我们在本篇文章中进行探讨研究,以供参考。
1、实现流程
整个流程中的重难点在于:
(1) 封装多个图形的数据集进行返回,包括图形的名称、顺序等;
(2) 对同一对象中的几个属性进行图形绘制。
2、数据组装
步骤1:从数据库读取初始数据;
步骤2:提取横坐标信息(日期),提取分组信息(各产品-虚构)
步骤3:初始化各图表数据
步骤4:将各分组下的各图表信息按照横坐标进行赋值,不存在的设置为0。例如:分组1在12-1至12-31每天的收入、成本和利润
步骤5:循环步骤4,直到所有分组都归纳到各图标中
步骤6:封装所有数据,生成绘制多图表的数据集。
3、前端代码
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="${pageContext.request.contextPath}/static/js/jquery.min.js"></script>
<script type="text/javascript" src="${pageContext.request.contextPath}/static/js/echarts-all.js"></script>
</head>
<title>EChart绘制多折线图</title>
<div id="chart">
<div id="countChart0" style="height:240px;"></div>
<div id="countChart1" style="height:240px;"></div>
<div id="countChart2" style="height:240px;"></div>
</div>
</body>
<script type="text/javascript">
$().ready(
function(){
//通过Ajax获取数据
$.ajax({
type: "post",
async: true, //同步执行
url: "showEChartLine.do",
dataType: "json", //返回数据形式为json
success: function (result) {
if (result) {
//将返回的category和series对象赋值给options对象内的category和series
//因为xAxis是一个数组 这里需要是xAxis[i]的形式
for (i = 0; i < 3; i++) {
var myChart = echarts.init(document.getElementById('countChart' + i));
myChart.showLoading();
var options = {
color: {
data: result[i].eChartData.color
},
title: {
x: 'center', // 'center' | 'left' | {number},
y: 'top', // 'center' | 'bottom' | {number}
text: result[i].title
},
tooltip: {
trigger: 'axis'
},
legend: {
orient: 'horizontal', // 'vertical'
x: 'center', // 'center' | 'left' | {number},
y: 'bottom', // 'center' | 'bottom' | {number}
data: result[i].eChartData.legend
},
toolbox: {
show: true,
feature: {
mark: false
}
},
calculable: true,
xAxis: [{
type: 'category',
show: false,
data: result[i].eChartData.category
}],
yAxis: [{
type: 'value',
splitArea: {
show: true
}
}],
series: [{
barWidth: '60%'
}]
};
options.series = result[i].eChartData.series;
myChart.hideLoading();
myChart.setOption(options);
}
}
},
error: function (errorMsg) {
alert("图表请求数据失败啦!");
}
});
});
</script>
</html>
本文中主要是同时绘制三个图表,所以直接定义了三个<div>标签,通过ajax请求后端获取数据集,遍历数据集并生成图表。其中,图表的标题、各分组的颜色、横坐标与纵坐标等都是通过返回的对象属性进行获取。
4、注解定义
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ChineseName {
String value() default "";
}
该注解的作用主要是用于标注对象中哪些属性需要绘制图表,且设定好图表标题。
5、实体对象
public class ProductProfit implements Serializable {
private String date;
private String source;
@Order(1)
@ChineseName("收入(元)")
private Double total;
@Order(2)
@ChineseName("成本(元)")
private Double principal;
@Order(3)
@ChineseName("利润(元)")
private Double profit;
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public String getSource() {
return source;
}
public void setSource(String source) {
this.source = source;
}
public Double getTotal() {
return total;
}
public void setTotal(Double total) {
this.total = total;
}
public Double getPrincipal() {
return principal;
}
public void setPrincipal(Double principal) {
this.principal = principal;
}
public Double getProfit() {
return profit;
}
public void setProfit(Double profit) {
this.profit = profit;
}
}
其中@Order定义了各图表在页面中的显示顺序,@ChineseName定义了对象中需要绘制图表的属性与标题。
6、数据封装类
Series.java
import java.util.List;
public class Series<T> {
public String name;
public String type;
public List<T> data;// 这里要用int 不能用String 不然前台显示不正常(特别是在做数学运算的时候)
public Series(String name, String type, List<T> data) {
super();
this.name = name;
this.type = type;
this.data = data;
}
}
图表绘制数据集,包含分组信息、图形类型以及纵坐标数据集信息。
EChartData.java
public class EChartData {
public List<String> legend = new ArrayList<String>();// 数据分组
public List<String> category = new ArrayList<String>();// 横坐标
public List<Series> series = new ArrayList<Series>();// 纵坐标
public List<String> color = new ArrayList<String>();// 颜色
public EChartData(List<String> legendList, List<String> categoryList,
List<Series> seriesList,List<String> colorList) {
super();
this.legend = legendList;
this.category = categoryList;
this.series = seriesList;
this.color = colorList;
}
}
页面绘制图形所需数据集,包含数据分组、横坐标、纵坐标、分组颜色等信息。 MultipleEChart.java
public class MultipleEChart implements Serializable {
/**
* 图表名称
*/
private String title;
/**
* 图表显示顺序
*/
private int order;
/**
* 图表数据集(横坐标、纵坐标、分组、颜色)
*/
private EChartData eChartData;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public int getOrder() {
return order;
}
public void setOrder(int order) {
this.order = order;
}
public EChartData geteChartData() {
return eChartData;
}
public void seteChartData(EChartData eChartData) {
this.eChartData = eChartData;
}
}
页面绘制多图表所需数据集,主要是在EChartData的外层增加了标题与顺序。
7、绘制工具类
import com.sophia.chart.annotation.ChineseName;
import com.sophia.chart.entity.EChartData;
import com.sophia.chart.entity.MultipleEChart;
import com.sophia.chart.entity.Series;
import org.springframework.core.annotation.Order;
import java.awt.*;
import java.lang.reflect.Field;
import java.util.List;
import java.util.*;
import java.util.stream.Collectors;
public class ChartUtil {
private static final Color[] COLORS = new Color[]{Color.RED, Color.BLUE, Color.CYAN, Color.GREEN, Color.MAGENTA,
Color.ORANGE, Color.PINK, Color.YELLOW, Color.BLACK, Color.GRAY, Color.LIGHT_GRAY};
public static String toHexFromColor(Color color) {
String r, g, b;
StringBuilder su = new StringBuilder();
r = Integer.toHexString(color.getRed());
g = Integer.toHexString(color.getGreen());
b = Integer.toHexString(color.getBlue());
r = r.length() == 1 ? "0" + r : r;
g = g.length() == 1 ? "0" + g : g;
b = b.length() == 1 ? "0" + b : b;
r = r.toUpperCase();
g = g.toUpperCase();
b = b.toUpperCase();
su.append("#");
su.append(r);
su.append(g);
su.append(b);
return su.toString();
}
/**
* 字符串转换成Color对象
*
* @param colorStr 16进制颜色字符串
* @return Color对象
*/
public static Color toColorFromString(String colorStr) {
colorStr = colorStr.substring(4);
Color color = new Color(Integer.parseInt(colorStr, 16));
//java.awt.Color[r=0,g=0,b=255]
return color;
}
/**
* 选择分组的颜色
*
* @param size
* @return
*/
public static String[] getColorList(int size) {
String[] colorArrays = new String[size];
for (int i = 0; i < size; i++) {
colorArrays[i] = toHexFromColor(COLORS[i]);
}
return colorArrays;
}
/**
* 获取需要绘制图表的所有属性
*
* @param clazz
* @param <T>
* @return
*/
public static <T> List<String> getLegendList(Class<T> clazz) {
List<String> legendList = new ArrayList<>();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(ChineseName.class)) {
field.setAccessible(true);
legendList.add(field.getName());
}
}
return legendList;
}
/**
* 初始化Series集合,key:图表名称,value:数据集
* @param clazz
* @return
*/
public static Map<String, List<Series>> initSeriesMap(Class clazz) {
Map<String, List<Series>> map = new HashMap<>();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(ChineseName.class)) {
map.put(field.getName(), new ArrayList<>());
}
}
return map;
}
/**
* 数据集新增数据
* @param map key: 图表名称 value:分组数据集
* @param i 横坐标对应索引位置
* @param t 实体对象
* @param <T>
*/
public static <T> void addDataToSeriesData(Map<String, Double[]> map, int i, T t) {
for (Map.Entry entry : map.entrySet()) {
String key = entry.getKey().toString();
Double[] value = (Double[]) entry.getValue();
value[i] = getSeriesData(t, key);
map.put(key, value);
}
}
/**
* 将每个分组的数据增加到结果数据集中
* @param map key:图表名称 value:key对应的chinese分组的数据集
* @param seriesData key:图表名称 value:图表数据集
* @param chinese 分组名称
*/
public static void addDataToSeries(Map<String, Double[]> map, Map<String, List<Series>> seriesData, String chinese) {
for (Map.Entry seriesEntry : seriesData.entrySet()) {
String key = seriesEntry.getKey().toString();
Double[] value = map.get(key);
List<Series> series = seriesData.get(key);
series.add(new Series(chinese,"line",Arrays.asList(value)));
seriesData.put(key,series);
}
}
/**
* 根据图表、分组等生成页面显示所需数据集
* @param clazz
* @param seriesData
* @param legendList
* @param category
* @return
*/
public static List<MultipleEChart> generateResultMap(Class clazz, Map<String, List<Series>> seriesData,
List<String> legendList, List<String> category) {
List<MultipleEChart> list = new ArrayList<>();
for (Map.Entry entry : seriesData.entrySet()) {
MultipleEChart multipleEChart = new MultipleEChart();
String key = entry.getKey().toString();
List<Series> value = (List<Series>) entry.getValue();
String title = convertLegendToChinese(clazz, key);
int order = getChartOrder(clazz,key);
multipleEChart.setTitle(title);
multipleEChart.setOrder(order);
multipleEChart.seteChartData(new EChartData(legendList, category, value, Arrays.asList(ChartUtil.getColorList(legendList.size()))));
list.add(multipleEChart);
}
return list.stream().sorted(Comparator.comparing(MultipleEChart::getOrder)).collect(Collectors.toList());
}
/**
* 获取图表顺序
* @param clazz
* @param key
* @return
*/
private static int getChartOrder(Class clazz,String key){
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Order.class)) {
field.setAccessible(true);
if (key.equals(field.getName())){
Order order = field.getAnnotation(Order.class);
return order.value();
}
}
}
return 0;
}
/**
* 初始化每个分组的数值,便于折线图显示的连续性
* @param clazz
* @param size
* @param <T>
* @return
*/
public static <T> Map<String, Double[]> getSeriesDataMap(Class<T> clazz, int size) {
Map<String, Double[]> map = new HashMap<>();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(ChineseName.class)) {
field.setAccessible(true);
Double[] data = new Double[size];
for (int i = 0; i < size; i++) {
data[i] = 0D;
}
map.put(field.getName(), data);
}
}
return map;
}
/**
* 通过属性获取属性值
*
* @param t
* @param key
* @param <T>
* @return
*/
public static <T> Double getSeriesData(T t, String key) {
Field[] fields = t.getClass().getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
if (key.equals(field.getName())) {
try {
return (double) field.get(t);
} catch (Exception e) {
return 0D;
}
}
}
return 0D;
}
/**
* 将所有分组通过注解转为中文
*
* @param clazz
* @param legendList
* @param <T>
* @return
*/
public static <T> List<String> convertAllLegendToChinese(Class clazz, List<String> legendList) {
List<String> chineseNameList = new ArrayList<>();
for (String legend : legendList) {
chineseNameList.add(convertLegendToChinese(clazz, legend));
}
return chineseNameList;
}
/**
* 将单一参数通过注解转为中文
*
* @param clazz
* @param legend
* @param <T>
* @return
*/
public static <T> String convertLegendToChinese(Class<T> clazz, String legend) {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(ChineseName.class)) {
field.setAccessible(true);
if (legend.equals(field.getName())) {
ChineseName chineseName = field.getAnnotation(ChineseName.class);
return chineseName.value();
}
}
}
return "";
}
}
该工具类主要是通过反射与注解机制,将绘制图形的赋值等方法进行抽离,为了便于不同对象绘制图形使用,避免同一场景使用重复代码,提高了代码的可读性与复用性。
8、逻辑代码
ProductProfitController.java
@Controller
@RequestMapping("/product-profit")
public class ProductProfitController {
@Autowired
private ProductProfitService service;
@RequestMapping("/view")
public ModelAndView viewProductProfit(ModelAndView mv){
mv.setViewName("MultipleEChart");
return mv;
}
@RequestMapping("/showEChartLine")
@ResponseBody
public List<MultipleEChart> getMultipleEChart(){
return service.getMultipleEChart();
}
}
该类主要是处理前端请求与接口响应。
ProductProfitService.java
@Service
public class ProductProfitService {
@Autowired
private ProductProfitMapper mapper;
public List<MultipleEChart> getMultipleEChart(){
List<ProductProfit> list = mapper.selectListGroupByDateAndSource();
List<String> category = list.stream().map(ProductProfit::getDate).distinct().collect(Collectors.toList());
List<String> legendList = list.stream().map(ProductProfit::getSource).distinct().collect(Collectors.toList());
// 初始化各图表,key:图表字段;value:纵坐标
Map<String,List<Series>> seriesMap = ChartUtil.initSeriesMap(ProductProfit.class);
List<String> chineseLegend = new ArrayList<String>();// 数据分组
for (String legend:legendList){
SourceEnum sourceEnum = SourceEnum.getSourceEnum(legend);
// 初始化每个分组,设置所有横坐标对应的纵坐标值都为0
Map<String,Double[]> initData = ChartUtil.getSeriesDataMap(ProductProfit.class,category.size());
for (int i=0;i<category.size();i++){
String loanDate=category.get(i);
// 对每个横坐标对应的纵坐标赋值
Optional<ProductProfit> infoOptional = list.stream().filter(item -> legend.equals(item.getSource()))
.filter(item -> loanDate.equals(item.getDate())).findFirst();
if (infoOptional.isPresent()){
ChartUtil.addDataToSeriesData(initData,i,infoOptional.get());
}
}
// 分组名称设置为中文
String chinese = sourceEnum == null ? legend : sourceEnum.getName();
chineseLegend.add(chinese);
// 各图表中添加各分组数据
ChartUtil.addDataToSeries(initData,seriesMap,chinese);
}
// 根据横坐标、分组等生成多图表数据集
return ChartUtil.generateResultMap(ProductProfit.class,seriesMap,chineseLegend,category);
}
}
该类主要是查询数据库数据,并通过工具类进行数据处理与数据集生成。
ProductProfitMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.sophia.chart.product.ProductProfitMapper">
<select id="selectListGroupByDateAndSource" resultType="ProductProfit">
select date,source,sum(total) total,sum(principal) principal,sum(profit) profit
from t_product_profit
group by date,source
order by date,source
</select>
</mapper>
查询数据库中的数据。
9、效果图
当横坐标数值较大时,不建议显示横坐标的值,否则美观较差。