Spring MVC与EChart同时绘制多个图形

8 篇文章 0 订阅

根据实际业务需要,有时可能需要在同一个页面中显示多个图形,例如下图:

      这个时候怎么去处理,一种比较简单粗暴但却很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、效果图

当横坐标数值较大时,不建议显示横坐标的值,否则美观较差。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值