Highcharts 学习笔记2-折线图(动态显示数据)

接上上次的学习,这次学习的内容是如何动态的显示数据。

一、学习动态显示数据前的准备工作

在正式说到如何显示前,我们需要知道,我们大概要如何来动态显示数据。

  1. 要动态的显示数据,那么我们后台一旦有数据变化,我们就需要响应给前端一个信号。
  2. 前端根据信号,更新数据,或者重新查询数据等。

涉及的知识点:WebSocket、SpringBoot、Mybatis

我使用的案例,是我自己课程设计作为一个例子,实时的显示一个光照数据。

项目中涉及到的环境:

编写代码平台:IntelliJ IDEA 2019.2.4
项目管理:Maven 3.6.0
使用框架:SpringBoot 2.4.0、Mybatis 2.1.4(整合SpringBoot)、MySQL 5.1.48、websocket 5.3.1(整合SpringBoot)、thymeleaf 3.0.11(整合SpringBoot)
辅助工具:gson 2.8.5(解析JSON数据)

<!-- 工程父项目 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.4.0</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<!-- 模板引擎 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<!-- MySQL 数据库连接 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.48</version>
    <scope>runtime</scope>
</dependency>

<!-- websocket 依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

<!-- 处理 JSON 数据 -->
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.5</version>
</dependency>

<!-- MyBatis 框架 -->
<dependency>
   <groupId>org.mybatis.spring.boot</groupId>
   <artifactId>mybatis-spring-boot-starter</artifactId>
   <version>2.1.4</version>
</dependency>

二、如何建立长连接,来实现前后端通信。

对于前面的一些创建数据库,将数据显示到一个折线图上,我在这里就不进行讲述了,不知道如何使用 Highcharts 来显示折线图的,可以去看看博主的第一篇文章。

Highcharts 学习笔记1-折线图(时间解析不对的坑点)

大致就是显示一张这样的光照折线图
在这里插入图片描述

我们需要解决的第一个问题,就是如何建立一种连接,使前后端能够通信。

解决办法就是:使用 WebSocket 建立长连接。

这里只是简单的讲述,详细的一些用法,请大家自行学习

WebSocket 使用的方法:

1. 编写一个 WebSocket 的配置

配置的一些具体的含义,这里不做解释。

下面的两个配置类,都需要写到项目中。

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

import javax.websocket.server.ServerEndpointConfig;

public class MyEndpointConfigure extends ServerEndpointConfig.Configurator implements ApplicationContextAware {

    private static volatile BeanFactory context;

    @Override
    public <T> T getEndpointInstance(Class<T> clazz) throws InstantiationException {
        return context.getBean(clazz);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        MyEndpointConfigure.context = applicationContext;
    }
}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

    @Bean
    public MyEndpointConfigure newConfigure() {
        return new MyEndpointConfigure();
    }

}

2. 编写一个 WebSocket 用来处理连接

通过访问 @ServerEndpoint 注解上的 value 值对应的 URL,建立一个长连接。

连接建立成功后,会调用 onOpen(Session session) 方法,参数是建立连接的会话。

建立成功后,会将成功建立的会话添加到 CopyOnWriteArraySet 集合中,方便我们进行群发消息。

我们需要给前端发送消息时,调用 sendMessage(String message) 方法,将我们需要发送的字符串发送给前端。

前端发送消息给后端,当前端发送消息后,后端会监听到前端发送的消息,会调用 onMessage(String message, Session session) 方法。

长连接关闭时,该监听对象,也会调用 onClose() 方法,同时将当前 session 从集合中移除

package com.smxy.sensor;

import com.google.gson.JsonObject;
import com.smxy.sensor.bean.LightData;
import com.smxy.sensor.config.MyEndpointConfigure;
import com.smxy.sensor.dao.LightDataDao;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.CopyOnWriteArraySet;

@Data
@Component
@Accessors(chain = true)
@ServerEndpoint(value = "/productWebSocket/{name}", configurator = MyEndpointConfigure.class)
public class ProductWebSocket {

    // concurrent包的线程安全Set,用来存放每个客户端对应的ProductWebSocket对象
    private static CopyOnWriteArraySet<ProductWebSocket> webSocketSet = new CopyOnWriteArraySet<ProductWebSocket>();

    private Session session;

    @Autowired
    private LightDataDao lightDataDao;

