|
|
马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
引言
在当今数据驱动的时代,数据可视化已成为企业决策过程中不可或缺的一环。通过直观的图表展示,复杂的数据集变得易于理解,帮助决策者快速识别趋势、模式和异常。Highcharts作为一款功能强大的JavaScript图表库,与Spring Boot这一流行的Java框架相结合,可以创建出既美观又实用的数据可视化解决方案。本文将详细介绍如何从零开始,将Highcharts与Spring Boot完美融合,打造动态数据可视化系统。
1. Highcharts与Spring Boot概述
1.1 Highcharts简介
Highcharts是一个基于纯JavaScript的图表库,支持多种图表类型,包括线图、柱状图、饼图、散点图等。它具有以下特点:
• 兼容性好:支持所有现代浏览器,包括移动端浏览器
• 图表类型丰富:提供超过20种图表类型
• 高度可定制:支持自定义图表的各个方面
• 动态更新:支持实时数据更新
• 导出功能:支持将图表导出为图片、PDF等格式
1.2 Spring Boot简介
Spring Boot是Spring框架的一个子项目,旨在简化Spring应用的创建和开发过程。它具有以下特点:
• 自动配置:根据依赖自动配置Spring应用
• 内嵌服务器:如Tomcat、Jetty等,无需部署WAR文件
• 生产就绪:提供监控、健康检查等生产环境所需功能
• 无代码生成:无需生成代码,无需XML配置
1.3 为什么选择Highcharts与Spring Boot结合
将Highcharts与Spring Boot结合有以下优势:
• 前后端分离:Spring Boot提供RESTful API,Highcharts负责前端展示
• 高效开发:Spring Boot简化后端开发,Highcharts简化图表创建
• 实时数据:Spring Boot可以轻松实现数据推送,Highcharts支持实时更新
• 生态系统:两者都有庞大的社区和丰富的文档资源
2. 环境搭建与项目初始化
2.1 开发环境准备
在开始之前,确保以下开发环境已准备就绪:
• JDK 8或更高版本
• Maven 3.6或更高版本
• IDE(如IntelliJ IDEA或Eclipse)
• 现代浏览器(如Chrome、Firefox等)
2.2 创建Spring Boot项目
可以通过Spring Initializr(https://start.spring.io/)快速创建SpringBoot项目,或使用IDE的Spring项目创建功能。
以下是使用Maven创建Spring Boot项目的pom.xml文件基本配置:
- <?xml version="1.0" encoding="UTF-8"?>
- <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
- <modelVersion>4.0.0</modelVersion>
- <parent>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-parent</artifactId>
- <version>2.7.5</version>
- <relativePath/> <!-- lookup parent from repository -->
- </parent>
- <groupId>com.example</groupId>
- <artifactId>highcharts-spring-boot-demo</artifactId>
- <version>0.0.1-SNAPSHOT</version>
- <name>highcharts-spring-boot-demo</name>
- <description>Demo project for Spring Boot with Highcharts</description>
-
- <properties>
- <java.version>11</java.version>
- </properties>
-
- <dependencies>
- <!-- Spring Boot Web -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- </dependency>
-
- <!-- Spring Boot Data JPA -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-jpa</artifactId>
- </dependency>
-
- <!-- H2 Database -->
- <dependency>
- <groupId>com.h2database</groupId>
- <artifactId>h2</artifactId>
- <scope>runtime</scope>
- </dependency>
-
- <!-- Lombok -->
- <dependency>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- <optional>true</optional>
- </dependency>
-
- <!-- Spring Boot Test -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-test</artifactId>
- <scope>test</scope>
- </dependency>
- </dependencies>
-
- <build>
- <plugins>
- <plugin>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-maven-plugin</artifactId>
- <configuration>
- <excludes>
- <exclude>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- </exclude>
- </excludes>
- </configuration>
- </plugin>
- </plugins>
- </build>
- </project>
复制代码
2.3 项目结构
创建一个标准的Maven项目结构,如下所示:
- highcharts-spring-boot-demo/
- ├── src/
- │ ├── main/
- │ │ ├── java/
- │ │ │ └── com/
- │ │ │ └── example/
- │ │ │ └── highchartsspringbootdemo/
- │ │ │ ├── controller/
- │ │ │ ├── model/
- │ │ │ ├── repository/
- │ │ │ ├── service/
- │ │ │ └── HighchartsSpringBootDemoApplication.java
- │ │ └── resources/
- │ │ ├── static/
- │ │ │ ├── css/
- │ │ │ ├── js/
- │ │ │ └── index.html
- │ │ ├── templates/
- │ │ └── application.properties
- │ └── test/
- │ └── java/
- │ └── com/
- │ └── example/
- │ └── highchartsspringbootdemo/
- └── pom.xml
复制代码
2.4 配置application.properties
在src/main/resources/application.properties文件中添加基本配置:
- # Server Configuration
- server.port=8080
- # H2 Database Configuration
- spring.h2.console.enabled=true
- spring.h2.console.path=/h2-console
- spring.datasource.url=jdbc:h2:mem:testdb
- spring.datasource.driverClassName=org.h2.Driver
- spring.datasource.username=sa
- spring.datasource.password=password
- # JPA Configuration
- spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
- spring.jpa.hibernate.ddl-auto=update
- spring.jpa.show-sql=true
复制代码
3. 后端实现(Spring Boot部分)
3.1 创建数据模型
首先,我们需要创建一个数据模型来表示我们的业务数据。假设我们要展示销售数据,可以创建一个SalesData类:
- package com.example.highchartsspringbootdemo.model;
- import lombok.Data;
- import javax.persistence.*;
- import java.time.LocalDate;
- @Data
- @Entity
- @Table(name = "sales_data")
- public class SalesData {
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Long id;
-
- @Column(nullable = false)
- private LocalDate date;
-
- @Column(nullable = false)
- private Double amount;
-
- @Column(nullable = false)
- private String product;
-
- @Column(nullable = false)
- private String region;
-
- public SalesData() {
- }
-
- public SalesData(LocalDate date, Double amount, String product, String region) {
- this.date = date;
- this.amount = amount;
- this.product = product;
- this.region = region;
- }
- }
复制代码
3.2 创建数据仓库
接下来,创建一个Spring Data JPA仓库接口来处理数据访问:
- package com.example.highchartsspringbootdemo.repository;
- import com.example.highchartsspringbootdemo.model.SalesData;
- import org.springframework.data.jpa.repository.JpaRepository;
- import org.springframework.data.jpa.repository.Query;
- import org.springframework.stereotype.Repository;
- import java.time.LocalDate;
- import java.util.List;
- @Repository
- public interface SalesDataRepository extends JpaRepository<SalesData, Long> {
-
- // 查找指定日期范围内的销售数据
- List<SalesData> findByDateBetween(LocalDate startDate, LocalDate endDate);
-
- // 按产品分组查询销售总额
- @Query("SELECT new com.example.highchartsspringbootdemo.model.ProductSalesSum(s.product, SUM(s.amount)) " +
- "FROM SalesData s WHERE s.date BETWEEN :startDate AND :endDate GROUP BY s.product")
- List<ProductSalesSum> sumSalesByProductBetweenDates(LocalDate startDate, LocalDate endDate);
-
- // 按地区分组查询销售总额
- @Query("SELECT new com.example.highchartsspringbootdemo.model.RegionSalesSum(s.region, SUM(s.amount)) " +
- "FROM SalesData s WHERE s.date BETWEEN :startDate AND :endDate GROUP BY s.region")
- List<RegionSalesSum> sumSalesByRegionBetweenDates(LocalDate startDate, LocalDate endDate);
- }
复制代码
为了支持上述查询中的自定义返回类型,我们需要创建两个简单的类:
- package com.example.highchartsspringbootdemo.model;
- import lombok.AllArgsConstructor;
- import lombok.Data;
- import lombok.NoArgsConstructor;
- @Data
- @NoArgsConstructor
- @AllArgsConstructor
- public class ProductSalesSum {
- private String product;
- private Double totalAmount;
- }
- package com.example.highchartsspringbootdemo.model;
- import lombok.AllArgsConstructor;
- import lombok.Data;
- import lombok.NoArgsConstructor;
- @Data
- @NoArgsConstructor
- @AllArgsConstructor
- public class RegionSalesSum {
- private String region;
- private Double totalAmount;
- }
复制代码
3.3 创建服务层
创建一个服务类来处理业务逻辑:
- package com.example.highchartsspringbootdemo.service;
- import com.example.highchartsspringbootdemo.model.ProductSalesSum;
- import com.example.highchartsspringbootdemo.model.RegionSalesSum;
- import com.example.highchartsspringbootdemo.model.SalesData;
- import com.example.highchartsspringbootdemo.repository.SalesDataRepository;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Service;
- import java.time.LocalDate;
- import java.util.List;
- import java.util.Random;
- @Service
- public class SalesDataService {
-
- private final SalesDataRepository salesDataRepository;
-
- @Autowired
- public SalesDataService(SalesDataRepository salesDataRepository) {
- this.salesDataRepository = salesDataRepository;
- }
-
- // 获取所有销售数据
- public List<SalesData> getAllSalesData() {
- return salesDataRepository.findAll();
- }
-
- // 获取指定日期范围内的销售数据
- public List<SalesData> getSalesDataBetweenDates(LocalDate startDate, LocalDate endDate) {
- return salesDataRepository.findByDateBetween(startDate, endDate);
- }
-
- // 按产品获取销售总额
- public List<ProductSalesSum> getSalesSumByProduct(LocalDate startDate, LocalDate endDate) {
- return salesDataRepository.sumSalesByProductBetweenDates(startDate, endDate);
- }
-
- // 按地区获取销售总额
- public List<RegionSalesSum> getSalesSumByRegion(LocalDate startDate, LocalDate endDate) {
- return salesDataRepository.sumSalesByRegionBetweenDates(startDate, endDate);
- }
-
- // 生成模拟数据
- public void generateSampleData() {
- String[] products = {"Laptop", "Smartphone", "Tablet", "Monitor", "Keyboard"};
- String[] regions = {"North", "South", "East", "West", "Central"};
- Random random = new Random();
-
- // 生成过去30天的数据
- for (int i = 0; i < 30; i++) {
- LocalDate date = LocalDate.now().minusDays(i);
-
- // 每天生成5-10条销售记录
- int recordsPerDay = 5 + random.nextInt(6);
- for (int j = 0; j < recordsPerDay; j++) {
- String product = products[random.nextInt(products.length)];
- String region = regions[random.nextInt(regions.length)];
- double amount = 100 + random.nextDouble() * 1000; // 100-1100之间的金额
-
- SalesData salesData = new SalesData(date, amount, product, region);
- salesDataRepository.save(salesData);
- }
- }
- }
- }
复制代码
3.4 创建控制器
创建一个REST控制器来处理HTTP请求:
- package com.example.highchartsspringbootdemo.controller;
- import com.example.highchartsspringbootdemo.model.ProductSalesSum;
- import com.example.highchartsspringbootdemo.model.RegionSalesSum;
- import com.example.highchartsspringbootdemo.model.SalesData;
- import com.example.highchartsspringbootdemo.service.SalesDataService;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.format.annotation.DateTimeFormat;
- import org.springframework.http.ResponseEntity;
- import org.springframework.web.bind.annotation.*;
- import java.time.LocalDate;
- import java.util.List;
- @RestController
- @RequestMapping("/api/sales")
- @CrossOrigin(origins = "*") // 允许跨域请求
- public class SalesDataController {
-
- private final SalesDataService salesDataService;
-
- @Autowired
- public SalesDataController(SalesDataService salesDataService) {
- this.salesDataService = salesDataService;
- }
-
- // 获取所有销售数据
- @GetMapping("/all")
- public ResponseEntity<List<SalesData>> getAllSalesData() {
- List<SalesData> salesData = salesDataService.getAllSalesData();
- return ResponseEntity.ok(salesData);
- }
-
- // 获取指定日期范围内的销售数据
- @GetMapping("/range")
- public ResponseEntity<List<SalesData>> getSalesDataBetweenDates(
- @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
- @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
- List<SalesData> salesData = salesDataService.getSalesDataBetweenDates(startDate, endDate);
- return ResponseEntity.ok(salesData);
- }
-
- // 按产品获取销售总额
- @GetMapping("/by-product")
- public ResponseEntity<List<ProductSalesSum>> getSalesSumByProduct(
- @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
- @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
- List<ProductSalesSum> productSalesSums = salesDataService.getSalesSumByProduct(startDate, endDate);
- return ResponseEntity.ok(productSalesSums);
- }
-
- // 按地区获取销售总额
- @GetMapping("/by-region")
- public ResponseEntity<List<RegionSalesSum>> getSalesSumByRegion(
- @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
- @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
- List<RegionSalesSum> regionSalesSums = salesDataService.getSalesSumByRegion(startDate, endDate);
- return ResponseEntity.ok(regionSalesSums);
- }
-
- // 生成模拟数据
- @PostMapping("/generate-sample-data")
- public ResponseEntity<String> generateSampleData() {
- salesDataService.generateSampleData();
- return ResponseEntity.ok("Sample data generated successfully");
- }
- }
复制代码
3.5 初始化数据
创建一个数据初始化类,在应用启动时生成一些模拟数据:
- package com.example.highchartsspringbootdemo;
- import com.example.highchartsspringbootdemo.service.SalesDataService;
- import org.springframework.boot.CommandLineRunner;
- import org.springframework.stereotype.Component;
- @Component
- public class DataInitializer implements CommandLineRunner {
-
- private final SalesDataService salesDataService;
-
- public DataInitializer(SalesDataService salesDataService) {
- this.salesDataService = salesDataService;
- }
-
- @Override
- public void run(String... args) throws Exception {
- // 检查数据库是否为空,如果为空则生成模拟数据
- if (salesDataService.getAllSalesData().isEmpty()) {
- salesDataService.generateSampleData();
- System.out.println("Sample data has been generated.");
- }
- }
- }
复制代码
4. 前端实现(Highcharts集成)
4.1 创建HTML页面
在src/main/resources/static目录下创建index.html文件:
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Sales Data Visualization</title>
-
- <!-- Bootstrap CSS -->
- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
-
- <!-- Highcharts CSS -->
- <link rel="stylesheet" href="https://code.highcharts.com/css/highcharts.css">
-
- <!-- Custom CSS -->
- <link rel="stylesheet" href="/css/style.css">
- </head>
- <body>
- <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
- <div class="container">
- <a class="navbar-brand" href="#">Sales Dashboard</a>
- <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
- <span class="navbar-toggler-icon"></span>
- </button>
- <div class="collapse navbar-collapse" id="navbarNav">
- <ul class="navbar-nav">
- <li class="nav-item">
- <a class="nav-link active" href="#" id="daily-sales-tab">Daily Sales</a>
- </li>
- <li class="nav-item">
- <a class="nav-link" href="#" id="product-sales-tab">Sales by Product</a>
- </li>
- <li class="nav-item">
- <a class="nav-link" href="#" id="region-sales-tab">Sales by Region</a>
- </li>
- </ul>
- </div>
- </div>
- </nav>
- <div class="container mt-4">
- <div class="row mb-4">
- <div class="col-md-12">
- <div class="card">
- <div class="card-header">
- <h5 class="card-title">Date Range Selection</h5>
- </div>
- <div class="card-body">
- <div class="row">
- <div class="col-md-4">
- <label for="start-date" class="form-label">Start Date</label>
- <input type="date" class="form-control" id="start-date">
- </div>
- <div class="col-md-4">
- <label for="end-date" class="form-label">End Date</label>
- <input type="date" class="form-control" id="end-date">
- </div>
- <div class="col-md-4 d-flex align-items-end">
- <button id="update-charts" class="btn btn-primary w-100">Update Charts</button>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div class="row">
- <div class="col-md-12">
- <div class="card">
- <div class="card-header">
- <h5 class="card-title" id="chart-title">Daily Sales Trend</h5>
- </div>
- <div class="card-body">
- <div id="sales-chart" style="height: 400px;"></div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- Bootstrap JS -->
- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
-
- <!-- Highcharts JS -->
- <script src="https://code.highcharts.com/highcharts.js"></script>
- <script src="https://code.highcharts.com/modules/exporting.js"></script>
- <script src="https://code.highcharts.com/modules/export-data.js"></script>
-
- <!-- Custom JS -->
- <script src="/js/app.js"></script>
- </body>
- </html>
复制代码
4.2 创建CSS文件
在src/main/resources/static/css目录下创建style.css文件:
- body {
- background-color: #f8f9fa;
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
- }
- .navbar-brand {
- font-weight: bold;
- }
- .card {
- border: none;
- border-radius: 10px;
- box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
- margin-bottom: 1.5rem;
- }
- .card-header {
- background-color: #fff;
- border-bottom: 1px solid rgba(0, 0, 0, 0.125);
- border-radius: 10px 10px 0 0 !important;
- }
- .card-title {
- margin-bottom: 0;
- color: #495057;
- font-weight: 600;
- }
- .btn-primary {
- background-color: #0d6efd;
- border-color: #0d6efd;
- }
- .btn-primary:hover {
- background-color: #0b5ed7;
- border-color: #0a58ca;
- }
- .nav-link.active {
- color: #fff !important;
- background-color: #0d6efd;
- border-radius: 0.25rem;
- }
- .highcharts-container {
- width: 100%;
- height: 100%;
- }
- .highcharts-title {
- font-weight: 600 !important;
- }
- .highcharts-axis-title {
- font-weight: 500 !important;
- }
- .highcharts-legend-item {
- font-weight: 500 !important;
- }
复制代码
4.3 创建JavaScript文件
在src/main/resources/static/js目录下创建app.js文件:
- document.addEventListener('DOMContentLoaded', function() {
- // 设置默认日期范围(过去30天)
- const endDate = new Date();
- const startDate = new Date();
- startDate.setDate(endDate.getDate() - 30);
-
- // 格式化日期为YYYY-MM-DD
- function formatDate(date) {
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, '0');
- const day = String(date.getDate()).padStart(2, '0');
- return `${year}-${month}-${day}`;
- }
-
- // 设置日期输入框的默认值
- document.getElementById('start-date').value = formatDate(startDate);
- document.getElementById('end-date').value = formatDate(endDate);
-
- // 当前图表类型
- let currentChartType = 'daily-sales';
-
- // 初始化图表
- let salesChart = Highcharts.chart('sales-chart', {
- chart: {
- type: 'line',
- zoomType: 'x'
- },
- title: {
- text: 'Daily Sales Trend'
- },
- xAxis: {
- type: 'category',
- title: {
- text: 'Date'
- }
- },
- yAxis: {
- title: {
- text: 'Sales Amount ($)'
- }
- },
- legend: {
- enabled: true
- },
- plotOptions: {
- line: {
- dataLabels: {
- enabled: false
- },
- enableMouseTracking: true
- }
- },
- series: []
- });
-
- // 加载数据的函数
- async function loadData(chartType, startDate, endDate) {
- try {
- let url;
- let chartConfig;
-
- switch (chartType) {
- case 'daily-sales':
- url = `/api/sales/range?startDate=${startDate}&endDate=${endDate}`;
- chartConfig = configureDailySalesChart;
- break;
- case 'product-sales':
- url = `/api/sales/by-product?startDate=${startDate}&endDate=${endDate}`;
- chartConfig = configureProductSalesChart;
- break;
- case 'region-sales':
- url = `/api/sales/by-region?startDate=${startDate}&endDate=${endDate}`;
- chartConfig = configureRegionSalesChart;
- break;
- default:
- throw new Error('Unknown chart type');
- }
-
- const response = await fetch(url);
- if (!response.ok) {
- throw new Error('Network response was not ok');
- }
-
- const data = await response.json();
- chartConfig(data);
- } catch (error) {
- console.error('Error loading data:', error);
- showError('Failed to load data. Please try again later.');
- }
- }
-
- // 配置每日销售图表
- function configureDailySalesChart(data) {
- // 按日期分组数据
- const groupedData = {};
- data.forEach(item => {
- const date = item.date;
- if (!groupedData[date]) {
- groupedData[date] = 0;
- }
- groupedData[date] += item.amount;
- });
-
- // 转换为Highcharts格式
- const chartData = Object.keys(groupedData).map(date => {
- return [date, groupedData[date]];
- });
-
- // 更新图表
- salesChart.update({
- title: {
- text: 'Daily Sales Trend'
- },
- xAxis: {
- type: 'category',
- title: {
- text: 'Date'
- }
- },
- yAxis: {
- title: {
- text: 'Sales Amount ($)'
- }
- },
- series: [{
- name: 'Daily Sales',
- data: chartData,
- color: '#0d6efd'
- }]
- });
- }
-
- // 配置产品销售图表
- function configureProductSalesChart(data) {
- // 转换为Highcharts格式
- const chartData = data.map(item => {
- return [item.product, item.totalAmount];
- });
-
- // 更新图表
- salesChart.update({
- chart: {
- type: 'column'
- },
- title: {
- text: 'Sales by Product'
- },
- xAxis: {
- type: 'category',
- title: {
- text: 'Product'
- }
- },
- yAxis: {
- title: {
- text: 'Sales Amount ($)'
- }
- },
- series: [{
- name: 'Product Sales',
- data: chartData,
- color: '#198754'
- }]
- });
- }
-
- // 配置地区销售图表
- function configureRegionSalesChart(data) {
- // 转换为Highcharts格式
- const chartData = data.map(item => {
- return [item.region, item.totalAmount];
- });
-
- // 更新图表
- salesChart.update({
- chart: {
- type: 'pie'
- },
- title: {
- text: 'Sales by Region'
- },
- xAxis: {
- title: {
- text: ''
- }
- },
- yAxis: {
- title: {
- text: ''
- }
- },
- plotOptions: {
- pie: {
- allowPointSelect: true,
- cursor: 'pointer',
- dataLabels: {
- enabled: true,
- format: '<b>{point.name}</b>: ${point.y:.2f}'
- }
- }
- },
- series: [{
- name: 'Region Sales',
- data: chartData
- }]
- });
- }
-
- // 显示错误信息
- function showError(message) {
- const alertDiv = document.createElement('div');
- alertDiv.className = 'alert alert-danger alert-dismissible fade show';
- alertDiv.innerHTML = `
- ${message}
- <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
- `;
-
- const container = document.querySelector('.container');
- container.insertBefore(alertDiv, container.firstChild);
-
- // 3秒后自动关闭
- setTimeout(() => {
- alertDiv.classList.remove('show');
- setTimeout(() => {
- alertDiv.remove();
- }, 150);
- }, 3000);
- }
-
- // 初始加载数据
- loadData(currentChartType, formatDate(startDate), formatDate(endDate));
-
- // 更新图表按钮点击事件
- document.getElementById('update-charts').addEventListener('click', function() {
- const startDate = document.getElementById('start-date').value;
- const endDate = document.getElementById('end-date').value;
-
- if (!startDate || !endDate) {
- showError('Please select both start and end dates.');
- return;
- }
-
- if (new Date(startDate) > new Date(endDate)) {
- showError('Start date must be before end date.');
- return;
- }
-
- loadData(currentChartType, startDate, endDate);
- });
-
- // 标签页切换事件
- document.getElementById('daily-sales-tab').addEventListener('click', function(e) {
- e.preventDefault();
- currentChartType = 'daily-sales';
- document.querySelectorAll('.nav-link').forEach(link => {
- link.classList.remove('active');
- });
- this.classList.add('active');
-
- const startDate = document.getElementById('start-date').value;
- const endDate = document.getElementById('end-date').value;
- loadData(currentChartType, startDate, endDate);
- });
-
- document.getElementById('product-sales-tab').addEventListener('click', function(e) {
- e.preventDefault();
- currentChartType = 'product-sales';
- document.querySelectorAll('.nav-link').forEach(link => {
- link.classList.remove('active');
- });
- this.classList.add('active');
-
- const startDate = document.getElementById('start-date').value;
- const endDate = document.getElementById('end-date').value;
- loadData(currentChartType, startDate, endDate);
- });
-
- document.getElementById('region-sales-tab').addEventListener('click', function(e) {
- e.preventDefault();
- currentChartType = 'region-sales';
- document.querySelectorAll('.nav-link').forEach(link => {
- link.classList.remove('active');
- });
- this.classList.add('active');
-
- const startDate = document.getElementById('start-date').value;
- const endDate = document.getElementById('end-date').value;
- loadData(currentChartType, startDate, endDate);
- });
- });
复制代码
5. 数据交互和动态更新
5.1 实现实时数据更新
为了实现实时数据更新,我们可以使用WebSocket技术。首先,添加WebSocket依赖到pom.xml:
- <!-- WebSocket -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-websocket</artifactId>
- </dependency>
复制代码
5.2 配置WebSocket
创建一个WebSocket配置类:
- package com.example.highchartsspringbootdemo.config;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.messaging.simp.config.MessageBrokerRegistry;
- import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
- import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
- import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
- @Configuration
- @EnableWebSocketMessageBroker
- public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
-
- @Override
- public void configureMessageBroker(MessageBrokerRegistry config) {
- config.enableSimpleBroker("/topic");
- config.setApplicationDestinationPrefixes("/app");
- }
-
- @Override
- public void registerStompEndpoints(StompEndpointRegistry registry) {
- registry.addEndpoint("/ws").withSockJS();
- }
- }
复制代码
5.3 创建实时数据控制器
创建一个控制器来处理实时数据推送:
- package com.example.highchartsspringbootdemo.controller;
- import com.example.highchartsspringbootdemo.model.SalesData;
- import com.example.highchartsspringbootdemo.service.SalesDataService;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.messaging.handler.annotation.MessageMapping;
- import org.springframework.messaging.handler.annotation.SendTo;
- import org.springframework.stereotype.Controller;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.ResponseBody;
- import java.time.LocalDate;
- import java.util.List;
- import java.util.Random;
- import java.util.concurrent.Executors;
- import java.util.concurrent.ScheduledExecutorService;
- import java.util.concurrent.TimeUnit;
- @Controller
- public class RealTimeDataController {
-
- private final SalesDataService salesDataService;
- private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
-
- @Autowired
- public RealTimeDataController(SalesDataService salesDataService) {
- this.salesDataService = salesDataService;
- startRealTimeDataGeneration();
- }
-
- // 开始实时生成数据
- private void startRealTimeDataGeneration() {
- scheduler.scheduleAtFixedRate(() -> {
- // 生成新的销售数据
- String[] products = {"Laptop", "Smartphone", "Tablet", "Monitor", "Keyboard"};
- String[] regions = {"North", "South", "East", "West", "Central"};
- Random random = new Random();
-
- String product = products[random.nextInt(products.length)];
- String region = regions[random.nextInt(regions.length)];
- double amount = 100 + random.nextDouble() * 1000;
-
- SalesData newSalesData = new SalesData(LocalDate.now(), amount, product, region);
- salesDataService.save(newSalesData);
-
- // 推送到前端
- broadcastNewSalesData(newSalesData);
- }, 10, 10, TimeUnit.SECONDS); // 每10秒生成一次新数据
- }
-
- // 广播新销售数据
- private void broadcastNewSalesData(SalesData salesData) {
- // 这里可以通过WebSocket将新数据推送到前端
- // 实际实现取决于你的WebSocket配置
- }
-
- // 获取今天的销售数据
- @GetMapping("/api/sales/today")
- @ResponseBody
- public List<SalesData> getTodaySalesData() {
- LocalDate today = LocalDate.now();
- return salesDataService.getSalesDataBetweenDates(today, today);
- }
-
- // 处理WebSocket消息
- @MessageMapping("/hello")
- @SendTo("/topic/greetings")
- public String greeting(String message) throws Exception {
- return "Hello, " + message + "!";
- }
- }
复制代码
5.4 更新前端以支持实时数据
在app.js中添加WebSocket支持:
- // 在文件顶部添加WebSocket连接
- let stompClient = null;
- function connect() {
- const socket = new SockJS('/ws');
- stompClient = Stomp.over(socket);
- stompClient.connect({}, function(frame) {
- console.log('Connected: ' + frame);
- stompClient.subscribe('/topic/sales', function(message) {
- const salesData = JSON.parse(message.body);
- updateChartWithNewData(salesData);
- });
- }, function(error) {
- console.log('Error: ' + error);
- // 5秒后尝试重新连接
- setTimeout(connect, 5000);
- });
- }
- // 断开连接
- function disconnect() {
- if (stompClient !== null) {
- stompClient.disconnect();
- }
- console.log("Disconnected");
- }
- // 更新图表数据
- function updateChartWithNewData(newData) {
- // 根据当前图表类型更新数据
- if (currentChartType === 'daily-sales') {
- // 检查是否已有该日期的数据点
- const date = newData.date;
- let found = false;
-
- salesChart.series[0].points.forEach(point => {
- if (point.category === date) {
- point.update(point.y + newData.amount);
- found = true;
- }
- });
-
- // 如果没有找到该日期的数据点,添加新的
- if (!found) {
- salesChart.series[0].addPoint([date, newData.amount]);
- }
- } else if (currentChartType === 'product-sales') {
- // 查找并更新产品数据
- const product = newData.product;
- let found = false;
-
- salesChart.series[0].points.forEach(point => {
- if (point.category === product) {
- point.update(point.y + newData.amount);
- found = true;
- }
- });
-
- // 如果没有找到该产品的数据点,添加新的
- if (!found) {
- // 重新加载整个图表数据
- const startDate = document.getElementById('start-date').value;
- const endDate = document.getElementById('end-date').value;
- loadData(currentChartType, startDate, endDate);
- }
- } else if (currentChartType === 'region-sales') {
- // 重新加载整个图表数据
- const startDate = document.getElementById('start-date').value;
- const endDate = document.getElementById('end-date').value;
- loadData(currentChartType, startDate, endDate);
- }
- }
- // 在页面加载时连接WebSocket
- window.addEventListener('load', function() {
- connect();
- });
- // 在页面关闭时断开连接
- window.addEventListener('beforeunload', function() {
- disconnect();
- });
复制代码
6. 实际案例和最佳实践
6.1 案例一:销售仪表板
我们已经实现了一个销售仪表板,它可以展示每日销售趋势、按产品分类的销售数据和按地区分类的销售数据。这个仪表板具有以下特点:
• 交互式图表:用户可以与图表交互,如缩放、悬停查看详细数据等
• 日期范围选择:用户可以选择特定日期范围查看数据
• 实时更新:图表可以实时更新以反映新数据
• 响应式设计:仪表板适应不同屏幕尺寸
6.2 案例二:系统性能监控
让我们创建一个系统性能监控的例子,展示如何使用Highcharts和Spring Boot来监控系统性能。
首先,创建一个系统性能数据模型:
- package com.example.highchartsspringbootdemo.model;
- import lombok.Data;
- import javax.persistence.*;
- import java.time.LocalDateTime;
- @Data
- @Entity
- @Table(name = "system_performance")
- public class SystemPerformance {
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Long id;
-
- @Column(nullable = false)
- private LocalDateTime timestamp;
-
- @Column(nullable = false)
- private Double cpuUsage;
-
- @Column(nullable = false)
- private Double memoryUsage;
-
- @Column(nullable = false)
- private Double diskUsage;
-
- @Column(nullable = false)
- private Double networkIn;
-
- @Column(nullable = false)
- private Double networkOut;
-
- public SystemPerformance() {
- }
-
- public SystemPerformance(LocalDateTime timestamp, Double cpuUsage, Double memoryUsage,
- Double diskUsage, Double networkIn, Double networkOut) {
- this.timestamp = timestamp;
- this.cpuUsage = cpuUsage;
- this.memoryUsage = memoryUsage;
- this.diskUsage = diskUsage;
- this.networkIn = networkIn;
- this.networkOut = networkOut;
- }
- }
复制代码
然后,创建一个仓库接口:
- package com.example.highchartsspringbootdemo.repository;
- import com.example.highchartsspringbootdemo.model.SystemPerformance;
- import org.springframework.data.jpa.repository.JpaRepository;
- import org.springframework.stereotype.Repository;
- import java.time.LocalDateTime;
- import java.util.List;
- @Repository
- public interface SystemPerformanceRepository extends JpaRepository<SystemPerformance, Long> {
-
- // 查找指定时间范围内的性能数据
- List<SystemPerformance> findByTimestampBetweenOrderByTimestamp(LocalDateTime startTime, LocalDateTime endTime);
- }
复制代码
创建一个服务类:
- package com.example.highchartsspringbootdemo.service;
- import com.example.highchartsspringbootdemo.model.SystemPerformance;
- import com.example.highchartsspringbootdemo.repository.SystemPerformanceRepository;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Service;
- import java.lang.management.ManagementFactory;
- import java.lang.management.OperatingSystemMXBean;
- import java.time.LocalDateTime;
- import java.util.List;
- import java.util.Random;
- @Service
- public class SystemPerformanceService {
-
- private final SystemPerformanceRepository systemPerformanceRepository;
-
- @Autowired
- public SystemPerformanceService(SystemPerformanceRepository systemPerformanceRepository) {
- this.systemPerformanceRepository = systemPerformanceRepository;
- }
-
- // 获取指定时间范围内的性能数据
- public List<SystemPerformance> getPerformanceDataBetweenTimes(LocalDateTime startTime, LocalDateTime endTime) {
- return systemPerformanceRepository.findByTimestampBetweenOrderByTimestamp(startTime, endTime);
- }
-
- // 保存性能数据
- public SystemPerformance save(SystemPerformance systemPerformance) {
- return systemPerformanceRepository.save(systemPerformance);
- }
-
- // 获取当前系统性能数据
- public SystemPerformance getCurrentSystemPerformance() {
- OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();
- com.sun.management.OperatingSystemMXBean sunOsBean =
- (com.sun.management.OperatingSystemMXBean) osBean;
-
- // 获取系统负载
- double cpuUsage = sunOsBean.getSystemCpuLoad() * 100;
- double memoryUsage = (1 - (double) sunOsBean.getFreePhysicalMemorySize() /
- (double) sunOsBean.getTotalPhysicalMemorySize()) * 100;
-
- // 模拟磁盘和网络使用率
- Random random = new Random();
- double diskUsage = 30 + random.nextDouble() * 50; // 30-80%
- double networkIn = random.nextDouble() * 100; // 0-100 MB/s
- double networkOut = random.nextDouble() * 100; // 0-100 MB/s
-
- return new SystemPerformance(
- LocalDateTime.now(),
- cpuUsage,
- memoryUsage,
- diskUsage,
- networkIn,
- networkOut
- );
- }
-
- // 生成模拟性能数据
- public void generateSamplePerformanceData() {
- Random random = new Random();
- LocalDateTime now = LocalDateTime.now();
-
- // 生成过去24小时的数据,每5分钟一条
- for (int i = 288; i >= 0; i--) { // 24小时 * 12次/小时 = 288次
- LocalDateTime timestamp = now.minusMinutes(i * 5);
-
- double cpuUsage = Math.max(0, Math.min(100, 20 + random.nextDouble() * 60));
- double memoryUsage = Math.max(0, Math.min(100, 30 + random.nextDouble() * 50));
- double diskUsage = Math.max(0, Math.min(100, 20 + random.nextDouble() * 60));
- double networkIn = random.nextDouble() * 100;
- double networkOut = random.nextDouble() * 100;
-
- SystemPerformance performance = new SystemPerformance(
- timestamp,
- cpuUsage,
- memoryUsage,
- diskUsage,
- networkIn,
- networkOut
- );
-
- systemPerformanceRepository.save(performance);
- }
- }
- }
复制代码
创建一个控制器:
- package com.example.highchartsspringbootdemo.controller;
- import com.example.highchartsspringbootdemo.model.SystemPerformance;
- import com.example.highchartsspringbootdemo.service.SystemPerformanceService;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.format.annotation.DateTimeFormat;
- import org.springframework.http.ResponseEntity;
- import org.springframework.web.bind.annotation.*;
- import java.time.LocalDateTime;
- import java.util.List;
- @RestController
- @RequestMapping("/api/performance")
- @CrossOrigin(origins = "*")
- public class SystemPerformanceController {
-
- private final SystemPerformanceService systemPerformanceService;
-
- @Autowired
- public SystemPerformanceController(SystemPerformanceService systemPerformanceService) {
- this.systemPerformanceService = systemPerformanceService;
- }
-
- // 获取指定时间范围内的性能数据
- @GetMapping("/range")
- public ResponseEntity<List<SystemPerformance>> getPerformanceDataBetweenTimes(
- @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime,
- @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime) {
- List<SystemPerformance> performanceData =
- systemPerformanceService.getPerformanceDataBetweenTimes(startTime, endTime);
- return ResponseEntity.ok(performanceData);
- }
-
- // 获取当前系统性能
- @GetMapping("/current")
- public ResponseEntity<SystemPerformance> getCurrentSystemPerformance() {
- SystemPerformance currentPerformance = systemPerformanceService.getCurrentSystemPerformance();
- return ResponseEntity.ok(currentPerformance);
- }
-
- // 生成模拟性能数据
- @PostMapping("/generate-sample-data")
- public ResponseEntity<String> generateSamplePerformanceData() {
- systemPerformanceService.generateSamplePerformanceData();
- return ResponseEntity.ok("Sample performance data generated successfully");
- }
- }
复制代码
创建一个性能监控的HTML页面performance.html:
创建对应的JavaScript文件performance.js:
- document.addEventListener('DOMContentLoaded', function() {
- // 设置默认时间范围(过去24小时)
- const endTime = new Date();
- const startTime = new Date();
- startTime.setHours(endTime.getHours() - 24);
-
- // 格式化日期时间为YYYY-MM-DDTHH:MM格式
- function formatDateTime(date) {
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, '0');
- const day = String(date.getDate()).padStart(2, '0');
- const hours = String(date.getHours()).padStart(2, '0');
- const minutes = String(date.getMinutes()).padStart(2, '0');
- return `${year}-${month}-${day}T${hours}:${minutes}`;
- }
-
- // 设置时间输入框的默认值
- document.getElementById('start-time').value = formatDateTime(startTime);
- document.getElementById('end-time').value = formatDateTime(endTime);
-
- // 当前图表类型
- let currentChartType = 'overview';
-
- // 初始化图表
- let performanceChart = Highcharts.chart('performance-chart', {
- chart: {
- zoomType: 'x'
- },
- title: {
- text: 'System Performance Overview'
- },
- xAxis: {
- type: 'datetime',
- title: {
- text: 'Time'
- }
- },
- yAxis: {
- title: {
- text: 'Usage (%)'
- },
- min: 0,
- max: 100
- },
- legend: {
- enabled: true
- },
- plotOptions: {
- line: {
- dataLabels: {
- enabled: false
- },
- enableMouseTracking: true
- }
- },
- series: []
- });
-
- // WebSocket连接
- let stompClient = null;
-
- function connect() {
- const socket = new SockJS('/ws');
- stompClient = Stomp.over(socket);
- stompClient.connect({}, function(frame) {
- console.log('Connected: ' + frame);
- stompClient.subscribe('/topic/performance', function(message) {
- const performanceData = JSON.parse(message.body);
- updateCurrentMetrics(performanceData);
- updateChartWithNewData(performanceData);
- });
- }, function(error) {
- console.log('Error: ' + error);
- // 5秒后尝试重新连接
- setTimeout(connect, 5000);
- });
- }
-
- // 断开连接
- function disconnect() {
- if (stompClient !== null) {
- stompClient.disconnect();
- }
- console.log("Disconnected");
- }
-
- // 更新当前指标
- function updateCurrentMetrics(data) {
- document.getElementById('cpu-usage').textContent = data.cpuUsage.toFixed(1) + '%';
- document.getElementById('memory-usage').textContent = data.memoryUsage.toFixed(1) + '%';
- document.getElementById('disk-usage').textContent = data.diskUsage.toFixed(1) + '%';
- document.getElementById('network-usage').textContent =
- (data.networkIn + data.networkOut).toFixed(1) + ' MB/s';
- }
-
- // 加载数据的函数
- async function loadPerformanceData(chartType, startTime, endTime) {
- try {
- const startTimeISO = new Date(startTime).toISOString();
- const endTimeISO = new Date(endTime).toISOString();
-
- const response = await fetch(`/api/performance/range?startTime=${startTimeISO}&endTime=${endTimeISO}`);
- if (!response.ok) {
- throw new Error('Network response was not ok');
- }
-
- const data = await response.json();
- configureChart(chartType, data);
- } catch (error) {
- console.error('Error loading performance data:', error);
- showError('Failed to load performance data. Please try again later.');
- }
- }
-
- // 配置图表
- function configureChart(chartType, data) {
- // 转换时间戳为Highcharts格式
- const chartData = data.map(item => {
- return [new Date(item.timestamp).getTime(), item];
- });
-
- switch (chartType) {
- case 'overview':
- performanceChart.update({
- title: {
- text: 'System Performance Overview'
- },
- yAxis: {
- title: {
- text: 'Usage (%)'
- },
- min: 0,
- max: 100
- },
- series: [
- {
- name: 'CPU Usage',
- data: chartData.map(item => [item[0], item[1].cpuUsage]),
- color: '#0d6efd'
- },
- {
- name: 'Memory Usage',
- data: chartData.map(item => [item[0], item[1].memoryUsage]),
- color: '#198754'
- },
- {
- name: 'Disk Usage',
- data: chartData.map(item => [item[0], item[1].diskUsage]),
- color: '#ffc107'
- }
- ]
- });
- break;
-
- case 'cpu':
- performanceChart.update({
- title: {
- text: 'CPU Usage'
- },
- yAxis: {
- title: {
- text: 'Usage (%)'
- },
- min: 0,
- max: 100
- },
- series: [
- {
- name: 'CPU Usage',
- data: chartData.map(item => [item[0], item[1].cpuUsage]),
- color: '#0d6efd'
- }
- ]
- });
- break;
-
- case 'memory':
- performanceChart.update({
- title: {
- text: 'Memory Usage'
- },
- yAxis: {
- title: {
- text: 'Usage (%)'
- },
- min: 0,
- max: 100
- },
- series: [
- {
- name: 'Memory Usage',
- data: chartData.map(item => [item[0], item[1].memoryUsage]),
- color: '#198754'
- }
- ]
- });
- break;
-
- case 'disk':
- performanceChart.update({
- title: {
- text: 'Disk Usage'
- },
- yAxis: {
- title: {
- text: 'Usage (%)'
- },
- min: 0,
- max: 100
- },
- series: [
- {
- name: 'Disk Usage',
- data: chartData.map(item => [item[0], item[1].diskUsage]),
- color: '#ffc107'
- }
- ]
- });
- break;
-
- case 'network':
- performanceChart.update({
- title: {
- text: 'Network I/O'
- },
- yAxis: {
- title: {
- text: 'MB/s'
- },
- min: 0
- },
- series: [
- {
- name: 'Network In',
- data: chartData.map(item => [item[0], item[1].networkIn]),
- color: '#17a2b8'
- },
- {
- name: 'Network Out',
- data: chartData.map(item => [item[0], item[1].networkOut]),
- color: '#6f42c1'
- }
- ]
- });
- break;
- }
- }
-
- // 更新图表数据
- function updateChartWithNewData(newData) {
- const timestamp = new Date(newData.timestamp).getTime();
-
- switch (currentChartType) {
- case 'overview':
- // 更新CPU使用率
- updateSeriesData(0, timestamp, newData.cpuUsage);
- // 更新内存使用率
- updateSeriesData(1, timestamp, newData.memoryUsage);
- // 更新磁盘使用率
- updateSeriesData(2, timestamp, newData.diskUsage);
- break;
-
- case 'cpu':
- // 更新CPU使用率
- updateSeriesData(0, timestamp, newData.cpuUsage);
- break;
-
- case 'memory':
- // 更新内存使用率
- updateSeriesData(0, timestamp, newData.memoryUsage);
- break;
-
- case 'disk':
- // 更新磁盘使用率
- updateSeriesData(0, timestamp, newData.diskUsage);
- break;
-
- case 'network':
- // 更新网络输入
- updateSeriesData(0, timestamp, newData.networkIn);
- // 更新网络输出
- updateSeriesData(1, timestamp, newData.networkOut);
- break;
- }
- }
-
- // 更新系列数据
- function updateSeriesData(seriesIndex, x, y) {
- if (performanceChart.series[seriesIndex]) {
- // 添加新数据点
- performanceChart.series[seriesIndex].addPoint([x, y], true, false);
-
- // 限制显示的数据点数量,保持图表清晰
- const maxPoints = 100;
- if (performanceChart.series[seriesIndex].data.length > maxPoints) {
- performanceChart.series[seriesIndex].data[0].remove(false);
- }
- }
- }
-
- // 显示错误信息
- function showError(message) {
- const alertDiv = document.createElement('div');
- alertDiv.className = 'alert alert-danger alert-dismissible fade show';
- alertDiv.innerHTML = `
- ${message}
- <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
- `;
-
- const container = document.querySelector('.container');
- container.insertBefore(alertDiv, container.firstChild);
-
- // 3秒后自动关闭
- setTimeout(() => {
- alertDiv.classList.remove('show');
- setTimeout(() => {
- alertDiv.remove();
- }, 150);
- }, 3000);
- }
-
- // 初始加载数据
- loadPerformanceData(currentChartType, formatDateTime(startTime), formatDateTime(endTime));
-
- // 更新图表按钮点击事件
- document.getElementById('update-charts').addEventListener('click', function() {
- const startTime = document.getElementById('start-time').value;
- const endTime = document.getElementById('end-time').value;
-
- if (!startTime || !endTime) {
- showError('Please select both start and end times.');
- return;
- }
-
- if (new Date(startTime) > new Date(endTime)) {
- showError('Start time must be before end time.');
- return;
- }
-
- loadPerformanceData(currentChartType, startTime, endTime);
- });
-
- // 标签页切换事件
- document.getElementById('overview-tab').addEventListener('click', function(e) {
- e.preventDefault();
- currentChartType = 'overview';
- document.querySelectorAll('.nav-link').forEach(link => {
- link.classList.remove('active');
- });
- this.classList.add('active');
-
- const startTime = document.getElementById('start-time').value;
- const endTime = document.getElementById('end-time').value;
- loadPerformanceData(currentChartType, startTime, endTime);
- });
-
- document.getElementById('cpu-tab').addEventListener('click', function(e) {
- e.preventDefault();
- currentChartType = 'cpu';
- document.querySelectorAll('.nav-link').forEach(link => {
- link.classList.remove('active');
- });
- this.classList.add('active');
-
- const startTime = document.getElementById('start-time').value;
- const endTime = document.getElementById('end-time').value;
- loadPerformanceData(currentChartType, startTime, endTime);
- });
-
- document.getElementById('memory-tab').addEventListener('click', function(e) {
- e.preventDefault();
- currentChartType = 'memory';
- document.querySelectorAll('.nav-link').forEach(link => {
- link.classList.remove('active');
- });
- this.classList.add('active');
-
- const startTime = document.getElementById('start-time').value;
- const endTime = document.getElementById('end-time').value;
- loadPerformanceData(currentChartType, startTime, endTime);
- });
-
- document.getElementById('disk-tab').addEventListener('click', function(e) {
- e.preventDefault();
- currentChartType = 'disk';
- document.querySelectorAll('.nav-link').forEach(link => {
- link.classList.remove('active');
- });
- this.classList.add('active');
-
- const startTime = document.getElementById('start-time').value;
- const endTime = document.getElementById('end-time').value;
- loadPerformanceData(currentChartType, startTime, endTime);
- });
-
- document.getElementById('network-tab').addEventListener('click', function(e) {
- e.preventDefault();
- currentChartType = 'network';
- document.querySelectorAll('.nav-link').forEach(link => {
- link.classList.remove('active');
- });
- this.classList.add('active');
-
- const startTime = document.getElementById('start-time').value;
- const endTime = document.getElementById('end-time').value;
- loadPerformanceData(currentChartType, startTime, endTime);
- });
-
- // 连接WebSocket
- connect();
-
- // 在页面关闭时断开连接
- window.addEventListener('beforeunload', function() {
- disconnect();
- });
-
- // 定期获取当前系统性能数据
- setInterval(async function() {
- try {
- const response = await fetch('/api/performance/current');
- if (!response.ok) {
- throw new Error('Network response was not ok');
- }
-
- const data = await response.json();
- updateCurrentMetrics(data);
- } catch (error) {
- console.error('Error fetching current performance data:', error);
- }
- }, 5000); // 每5秒更新一次
- });
复制代码
6.3 最佳实践
在将Highcharts与Spring Boot集成时,以下是一些最佳实践:
1. 前后端分离:保持前端和后端的清晰分离,使用RESTful API进行通信。
2. 数据格式化:在后端提供格式化良好的数据,减少前端处理负担。
3. 错误处理:实现全面的错误处理,包括网络错误、数据格式错误等。
4. 性能优化:使用分页和延迟加载处理大量数据实现数据缓存减少数据库查询使用WebSocket实现实时更新,避免轮询
5. 使用分页和延迟加载处理大量数据
6. 实现数据缓存减少数据库查询
7. 使用WebSocket实现实时更新,避免轮询
8. 安全性:实现适当的认证和授权防止SQL注入和XSS攻击使用HTTPS保护数据传输
9. 实现适当的认证和授权
10. 防止SQL注入和XSS攻击
11. 使用HTTPS保护数据传输
12. 可扩展性:设计可扩展的架构,便于添加新的图表类型使用配置而非硬编码值实现模块化设计
13. 设计可扩展的架构,便于添加新的图表类型
14. 使用配置而非硬编码值
15. 实现模块化设计
16. 用户体验:提供加载指示器实现响应式设计适应不同设备添加交互功能如缩放、悬停提示等
17. 提供加载指示器
18. 实现响应式设计适应不同设备
19. 添加交互功能如缩放、悬停提示等
前后端分离:保持前端和后端的清晰分离,使用RESTful API进行通信。
数据格式化:在后端提供格式化良好的数据,减少前端处理负担。
错误处理:实现全面的错误处理,包括网络错误、数据格式错误等。
性能优化:
• 使用分页和延迟加载处理大量数据
• 实现数据缓存减少数据库查询
• 使用WebSocket实现实时更新,避免轮询
安全性:
• 实现适当的认证和授权
• 防止SQL注入和XSS攻击
• 使用HTTPS保护数据传输
可扩展性:
• 设计可扩展的架构,便于添加新的图表类型
• 使用配置而非硬编码值
• 实现模块化设计
用户体验:
• 提供加载指示器
• 实现响应式设计适应不同设备
• 添加交互功能如缩放、悬停提示等
7. 总结与展望
7.1 总结
本文详细介绍了如何将Highcharts与Spring Boot完美融合,打造动态数据可视化解决方案。我们从环境搭建开始,逐步构建了一个完整的销售数据可视化系统,并扩展到了系统性能监控场景。通过这个解决方案,我们可以:
• 使用Spring Boot构建强大的后端服务
• 利用Highcharts创建美观、交互式的图表
• 通过WebSocket实现实时数据更新
• 设计响应式用户界面适应不同设备
7.2 展望
未来,我们可以进一步扩展这个解决方案:
1. 增强数据分析能力:集成机器学习算法进行预测分析添加更多数据聚合和统计功能
2. 集成机器学习算法进行预测分析
3. 添加更多数据聚合和统计功能
4. 提升用户体验:实现更丰富的交互功能添加自定义仪表板功能支持图表导出和报告生成
5. 实现更丰富的交互功能
6. 添加自定义仪表板功能
7. 支持图表导出和报告生成
8. 扩展应用场景:适用于更多业务领域如金融、物流、医疗等支持更多数据源如IoT设备、社交媒体等
9. 适用于更多业务领域如金融、物流、医疗等
10. 支持更多数据源如IoT设备、社交媒体等
11. 技术升级:采用微服务架构提高可扩展性使用容器化部署简化运维集成云服务提升可靠性
12. 采用微服务架构提高可扩展性
13. 使用容器化部署简化运维
14. 集成云服务提升可靠性
增强数据分析能力:
• 集成机器学习算法进行预测分析
• 添加更多数据聚合和统计功能
提升用户体验:
• 实现更丰富的交互功能
• 添加自定义仪表板功能
• 支持图表导出和报告生成
扩展应用场景:
• 适用于更多业务领域如金融、物流、医疗等
• 支持更多数据源如IoT设备、社交媒体等
技术升级:
• 采用微服务架构提高可扩展性
• 使用容器化部署简化运维
• 集成云服务提升可靠性
Highcharts与Spring Boot的结合为数据可视化提供了强大而灵活的解决方案。通过本文的指导,你已经掌握了从零开始构建动态数据可视化系统的关键技能,可以将其应用到实际项目中,实现数据驱动的决策支持。 |
|