    /**
     * 添加测试线程
     */
    public void addTestThread() {
        new Thread(() -> {
            Random random = new Random();
            while (true) {
                try {
                    Thread.sleep(1000);
                    String lightData = String.format("%.2f", random.nextInt(300) + 100 + Math.random());
                    /* 光照数据 */
                    LightData lightDataClass = new LightData().setTime(new Date()).setData(lightData);
                    boolean res1 = lightDataDao.addData(lightDataClass);
                    System.out.println("lightDataDaoResult: " + lightData);
                    this.judgeData("Light", lightDataClass);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(@PathParam("name") String name, Session session) {
        System.out.println(name + "-建立连接");
        this.session = session;
        webSocketSet.add(this);
        /* 测试实时页面实时显示数据 */
        addTestThread();
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        System.out.println("连接关闭");
        webSocketSet.remove(this);
    }

    /**
     * 收到客户端消息后调用的方法
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.println("ID: " + session.getId() + "  message: " + message);
    }

    /**
     * 发生错误时调用
     */
    @OnError
    public void onError(Session session, Throwable error) {
        error.printStackTrace();
    }

    /**
     * 发送消息
     */
    public void sendMessage(String message) {
        try {
            this.session.getBasicRemote().sendText(message);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 群发消息
     */
    public void sendGroupMessage(String message) {
        for (ProductWebSocket webSocket : webSocketSet) {
            webSocket.sendMessage(message);
        }
    }

    /**
     * 将更新的数据发送给前端
     */
    public void judgeData(String type, LightData lightData) {
        JsonObject jsonObject = new JsonObject();
        jsonObject.addProperty("data", Double.parseDouble(lightData.getData()));
        jsonObject.addProperty("time", lightData.getTime().getTime());
        /* 发送更新消息 */
        this.sendGroupMessage("update" + type + "|" + jsonObject.toString());
        return;
    }
}

代码中,我在建立连接的一开始,就加入了一个线程,用来模拟实时更新数据库,更新数据库的同时,我们将插入到数据库的这条数据,也通过 gson 辅助工具,将这条数据转为 Json 数据发送给前端。

处理 JSON 数据,并发送消息,就是上述类中的 judgeData(String message) 这个方法中实现了。

这里为什么需要将更新的数据,发送给前端,等到我们解决第二个问题时候,再来解释。

3. 前端建立连接

刚刚,我们在编写 WebSocket 类的时候,也说到,需要发送一个请求,连建立长连接。

接下来我们就需要前端编写 JS 来建立长连接。

通过下述代码,我们就成功的建立的长连接。

var websocket = null;
// 判断当前浏览器是否支持 WebSocket
if ('WebSocket' in window) {
    // 连接 WebSocket 节点
    websocket = new WebSocket("ws://localhost:11111/productWebSocket/graphDataPage");
} else {
    alert("浏览器不支持 WebSocket,无法正常使用,请更换浏览器");
}

三、前端展示实时数据

当连接建立后,我们就可以和后端一样的,编写一个方法,来监听后端反馈给前端的更新信号

这里解释下,这里做了一个字符串的分割,使用的是 | 来进行分割,主要是因为用来表示一种格式:信号 | 数据。

通过前面的信号来判断当前是什么操作,随后使用前端的 JSON.parse() 来将 JSON 对象转换为对象。

/* 接收到消息后的回调函数 */
websocket.onmessage = function (event) {
    console.log(event.data);
    var str = event.data.split("|");
    if (str[0] == 'updateLight') {
    	var data = JSON.parse(str[1]);
        chartLight.series[0].addPoint([data.time, data.data], true);
    }
}

在官方中,给了一个实时显示的代码例子,通过这个例子
在这里插入图片描述
在这里,可以看到我使用了一个方法 addPoint ,该方法的详细介绍在:https://api.hcharts.cn/highcharts#Series.addPoint

这个方法的,主要的作用是添加一个数据点。改方法实际上有4个参数。

第一个参数:点的坐标
第二个参数:是否在添加点后,重绘图表(默认:true)
第三个参数:是否在添加点后,删除第一个点(默认:false)
第四个参数:新增点时包含默认动画效果(默认:true)

这里我只使用到了三个参数,点的坐标、重绘图表,不删除第一点。

看到这里就知道,更新后为什么需要将更新的数据发送给前端,原因就是前端展示,需要将新的数据添加到图表中。

如果你不这样做,而是重新发送最开始创建图表的请求,则效果就是图表会头到尾重新绘制一遍。这不是我们要的实时显示的效果。

来看看,我们实时更新的效果
在这里插入图片描述
完整的 JS 代码:

var chartLight = null;
/* 关闭世界标准时间 */
/* 不关闭会出现解析时间错误 */
Highcharts.setOptions({global: {useUTC: false}});
/* JSON 数据格式 */
/* 参数:[[1370131200000, 0.7695]] */
/* 时间(long)、数据 */
var httpUrl = "http://localhost:11111/getGraphData"

/* 初始化折线图数据 */
getLightGraphData();

/* 获取光照折线图数据 */
function getLightGraphData() {
    $.getJSON(httpUrl + "/Light", function (data) {
        chartLight = Highcharts.chart('containerLight', {
            /* 图标配置 */
            chart: {
                zoomType: 'x',
                type: 'spline'
            },
            /* 标题 */
            title: {
                text: '光照折线图'
            },
            /* 副标题 */
            subtitle: {
                text: document.ontouchstart === undefined ?
                    '鼠标拖动可以进行缩放' : '手势操作进行缩放'
            },
            /* x轴 */
            xAxis: {
                /* 坐标轴类型 */
                type: 'datetime',
                /* 时间轴标签格式化字符串 */
                dateTimeLabelFormats: {
                    millisecond: '%H:%M:%S.%L',
                    second: '%H:%M:%S',
                    minute: '%H:%M',
                    hour: '%H:%M',
                    day: '%Y-%m-%d',
                    week: '%m-%d',
                    month: '%Y-%m',
                    year: '%Y'
                }
            },
            /* 数据提示框 */
            tooltip: {
                /* 时间轴标签格式化字符串 */
                dateTimeLabelFormats: {
                    second: '%Y-%m-%d %H:%M:%S',
                    minute: '%Y-%m-%d %H:%M',
                    hour: '%Y-%m-%d %H',
                    day: '%Y-%m-%d',
                    week: '%Y-%m-%d',
                    month: '%Y-%m',
                    year: '%Y'
                }
            },
            /* y轴 */
            yAxis: {
                /* 标题 */
                title: {
                    text: '光照'
                },
                /* 轴标签 */
                labels: {
                    formatter: function () {
                        return this.value + " xl"
                    }
                }
            },
            /* 图例 */
            legend: {
                /* 图例开关 */
                enabled: false
            },
            /* 数据列 */
            series: [{
                /* 名字 */
                name: '光照',
                /* 显示数据 */
                data: data.extend.list,
                /* 设置折线颜色 */
                color: '#f7a35c',
            }]
        });
    });
}
var websocket = null;

// 判断当前浏览器是否支持 WebSocket
if ('WebSocket' in window) {
    // 连接 WebSocket 节点
    websocket = new WebSocket("ws://localhost:11111/productWebSocket/graphDataPage");
} else {
    alert("浏览器不支持 WebSocket,无法正常使用,请更换浏览器");
}

/* 接收到消息后的回调函数 */
websocket.onmessage = function (event) {
    console.log(event.data);
    var str = event.data.split("|");
    if (str[0] == 'updateLight') {
   		var data = JSON.parse(str[1]);
        chartLight.series[0].addPoint([data.time, data.data], true);
    }
}

获取图表数据的请求代码:

中间的 lightDataDao 是 Dao 对象,用来执行数据库操作的类,属于 Mybatis 的知识点。

/**
 * 获取折线图数据
 */
@GetMapping("/getGraphData/{type}")
@ResponseBody
public ResultData getGraphData(@PathVariable("type") String type) {
    Object dataObject[][] = null;
    List<LightData> lightData = lightDataDao.getAllData();
    dataObject = new Object[lightData.size()][2];
    for (int i = 0; i < lightData.size(); i++) {
        dataObject[i][0] = lightData.get(i).getTime().getTime();
        dataObject[i][1] = Double.parseDouble(lightData.get(i).getData());
    }
    return ResultData.success().add("list", dataObject);
}

结果封装类:

import lombok.Data;
import lombok.experimental.Accessors;

import java.util.HashMap;
import java.util.Map;

@Data
@Accessors(chain = true)
public class ResultData {

    /* 状态码:200正常;400错误 */
    private Integer code;

    /* 提示信息 */
    private String msg;

    /* 返回给前端的数据 */
    private Map<String, Object> extend = new HashMap<>();

    public static ResultData success() {
        return new ResultData().setCode(200).setMsg("成功");
    }

    public static ResultData failure() {
        return new ResultData().setCode(400).setMsg("失败");
    }

    public ResultData add(String key, Object value) {
        this.extend.put(key, value);
        return this;
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hiram Fan

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值