活动公告

系统通知
05-18 21:22
系统通知
通知:本站资源由网友上传分享,如有违规等问题请到版务模块进行投诉,资源失效请在帖子内回复要求补档,会尽快处理!
10-23 09:31

Chart.js折线图绘制全解析手把手教你创建动态数据可视化

SunJu_FaceMall

3万

主题

2860

科技点

3万

积分

白金月票

碾压王

积分
32872

塔罗立华奏

<font color=白金月票" /> 发表于 2025-9-9 18:50:12 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
引言

在数据驱动的时代,数据可视化已成为理解和传达复杂数据的关键工具。折线图作为最常用的数据可视化形式之一,能够直观地展示数据随时间或其他连续变量的变化趋势。Chart.js作为一个轻量级、灵活且功能强大的JavaScript图表库,为开发者提供了创建各种类型图表的便捷方式,尤其是折线图。本文将全面解析Chart.js折线图的绘制方法,从基础入门到高级应用,手把手教你创建动态数据可视化效果。

Chart.js简介

Chart.js是一个基于HTML5 Canvas的简单、灵活的JavaScript图表库,由Nick Downie于2013年创建。它具有以下特点:

• 轻量级:压缩后仅约11KB,加载速度快
• 响应式设计:图表能自动适应容器大小
• 跨浏览器兼容:支持所有现代浏览器
• 8种图表类型:包括折线图、柱状图、饼图等
• 丰富的配置选项:可自定义颜色、样式、动画等
• 开源免费:基于MIT许可证,可自由使用和修改

Chart.js使用Canvas元素渲染图表,性能优异,特别适合需要频繁更新的动态数据可视化场景。

环境准备

安装Chart.js

有几种方式可以引入Chart.js到你的项目中:

最简单的方式是直接使用CDN链接,在HTML文件中添加以下代码:
  1. <!-- 引入Chart.js -->
  2. <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
复制代码

如果你使用Node.js环境,可以通过npm安装:
  1. npm install chart.js
复制代码

然后在JavaScript文件中引入:
  1. import Chart from 'chart.js';
复制代码

你也可以直接从Chart.js官网下载最新版本的库文件,然后在项目中引入:
  1. <script src="path/to/chart.min.js"></script>
复制代码

准备HTML结构

创建一个基本的HTML文件,包含一个canvas元素作为图表的容器:
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6.     <title>Chart.js折线图示例</title>
  7.     <style>
  8.         .chart-container {
  9.             width: 800px;
  10.             height: 400px;
  11.             margin: 50px auto;
  12.         }
  13.     </style>
  14. </head>
  15. <body>
  16.     <div class="chart-container">
  17.         <canvas id="myLineChart"></canvas>
  18.     </div>
  19.    
  20.     <!-- 引入Chart.js -->
  21.     <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  22.     <script src="app.js"></script>
  23. </body>
  24. </html>
复制代码

基础折线图创建

创建第一个折线图

现在,让我们创建一个简单的折线图。在app.js文件中添加以下代码:
  1. // 获取canvas元素和上下文
  2. const ctx = document.getElementById('myLineChart').getContext('2d');
  3. // 定义图表数据
  4. const data = {
  5.     labels: ['一月', '二月', '三月', '四月', '五月', '六月', '七月'],
  6.     datasets: [{
  7.         label: '销售额(万元)',
  8.         data: [65, 59, 80, 81, 56, 55, 70],
  9.         borderColor: 'rgb(75, 192, 192)',
  10.         backgroundColor: 'rgba(75, 192, 192, 0.2)',
  11.         tension: 0.1
  12.     }]
  13. };
  14. // 定义图表配置
  15. const config = {
  16.     type: 'line', // 图表类型
  17.     data: data,   // 图表数据
  18.     options: {}   // 图表选项
  19. };
  20. // 创建图表
  21. const myLineChart = new Chart(ctx, config);
复制代码

这段代码会创建一个基本的折线图,显示七个月的销售数据。让我们分析一下代码的各个部分:

1. 获取canvas上下文:我们首先获取canvas元素和它的2D渲染上下文。
2. 定义数据:labels:X轴的标签,这里表示月份。datasets:数据集数组,每个对象代表一条线。label:数据集的标签,显示在图例中。data:Y轴的数据点。borderColor:线条的颜色。backgroundColor:线条下方填充区域的颜色。tension:控制线条的曲率,0表示直线,1表示最大曲率。
3. labels:X轴的标签,这里表示月份。
4. datasets:数据集数组,每个对象代表一条线。label:数据集的标签,显示在图例中。data:Y轴的数据点。borderColor:线条的颜色。backgroundColor:线条下方填充区域的颜色。tension:控制线条的曲率,0表示直线,1表示最大曲率。
5. label:数据集的标签,显示在图例中。
6. data:Y轴的数据点。
7. borderColor:线条的颜色。
8. backgroundColor:线条下方填充区域的颜色。
9. tension:控制线条的曲率,0表示直线,1表示最大曲率。
10. 定义配置:type:图表类型,这里是’line’表示折线图。data:上面定义的数据。options:图表的配置选项,这里暂时为空。
11. type:图表类型,这里是’line’表示折线图。
12. data:上面定义的数据。
13. options:图表的配置选项,这里暂时为空。
14. 创建图表:使用new Chart()构造函数创建图表实例。

• labels:X轴的标签,这里表示月份。
• datasets:数据集数组,每个对象代表一条线。label:数据集的标签,显示在图例中。data:Y轴的数据点。borderColor:线条的颜色。backgroundColor:线条下方填充区域的颜色。tension:控制线条的曲率,0表示直线,1表示最大曲率。
• label:数据集的标签,显示在图例中。
• data:Y轴的数据点。
• borderColor:线条的颜色。
• backgroundColor:线条下方填充区域的颜色。
• tension:控制线条的曲率,0表示直线,1表示最大曲率。

• label:数据集的标签,显示在图例中。
• data:Y轴的数据点。
• borderColor:线条的颜色。
• backgroundColor:线条下方填充区域的颜色。
• tension:控制线条的曲率,0表示直线,1表示最大曲率。

• type:图表类型,这里是’line’表示折线图。
• data:上面定义的数据。
• options:图表的配置选项,这里暂时为空。

多数据集折线图

通常我们需要在同一个图表中显示多个数据集以便比较。下面是一个包含两个数据集的折线图示例:
  1. const ctx = document.getElementById('myLineChart').getContext('2d');
  2. const data = {
  3.     labels: ['一月', '二月', '三月', '四月', '五月', '六月', '七月'],
  4.     datasets: [
  5.         {
  6.             label: '产品A销售额(万元)',
  7.             data: [65, 59, 80, 81, 56, 55, 70],
  8.             borderColor: 'rgb(75, 192, 192)',
  9.             backgroundColor: 'rgba(75, 192, 192, 0.2)',
  10.             tension: 0.1
  11.         },
  12.         {
  13.             label: '产品B销售额(万元)',
  14.             data: [28, 48, 40, 19, 86, 27, 90],
  15.             borderColor: 'rgb(255, 99, 132)',
  16.             backgroundColor: 'rgba(255, 99, 132, 0.2)',
  17.             tension: 0.1
  18.         }
  19.     ]
  20. };
  21. const config = {
  22.     type: 'line',
  23.     data: data,
  24.     options: {
  25.         responsive: true,
  26.         plugins: {
  27.             title: {
  28.                 display: true,
  29.                 text: '产品销售趋势对比'
  30.             },
  31.             legend: {
  32.                 position: 'top',
  33.             }
  34.         }
  35.     }
  36. };
  37. const myLineChart = new Chart(ctx, config);
复制代码

在这个例子中,我们添加了第二个数据集来表示产品B的销售额,并添加了一些基本配置选项:

• responsive: true:使图表响应式,能够自适应容器大小。
• plugins.title:设置图表标题。
• plugins.legend:设置图例位置。

折线图配置详解

Chart.js提供了丰富的配置选项,让我们可以自定义图表的各个方面。下面详细介绍一些常用的配置选项。

标题配置
  1. options: {
  2.     plugins: {
  3.         title: {
  4.             display: true,          // 是否显示标题
  5.             text: '月度销售趋势',   // 标题文本
  6.             color: '#333',          // 标题颜色
  7.             font: {
  8.                 size: 16,           // 字体大小
  9.                 weight: 'bold'      // 字体粗细
  10.             },
  11.             padding: 20             // 标题内边距
  12.         }
  13.     }
  14. }
复制代码

图例配置
  1. options: {
  2.     plugins: {
  3.         legend: {
  4.             display: true,          // 是否显示图例
  5.             position: 'top',        // 图例位置:'top', 'bottom', 'left', 'right'
  6.             align: 'center',        // 图例对齐方式:'start', 'center', 'end'
  7.             labels: {
  8.                 color: '#333',      // 标签颜色
  9.                 font: {
  10.                     size: 12        // 字体大小
  11.                 },
  12.                 padding: 20,        // 标签内边距
  13.                 usePointStyle: true // 使用数据点样式
  14.             }
  15.         }
  16.     }
  17. }
复制代码

坐标轴配置
  1. options: {
  2.     scales: {
  3.         x: {
  4.             display: true,                  // 是否显示X轴
  5.             title: {
  6.                 display: true,              // 是否显示X轴标题
  7.                 text: '月份',               // X轴标题文本
  8.                 color: '#666',              // 标题颜色
  9.                 font: {
  10.                     size: 14,               // 标题字体大小
  11.                     weight: 'bold'          // 标题字体粗细
  12.                 }
  13.             },
  14.             ticks: {
  15.                 color: '#666',              // 刻度标签颜色
  16.                 font: {
  17.                     size: 12                // 刻度标签字体大小
  18.                 }
  19.             },
  20.             grid: {
  21.                 display: true,              // 是否显示网格线
  22.                 color: 'rgba(0, 0, 0, 0.1)' // 网格线颜色
  23.             }
  24.         }
  25.     }
  26. }
复制代码
  1. options: {
  2.     scales: {
  3.         y: {
  4.             display: true,                  // 是否显示Y轴
  5.             title: {
  6.                 display: true,              // 是否显示Y轴标题
  7.                 text: '销售额(万元)',      // Y轴标题文本
  8.                 color: '#666',              // 标题颜色
  9.                 font: {
  10.                     size: 14,               // 标题字体大小
  11.                     weight: 'bold'          // 标题字体粗细
  12.                 }
  13.             },
  14.             ticks: {
  15.                 color: '#666',              // 刻度标签颜色
  16.                 font: {
  17.                     size: 12                // 刻度标签字体大小
  18.                 },
  19.                 // 格式化Y轴刻度标签
  20.                 callback: function(value) {
  21.                     return value + '万';
  22.                 }
  23.             },
  24.             grid: {
  25.                 display: true,              // 是否显示网格线
  26.                 color: 'rgba(0, 0, 0, 0.1)' // 网格线颜色
  27.             },
  28.             // 设置Y轴起始值
  29.             beginAtZero: true,
  30.             // 建议的最大值
  31.             suggestedMax: 100
  32.         }
  33.     }
  34. }
复制代码

数据集样式配置
  1. datasets: [{
  2.     label: '销售额(万元)',
  3.     data: [65, 59, 80, 81, 56, 55, 70],
  4.    
  5.     // 线条样式
  6.     borderColor: 'rgb(75, 192, 192)',      // 线条颜色
  7.     borderWidth: 2,                        // 线条宽度
  8.     borderDash: [],                        // 虚线样式,[]表示实线
  9.     borderDashOffset: 0.0,                 // 虚线偏移量
  10.     tension: 0.4,                          // 线条曲率,0-1之间
  11.     capBezierPoints: true,                 // 是否保持贝塞尔曲线的控制点
  12.    
  13.     // 填充区域样式
  14.     backgroundColor: 'rgba(75, 192, 192, 0.2)', // 填充颜色
  15.     fill: true,                            // 是否填充线下区域
  16.     // 填充模式:'origin'(填充到X轴), 'start'(填充到图表顶部), 'end'(填充到图表底部)
  17.     // 或者相对于其他数据集填充:{value: datasetIndex}
  18.     fill: 'origin',                       
  19.    
  20.     // 数据点样式
  21.     pointBackgroundColor: 'rgb(75, 192, 192)', // 数据点背景色
  22.     pointBorderColor: '#fff',               // 数据点边框色
  23.     pointBorderWidth: 1,                    // 数据点边框宽度
  24.     pointRadius: 4,                         // 数据点半径
  25.     pointHoverRadius: 6,                    // 悬停时数据点半径
  26.     pointHoverBackgroundColor: 'rgb(75, 192, 192)', // 悬停时数据点背景色
  27.     pointHoverBorderColor: '#fff',          // 悬停时数据点边框色
  28.     pointHoverBorderWidth: 2,               // 悬停时数据点边框宽度
  29.     pointStyle: 'circle',                   // 数据点样式:'circle', 'rect', 'triangle', 'rectRot', 'cross', 'crossRot', 'star', 'line', 'dash'
  30.     pointRotation: 0,                       // 数据点旋转角度
  31.     pointHitRadius: 10,                     // 鼠标点击数据点的命中半径
  32.    
  33.     // 其他配置
  34.     showLine: true,                         // 是否显示线条
  35.     spanGaps: false,                        // 是否跨越空值
  36.     stepped: false                          // 是否为阶梯线图
  37. }]
复制代码

工具提示配置
  1. options: {
  2.     plugins: {
  3.         tooltip: {
  4.             enabled: true,                   // 是否启用工具提示
  5.             mode: 'index',                   // 工具提示模式:'index', 'dataset', 'point', 'nearest'
  6.             intersect: false,                // 工具提示是否与元素相交
  7.             backgroundColor: 'rgba(0, 0, 0, 0.8)', // 工具提示背景色
  8.             titleColor: '#fff',              // 标题颜色
  9.             titleFont: {
  10.                 size: 14,                   // 标题字体大小
  11.                 weight: 'bold'              // 标题字体粗细
  12.             },
  13.             bodyColor: '#fff',               // 内容颜色
  14.             bodyFont: {
  15.                 size: 12                    // 内容字体大小
  16.             },
  17.             padding: 10,                     // 工具提示内边距
  18.             cornerRadius: 4,                 // 工具提示圆角
  19.             displayColors: true,             // 是否显示颜色框
  20.             borderColor: '#ddd',             // 工具提示边框颜色
  21.             borderWidth: 1,                   // 工具提示边框宽度
  22.             
  23.             // 自定义工具提示回调函数
  24.             callbacks: {
  25.                 title: function(tooltipItems) {
  26.                     // 自定义标题
  27.                     return '月份: ' + tooltipItems[0].label;
  28.                 },
  29.                 label: function(context) {
  30.                     // 自定义标签
  31.                     let label = context.dataset.label || '';
  32.                     if (label) {
  33.                         label += ': ';
  34.                     }
  35.                     if (context.parsed.y !== null) {
  36.                         label += context.parsed.y + '万元';
  37.                     }
  38.                     return label;
  39.                 }
  40.             }
  41.         }
  42.     }
  43. }
复制代码

动画配置
  1. options: {
  2.     animation: {
  3.         duration: 1000,                     // 动画持续时间(毫秒)
  4.         easing: 'easeOutQuart',             // 动画缓动函数
  5.         delay: (context) => {
  6.             // 为每个数据点设置不同的延迟
  7.             let delay = 0;
  8.             if (context.type === 'data' && context.mode === 'default') {
  9.                 delay = context.dataIndex * 100 + context.datasetIndex * 100;
  10.             }
  11.             return delay;
  12.         },
  13.         loop: false,                        // 是否循环动画
  14.         
  15.         // 动画完成时的回调
  16.         onComplete: () => {
  17.             console.log('动画完成');
  18.         },
  19.         
  20.         // 动画进行中的回调
  21.         onProgress: (animation) => {
  22.             console.log('动画进度: ' + animation.currentStep / animation.numSteps);
  23.         }
  24.     }
  25. }
复制代码

高级功能

多轴折线图

当需要在同一图表中显示不同单位或范围的数据时,可以使用多轴折线图:
  1. const ctx = document.getElementById('myLineChart').getContext('2d');
  2. const data = {
  3.     labels: ['一月', '二月', '三月', '四月', '五月', '六月', '七月'],
  4.     datasets: [
  5.         {
  6.             label: '销售额(万元)',
  7.             data: [65, 59, 80, 81, 56, 55, 70],
  8.             borderColor: 'rgb(75, 192, 192)',
  9.             backgroundColor: 'rgba(75, 192, 192, 0.2)',
  10.             yAxisID: 'y',  // 关联到Y轴
  11.         },
  12.         {
  13.             label: '订单量(千单)',
  14.             data: [28, 48, 40, 19, 86, 27, 90],
  15.             borderColor: 'rgb(255, 99, 132)',
  16.             backgroundColor: 'rgba(255, 99, 132, 0.2)',
  17.             yAxisID: 'y1', // 关联到第二个Y轴
  18.         }
  19.     ]
  20. };
  21. const config = {
  22.     type: 'line',
  23.     data: data,
  24.     options: {
  25.         responsive: true,
  26.         interaction: {
  27.             mode: 'index',
  28.             intersect: false,
  29.         },
  30.         scales: {
  31.             y: {
  32.                 type: 'linear',
  33.                 display: true,
  34.                 position: 'left',
  35.                 title: {
  36.                     display: true,
  37.                     text: '销售额(万元)'
  38.                 }
  39.             },
  40.             y1: {
  41.                 type: 'linear',
  42.                 display: true,
  43.                 position: 'right',
  44.                 title: {
  45.                     display: true,
  46.                     text: '订单量(千单)'
  47.                 },
  48.                 // 确保右侧Y轴不与左侧重叠
  49.                 grid: {
  50.                     drawOnChartArea: false,
  51.                 },
  52.             },
  53.         }
  54.     }
  55. };
  56. const myLineChart = new Chart(ctx, config);
复制代码

堆叠折线图

堆叠折线图可以显示数据的累积效果:
  1. const ctx = document.getElementById('myLineChart').getContext('2d');
  2. const data = {
  3.     labels: ['一月', '二月', '三月', '四月', '五月', '六月', '七月'],
  4.     datasets: [
  5.         {
  6.             label: '产品A',
  7.             data: [65, 59, 80, 81, 56, 55, 70],
  8.             borderColor: 'rgb(75, 192, 192)',
  9.             backgroundColor: 'rgba(75, 192, 192, 0.2)',
  10.             stack: 'Stack 0', // 堆叠组
  11.         },
  12.         {
  13.             label: '产品B',
  14.             data: [28, 48, 40, 19, 86, 27, 90],
  15.             borderColor: 'rgb(255, 99, 132)',
  16.             backgroundColor: 'rgba(255, 99, 132, 0.2)',
  17.             stack: 'Stack 0', // 同一组的数据集将堆叠在一起
  18.         }
  19.     ]
  20. };
  21. const config = {
  22.     type: 'line',
  23.     data: data,
  24.     options: {
  25.         responsive: true,
  26.         plugins: {
  27.             title: {
  28.                 display: true,
  29.                 text: '产品销售堆叠趋势'
  30.             }
  31.         },
  32.         scales: {
  33.             y: {
  34.                 stacked: true, // 启用Y轴堆叠
  35.             }
  36.         }
  37.     }
  38. };
  39. const myLineChart = new Chart(ctx, config);
复制代码

渐变填充折线图

使用Canvas API创建渐变效果,使折线图更加美观:
  1. const ctx = document.getElementById('myLineChart').getContext('2d');
  2. // 创建渐变
  3. const gradient = ctx.createLinearGradient(0, 0, 0, 400);
  4. gradient.addColorStop(0, 'rgba(75, 192, 192, 0.6)');
  5. gradient.addColorStop(1, 'rgba(75, 192, 192, 0.1)');
  6. const data = {
  7.     labels: ['一月', '二月', '三月', '四月', '五月', '六月', '七月'],
  8.     datasets: [{
  9.         label: '销售额(万元)',
  10.         data: [65, 59, 80, 81, 56, 55, 70],
  11.         borderColor: 'rgb(75, 192, 192)',
  12.         backgroundColor: gradient, // 使用渐变
  13.         tension: 0.4,
  14.         fill: true
  15.     }]
  16. };
  17. const config = {
  18.     type: 'line',
  19.     data: data,
  20.     options: {
  21.         responsive: true,
  22.         plugins: {
  23.             title: {
  24.                 display: true,
  25.                 text: '渐变填充销售趋势'
  26.             }
  27.         }
  28.     }
  29. };
  30. const myLineChart = new Chart(ctx, config);
复制代码

阶梯折线图

阶梯折线图适合展示离散变化的数据:
  1. const ctx = document.getElementById('myLineChart').getContext('2d');
  2. const data = {
  3.     labels: ['一月', '二月', '三月', '四月', '五月', '六月', '七月'],
  4.     datasets: [{
  5.         label: '销售额(万元)',
  6.         data: [65, 59, 80, 81, 56, 55, 70],
  7.         borderColor: 'rgb(75, 192, 192)',
  8.         backgroundColor: 'rgba(75, 192, 192, 0.2)',
  9.         stepped: true, // 启用阶梯线
  10.         tension: 0
  11.     }]
  12. };
  13. const config = {
  14.     type: 'line',
  15.     data: data,
  16.     options: {
  17.         responsive: true,
  18.         plugins: {
  19.             title: {
  20.                 display: true,
  21.                 text: '阶梯销售趋势'
  22.             }
  23.         }
  24.     }
  25. };
  26. const myLineChart = new Chart(ctx, config);
复制代码

动态数据可视化

Chart.js的强大之处在于它能够轻松实现动态数据可视化,让我们能够实时更新图表数据,创建交互式的数据展示效果。

实时更新数据

以下是一个实时更新折线图数据的示例,模拟实时数据流:
  1. const ctx = document.getElementById('myLineChart').getContext('2d');
  2. // 初始数据
  3. const initialData = {
  4.     labels: [],
  5.     datasets: [{
  6.         label: '实时数据',
  7.         data: [],
  8.         borderColor: 'rgb(75, 192, 192)',
  9.         backgroundColor: 'rgba(75, 192, 192, 0.2)',
  10.         tension: 0.4
  11.     }]
  12. };
  13. // 配置选项
  14. const config = {
  15.     type: 'line',
  16.     data: initialData,
  17.     options: {
  18.         responsive: true,
  19.         scales: {
  20.             x: {
  21.                 display: true,
  22.                 title: {
  23.                     display: true,
  24.                     text: '时间'
  25.                 }
  26.             },
  27.             y: {
  28.                 display: true,
  29.                 title: {
  30.                     display: true,
  31.                     text: '数值'
  32.                 },
  33.                 suggestedMin: 0,
  34.                 suggestedMax: 100
  35.             }
  36.         },
  37.         animation: {
  38.             duration: 0 // 禁用动画以提高性能
  39.         }
  40.     }
  41. };
  42. // 创建图表
  43. const myLineChart = new Chart(ctx, config);
  44. // 数据更新函数
  45. function addData() {
  46.     // 生成随机数据
  47.     const newValue = Math.floor(Math.random() * 100);
  48.     const now = new Date();
  49.     const timeLabel = now.getHours() + ':' + now.getMinutes() + ':' + now.getSeconds();
  50.    
  51.     // 添加新数据
  52.     myLineChart.data.labels.push(timeLabel);
  53.     myLineChart.data.datasets[0].data.push(newValue);
  54.    
  55.     // 如果数据点超过20个,移除最旧的数据
  56.     if (myLineChart.data.labels.length > 20) {
  57.         myLineChart.data.labels.shift();
  58.         myLineChart.data.datasets[0].data.shift();
  59.     }
  60.    
  61.     // 更新图表
  62.     myLineChart.update();
  63. }
  64. // 每秒更新一次数据
  65. setInterval(addData, 1000);
复制代码

交互式数据控制

创建一个交互式界面,允许用户控制图表数据的显示:
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6.     <title>交互式折线图</title>
  7.     <style>
  8.         .chart-container {
  9.             width: 800px;
  10.             height: 400px;
  11.             margin: 20px auto;
  12.         }
  13.         .controls {
  14.             width: 800px;
  15.             margin: 0 auto;
  16.             padding: 15px;
  17.             background-color: #f5f5f5;
  18.             border-radius: 5px;
  19.             display: flex;
  20.             justify-content: space-between;
  21.             align-items: center;
  22.         }
  23.         .control-group {
  24.             display: flex;
  25.             align-items: center;
  26.         }
  27.         .control-group label {
  28.             margin-right: 10px;
  29.         }
  30.         button {
  31.             padding: 8px 15px;
  32.             background-color: #4CAF50;
  33.             color: white;
  34.             border: none;
  35.             border-radius: 4px;
  36.             cursor: pointer;
  37.         }
  38.         button:hover {
  39.             background-color: #45a049;
  40.         }
  41.         input[type="checkbox"] {
  42.             margin-right: 5px;
  43.         }
  44.         input[type="range"] {
  45.             width: 150px;
  46.         }
  47.     </style>
  48. </head>
  49. <body>
  50.     <div class="controls">
  51.         <div class="control-group">
  52.             <button id="addDataBtn">添加数据点</button>
  53.             <button id="removeDataBtn">移除数据点</button>
  54.         </div>
  55.         <div class="control-group">
  56.             <label>
  57.                 <input type="checkbox" id="fillToggle" checked> 填充区域
  58.             </label>
  59.             <label>
  60.                 <input type="checkbox" id="pointsToggle" checked> 显示数据点
  61.             </label>
  62.         </div>
  63.         <div class="control-group">
  64.             <label for="tensionRange">线条曲率:</label>
  65.             <input type="range" id="tensionRange" min="0" max="0.5" step="0.1" value="0.4">
  66.             <span id="tensionValue">0.4</span>
  67.         </div>
  68.     </div>
  69.    
  70.     <div class="chart-container">
  71.         <canvas id="myLineChart"></canvas>
  72.     </div>
  73.    
  74.     <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  75.     <script>
  76.         const ctx = document.getElementById('myLineChart').getContext('2d');
  77.         
  78.         // 初始数据
  79.         const data = {
  80.             labels: ['一月', '二月', '三月', '四月', '五月', '六月', '七月'],
  81.             datasets: [{
  82.                 label: '销售额(万元)',
  83.                 data: [65, 59, 80, 81, 56, 55, 70],
  84.                 borderColor: 'rgb(75, 192, 192)',
  85.                 backgroundColor: 'rgba(75, 192, 192, 0.2)',
  86.                 tension: 0.4,
  87.                 fill: true,
  88.                 pointRadius: 4,
  89.                 pointHoverRadius: 6
  90.             }]
  91.         };
  92.         
  93.         // 配置选项
  94.         const config = {
  95.             type: 'line',
  96.             data: data,
  97.             options: {
  98.                 responsive: true,
  99.                 plugins: {
  100.                     title: {
  101.                         display: true,
  102.                         text: '交互式销售趋势'
  103.                     }
  104.                 }
  105.             }
  106.         };
  107.         
  108.         // 创建图表
  109.         const myLineChart = new Chart(ctx, config);
  110.         
  111.         // 获取控制元素
  112.         const addDataBtn = document.getElementById('addDataBtn');
  113.         const removeDataBtn = document.getElementById('removeDataBtn');
  114.         const fillToggle = document.getElementById('fillToggle');
  115.         const pointsToggle = document.getElementById('pointsToggle');
  116.         const tensionRange = document.getElementById('tensionRange');
  117.         const tensionValue = document.getElementById('tensionValue');
  118.         
  119.         // 添加数据点
  120.         addDataBtn.addEventListener('click', function() {
  121.             const months = ['八月', '九月', '十月', '十一月', '十二月'];
  122.             const currentLabels = myLineChart.data.labels;
  123.             
  124.             // 检查是否还有月份可以添加
  125.             const nextMonthIndex = currentLabels.length - 7;
  126.             if (nextMonthIndex < months.length) {
  127.                 // 添加新标签和数据
  128.                 myLineChart.data.labels.push(months[nextMonthIndex]);
  129.                 myLineChart.data.datasets[0].data.push(Math.floor(Math.random() * 40) + 60);
  130.                
  131.                 // 更新图表
  132.                 myLineChart.update();
  133.             } else {
  134.                 alert('已添加所有月份的数据!');
  135.             }
  136.         });
  137.         
  138.         // 移除数据点
  139.         removeDataBtn.addEventListener('click', function() {
  140.             if (myLineChart.data.labels.length > 1) {
  141.                 // 移除最后一个标签和数据
  142.                 myLineChart.data.labels.pop();
  143.                 myLineChart.data.datasets[0].data.pop();
  144.                
  145.                 // 更新图表
  146.                 myLineChart.update();
  147.             } else {
  148.                 alert('至少需要保留一个数据点!');
  149.             }
  150.         });
  151.         
  152.         // 切换填充区域
  153.         fillToggle.addEventListener('change', function() {
  154.             myLineChart.data.datasets[0].fill = this.checked;
  155.             myLineChart.update();
  156.         });
  157.         
  158.         // 切换数据点显示
  159.         pointsToggle.addEventListener('change', function() {
  160.             if (this.checked) {
  161.                 myLineChart.data.datasets[0].pointRadius = 4;
  162.                 myLineChart.data.datasets[0].pointHoverRadius = 6;
  163.             } else {
  164.                 myLineChart.data.datasets[0].pointRadius = 0;
  165.                 myLineChart.data.datasets[0].pointHoverRadius = 0;
  166.             }
  167.             myLineChart.update();
  168.         });
  169.         
  170.         // 调整线条曲率
  171.         tensionRange.addEventListener('input', function() {
  172.             const value = parseFloat(this.value);
  173.             tensionValue.textContent = value.toFixed(1);
  174.             myLineChart.data.datasets[0].tension = value;
  175.             myLineChart.update();
  176.         });
  177.     </script>
  178. </body>
  179. </html>
复制代码

数据筛选与缩放

实现数据筛选和缩放功能,让用户能够聚焦于特定的数据范围:
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6.     <title>可缩放折线图</title>
  7.     <style>
  8.         .chart-container {
  9.             width: 800px;
  10.             height: 400px;
  11.             margin: 20px auto;
  12.             position: relative;
  13.         }
  14.         .controls {
  15.             width: 800px;
  16.             margin: 0 auto;
  17.             padding: 15px;
  18.             background-color: #f5f5f5;
  19.             border-radius: 5px;
  20.             display: flex;
  21.             justify-content: space-between;
  22.             align-items: center;
  23.         }
  24.         .control-group {
  25.             display: flex;
  26.             align-items: center;
  27.         }
  28.         .control-group label {
  29.             margin-right: 10px;
  30.         }
  31.         button {
  32.             padding: 8px 15px;
  33.             background-color: #4CAF50;
  34.             color: white;
  35.             border: none;
  36.             border-radius: 4px;
  37.             cursor: pointer;
  38.             margin-right: 5px;
  39.         }
  40.         button:hover {
  41.             background-color: #45a049;
  42.         }
  43.         select {
  44.             padding: 5px;
  45.             border-radius: 4px;
  46.             border: 1px solid #ddd;
  47.         }
  48.     </style>
  49. </head>
  50. <body>
  51.     <div class="controls">
  52.         <div class="control-group">
  53.             <button id="resetZoomBtn">重置缩放</button>
  54.             <label for="datasetSelect">选择数据集:</label>
  55.             <select id="datasetSelect">
  56.                 <option value="all">全部显示</option>
  57.                 <option value="0">产品A</option>
  58.                 <option value="1">产品B</option>
  59.                 <option value="2">产品C</option>
  60.             </select>
  61.         </div>
  62.         <div class="control-group">
  63.             <label>
  64.                 <input type="checkbox" id="animationToggle" checked> 启用动画
  65.             </label>
  66.         </div>
  67.     </div>
  68.    
  69.     <div class="chart-container">
  70.         <canvas id="myLineChart"></canvas>
  71.     </div>
  72.    
  73.     <!-- 引入Chart.js和缩放插件 -->
  74.     <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  75.     <script src="https://cdn.jsdelivr.net/npm/hammerjs@2.0.8"></script>
  76.     <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom@1.2.1"></script>
  77.     <script>
  78.         const ctx = document.getElementById('myLineChart').getContext('2d');
  79.         
  80.         // 生成更多数据点
  81.         function generateLabels() {
  82.             const labels = [];
  83.             for (let i = 1; i <= 30; i++) {
  84.                 labels.push('第' + i + '天');
  85.             }
  86.             return labels;
  87.         }
  88.         
  89.         function generateData() {
  90.             const data = [];
  91.             let value = 50;
  92.             for (let i = 0; i < 30; i++) {
  93.                 value += Math.random() * 10 - 5;
  94.                 data.push(Math.max(10, Math.min(90, value)));
  95.             }
  96.             return data;
  97.         }
  98.         
  99.         // 初始数据
  100.         const data = {
  101.             labels: generateLabels(),
  102.             datasets: [
  103.                 {
  104.                     label: '产品A',
  105.                     data: generateData(),
  106.                     borderColor: 'rgb(75, 192, 192)',
  107.                     backgroundColor: 'rgba(75, 192, 192, 0.2)',
  108.                     tension: 0.4,
  109.                     hidden: false
  110.                 },
  111.                 {
  112.                     label: '产品B',
  113.                     data: generateData(),
  114.                     borderColor: 'rgb(255, 99, 132)',
  115.                     backgroundColor: 'rgba(255, 99, 132, 0.2)',
  116.                     tension: 0.4,
  117.                     hidden: false
  118.                 },
  119.                 {
  120.                     label: '产品C',
  121.                     data: generateData(),
  122.                     borderColor: 'rgb(255, 205, 86)',
  123.                     backgroundColor: 'rgba(255, 205, 86, 0.2)',
  124.                     tension: 0.4,
  125.                     hidden: false
  126.                 }
  127.             ]
  128.         };
  129.         
  130.         // 配置选项
  131.         const config = {
  132.             type: 'line',
  133.             data: data,
  134.             options: {
  135.                 responsive: true,
  136.                 interaction: {
  137.                     mode: 'index',
  138.                     intersect: false,
  139.                 },
  140.                 plugins: {
  141.                     title: {
  142.                         display: true,
  143.                         text: '产品销售趋势(可缩放)'
  144.                     },
  145.                     zoom: {
  146.                         pan: {
  147.                             enabled: true,
  148.                             mode: 'x', // 只允许水平方向平移
  149.                         },
  150.                         zoom: {
  151.                             wheel: {
  152.                                 enabled: true, // 启用鼠标滚轮缩放
  153.                             },
  154.                             pinch: {
  155.                                 enabled: true // 启用捏合缩放
  156.                             },
  157.                             mode: 'x', // 只允许水平方向缩放
  158.                         }
  159.                     }
  160.                 },
  161.                 scales: {
  162.                     y: {
  163.                         beginAtZero: true
  164.                     }
  165.                 }
  166.             }
  167.         };
  168.         
  169.         // 创建图表
  170.         const myLineChart = new Chart(ctx, config);
  171.         
  172.         // 获取控制元素
  173.         const resetZoomBtn = document.getElementById('resetZoomBtn');
  174.         const datasetSelect = document.getElementById('datasetSelect');
  175.         const animationToggle = document.getElementById('animationToggle');
  176.         
  177.         // 重置缩放
  178.         resetZoomBtn.addEventListener('click', function() {
  179.             myLineChart.resetZoom();
  180.         });
  181.         
  182.         // 选择数据集
  183.         datasetSelect.addEventListener('change', function() {
  184.             const value = this.value;
  185.             if (value === 'all') {
  186.                 // 显示所有数据集
  187.                 myLineChart.data.datasets.forEach(dataset => {
  188.                     dataset.hidden = false;
  189.                 });
  190.             } else {
  191.                 // 只显示选中的数据集
  192.                 const index = parseInt(value);
  193.                 myLineChart.data.datasets.forEach((dataset, i) => {
  194.                     dataset.hidden = i !== index;
  195.                 });
  196.             }
  197.             myLineChart.update();
  198.         });
  199.         
  200.         // 切换动画
  201.         animationToggle.addEventListener('change', function() {
  202.             myLineChart.options.animation.duration = this.checked ? 1000 : 0;
  203.             myLineChart.update();
  204.         });
  205.     </script>
  206. </body>
  207. </html>
复制代码

实战案例:创建一个完整的动态数据可视化项目

让我们结合前面所学知识,创建一个完整的动态数据可视化项目,模拟实时监控多个产品的销售情况。

项目概述

我们将创建一个实时销售监控系统,具有以下功能:

1. 实时显示多个产品的销售数据
2. 支持数据筛选和切换
3. 提供数据统计信息
4. 支持导出图表为图片
5. 响应式设计,适配不同屏幕尺寸

完整代码实现
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6.     <title>实时销售监控系统</title>
  7.     <style>
  8.         * {
  9.             box-sizing: border-box;
  10.             margin: 0;
  11.             padding: 0;
  12.         }
  13.         
  14.         body {
  15.             font-family: 'Arial', sans-serif;
  16.             background-color: #f5f7fa;
  17.             color: #333;
  18.         }
  19.         
  20.         .header {
  21.             background-color: #2c3e50;
  22.             color: white;
  23.             padding: 20px;
  24.             text-align: center;
  25.             box-shadow: 0 2px 5px rgba(0,0,0,0.1);
  26.         }
  27.         
  28.         .header h1 {
  29.             margin-bottom: 5px;
  30.         }
  31.         
  32.         .header p {
  33.             opacity: 0.8;
  34.         }
  35.         
  36.         .container {
  37.             max-width: 1200px;
  38.             margin: 0 auto;
  39.             padding: 20px;
  40.         }
  41.         
  42.         .dashboard {
  43.             display: grid;
  44.             grid-template-columns: 1fr 1fr 1fr;
  45.             gap: 20px;
  46.             margin-bottom: 20px;
  47.         }
  48.         
  49.         .card {
  50.             background-color: white;
  51.             border-radius: 8px;
  52.             padding: 20px;
  53.             box-shadow: 0 2px 10px rgba(0,0,0,0.05);
  54.             transition: transform 0.3s;
  55.         }
  56.         
  57.         .card:hover {
  58.             transform: translateY(-5px);
  59.         }
  60.         
  61.         .card-title {
  62.             font-size: 16px;
  63.             color: #7f8c8d;
  64.             margin-bottom: 10px;
  65.         }
  66.         
  67.         .card-value {
  68.             font-size: 28px;
  69.             font-weight: bold;
  70.             color: #2c3e50;
  71.         }
  72.         
  73.         .card-change {
  74.             font-size: 14px;
  75.             margin-top: 5px;
  76.         }
  77.         
  78.         .positive {
  79.             color: #27ae60;
  80.         }
  81.         
  82.         .negative {
  83.             color: #e74c3c;
  84.         }
  85.         
  86.         .chart-container {
  87.             background-color: white;
  88.             border-radius: 8px;
  89.             padding: 20px;
  90.             box-shadow: 0 2px 10px rgba(0,0,0,0.05);
  91.             margin-bottom: 20px;
  92.         }
  93.         
  94.         .chart-header {
  95.             display: flex;
  96.             justify-content: space-between;
  97.             align-items: center;
  98.             margin-bottom: 20px;
  99.         }
  100.         
  101.         .chart-title {
  102.             font-size: 18px;
  103.             font-weight: bold;
  104.             color: #2c3e50;
  105.         }
  106.         
  107.         .chart-controls {
  108.             display: flex;
  109.             gap: 10px;
  110.         }
  111.         
  112.         .btn {
  113.             padding: 8px 15px;
  114.             border: none;
  115.             border-radius: 4px;
  116.             cursor: pointer;
  117.             font-size: 14px;
  118.             transition: background-color 0.3s;
  119.         }
  120.         
  121.         .btn-primary {
  122.             background-color: #3498db;
  123.             color: white;
  124.         }
  125.         
  126.         .btn-primary:hover {
  127.             background-color: #2980b9;
  128.         }
  129.         
  130.         .btn-success {
  131.             background-color: #27ae60;
  132.             color: white;
  133.         }
  134.         
  135.         .btn-success:hover {
  136.             background-color: #219653;
  137.         }
  138.         
  139.         .btn-warning {
  140.             background-color: #f39c12;
  141.             color: white;
  142.         }
  143.         
  144.         .btn-warning:hover {
  145.             background-color: #e67e22;
  146.         }
  147.         
  148.         .chart-wrapper {
  149.             position: relative;
  150.             height: 400px;
  151.         }
  152.         
  153.         .data-table {
  154.             background-color: white;
  155.             border-radius: 8px;
  156.             padding: 20px;
  157.             box-shadow: 0 2px 10px rgba(0,0,0,0.05);
  158.             overflow-x: auto;
  159.         }
  160.         
  161.         .data-table table {
  162.             width: 100%;
  163.             border-collapse: collapse;
  164.         }
  165.         
  166.         .data-table th, .data-table td {
  167.             padding: 12px 15px;
  168.             text-align: left;
  169.             border-bottom: 1px solid #ecf0f1;
  170.         }
  171.         
  172.         .data-table th {
  173.             background-color: #f8f9fa;
  174.             font-weight: bold;
  175.             color: #2c3e50;
  176.         }
  177.         
  178.         .data-table tr:hover {
  179.             background-color: #f8f9fa;
  180.         }
  181.         
  182.         .status-indicator {
  183.             display: inline-block;
  184.             width: 10px;
  185.             height: 10px;
  186.             border-radius: 50%;
  187.             margin-right: 5px;
  188.         }
  189.         
  190.         .status-online {
  191.             background-color: #27ae60;
  192.         }
  193.         
  194.         .status-offline {
  195.             background-color: #e74c3c;
  196.         }
  197.         
  198.         @media (max-width: 768px) {
  199.             .dashboard {
  200.                 grid-template-columns: 1fr;
  201.             }
  202.             
  203.             .chart-header {
  204.                 flex-direction: column;
  205.                 align-items: flex-start;
  206.             }
  207.             
  208.             .chart-controls {
  209.                 margin-top: 10px;
  210.             }
  211.         }
  212.     </style>
  213. </head>
  214. <body>
  215.     <div class="header">
  216.         <h1>实时销售监控系统</h1>
  217.         <p>多产品销售数据实时可视化分析</p>
  218.     </div>
  219.    
  220.     <div class="container">
  221.         <!-- 数据统计卡片 -->
  222.         <div class="dashboard">
  223.             <div class="card">
  224.                 <div class="card-title">总销售额</div>
  225.                 <div class="card-value" id="totalSales">¥0</div>
  226.                 <div class="card-change positive" id="totalSalesChange">+0%</div>
  227.             </div>
  228.             <div class="card">
  229.                 <div class="card-title">订单数量</div>
  230.                 <div class="card-value" id="totalOrders">0</div>
  231.                 <div class="card-change positive" id="totalOrdersChange">+0%</div>
  232.             </div>
  233.             <div class="card">
  234.                 <div class="card-title">平均订单价值</div>
  235.                 <div class="card-value" id="avgOrderValue">¥0</div>
  236.                 <div class="card-change negative" id="avgOrderValueChange">-0%</div>
  237.             </div>
  238.         </div>
  239.         
  240.         <!-- 图表区域 -->
  241.         <div class="chart-container">
  242.             <div class="chart-header">
  243.                 <div class="chart-title">产品销售趋势</div>
  244.                 <div class="chart-controls">
  245.                     <button class="btn btn-primary" id="toggleDatasetBtn">切换数据集</button>
  246.                     <button class="btn btn-success" id="exportChartBtn">导出图表</button>
  247.                     <button class="btn btn-warning" id="pauseBtn">暂停更新</button>
  248.                 </div>
  249.             </div>
  250.             <div class="chart-wrapper">
  251.                 <canvas id="salesChart"></canvas>
  252.             </div>
  253.         </div>
  254.         
  255.         <!-- 数据表格 -->
  256.         <div class="data-table">
  257.             <h3 style="margin-bottom: 15px;">产品实时数据</h3>
  258.             <table>
  259.                 <thead>
  260.                     <tr>
  261.                         <th>产品名称</th>
  262.                         <th>状态</th>
  263.                         <th>当前销售额</th>
  264.                         <th>今日订单</th>
  265.                         <th>平均订单价值</th>
  266.                         <th>变化趋势</th>
  267.                     </tr>
  268.                 </thead>
  269.                 <tbody id="productTableBody">
  270.                     <!-- 表格内容将通过JavaScript动态生成 -->
  271.                 </tbody>
  272.             </table>
  273.         </div>
  274.     </div>
  275.    
  276.     <!-- 引入Chart.js -->
  277.     <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  278.     <script>
  279.         // 产品数据
  280.         const products = [
  281.             { name: '产品A', color: 'rgb(75, 192, 192)', status: 'online' },
  282.             { name: '产品B', color: 'rgb(255, 99, 132)', status: 'online' },
  283.             { name: '产品C', color: 'rgb(255, 205, 86)', status: 'online' },
  284.             { name: '产品D', color: 'rgb(54, 162, 235)', status: 'online' },
  285.             { name: '产品E', color: 'rgb(153, 102, 255)', status: 'online' }
  286.         ];
  287.         
  288.         // 初始化图表数据
  289.         const chartData = {
  290.             labels: [],
  291.             datasets: products.map(product => ({
  292.                 label: product.name,
  293.                 data: [],
  294.                 borderColor: product.color,
  295.                 backgroundColor: product.color.replace('rgb', 'rgba').replace(')', ', 0.2)'),
  296.                 tension: 0.4,
  297.                 fill: false
  298.             }))
  299.         };
  300.         
  301.         // 初始化统计值
  302.         let totalSales = 0;
  303.         let totalOrders = 0;
  304.         let avgOrderValue = 0;
  305.         let previousTotalSales = 0;
  306.         let previousTotalOrders = 0;
  307.         let previousAvgOrderValue = 0;
  308.         
  309.         // 图表配置
  310.         const chartConfig = {
  311.             type: 'line',
  312.             data: chartData,
  313.             options: {
  314.                 responsive: true,
  315.                 maintainAspectRatio: false,
  316.                 interaction: {
  317.                     mode: 'index',
  318.                     intersect: false,
  319.                 },
  320.                 plugins: {
  321.                     legend: {
  322.                         position: 'top',
  323.                     },
  324.                     tooltip: {
  325.                         callbacks: {
  326.                             label: function(context) {
  327.                                 let label = context.dataset.label || '';
  328.                                 if (label) {
  329.                                     label += ': ';
  330.                                 }
  331.                                 if (context.parsed.y !== null) {
  332.                                     label += '¥' + context.parsed.y.toFixed(2);
  333.                                 }
  334.                                 return label;
  335.                             }
  336.                         }
  337.                     }
  338.                 },
  339.                 scales: {
  340.                     y: {
  341.                         beginAtZero: true,
  342.                         title: {
  343.                             display: true,
  344.                             text: '销售额 (¥)'
  345.                         },
  346.                         ticks: {
  347.                             callback: function(value) {
  348.                                 return '¥' + value;
  349.                             }
  350.                         }
  351.                     },
  352.                     x: {
  353.                         title: {
  354.                             display: true,
  355.                             text: '时间'
  356.                         }
  357.                     }
  358.                 },
  359.                 animation: {
  360.                     duration: 500
  361.                 }
  362.             }
  363.         };
  364.         
  365.         // 创建图表
  366.         const ctx = document.getElementById('salesChart').getContext('2d');
  367.         const salesChart = new Chart(ctx, chartConfig);
  368.         
  369.         // 获取控制元素
  370.         const toggleDatasetBtn = document.getElementById('toggleDatasetBtn');
  371.         const exportChartBtn = document.getElementById('exportChartBtn');
  372.         const pauseBtn = document.getElementById('pauseBtn');
  373.         const productTableBody = document.getElementById('productTableBody');
  374.         
  375.         // 控制变量
  376.         let isPaused = false;
  377.         let visibleDatasets = new Set(products.map((_, index) => index));
  378.         let dataUpdateInterval;
  379.         
  380.         // 初始化表格
  381.         function initTable() {
  382.             productTableBody.innerHTML = '';
  383.             products.forEach((product, index) => {
  384.                 const row = document.createElement('tr');
  385.                 row.innerHTML = `
  386.                     <td>${product.name}</td>
  387.                     <td>
  388.                         <span class="status-indicator status-${product.status}"></span>
  389.                         ${product.status === 'online' ? '在线' : '离线'}
  390.                     </td>
  391.                     <td>¥<span id="sales-${index}">0.00</span></td>
  392.                     <td><span id="orders-${index}">0</span></td>
  393.                     <td>¥<span id="avgValue-${index}">0.00</span></td>
  394.                     <td><span id="trend-${index}" class="positive">+0%</span></td>
  395.                 `;
  396.                 productTableBody.appendChild(row);
  397.             });
  398.         }
  399.         
  400.         // 更新数据
  401.         function updateData() {
  402.             if (isPaused) return;
  403.             
  404.             // 生成新的时间标签
  405.             const now = new Date();
  406.             const timeLabel = now.getHours() + ':' + now.getMinutes() + ':' + now.getSeconds();
  407.             
  408.             // 添加新标签
  409.             chartData.labels.push(timeLabel);
  410.             
  411.             // 限制数据点数量
  412.             const maxDataPoints = 20;
  413.             if (chartData.labels.length > maxDataPoints) {
  414.                 chartData.labels.shift();
  415.             }
  416.             
  417.             // 重置统计值
  418.             totalSales = 0;
  419.             totalOrders = 0;
  420.             
  421.             // 为每个产品生成新数据
  422.             products.forEach((product, index) => {
  423.                 // 生成随机数据
  424.                 const baseValue = 1000 + index * 200;
  425.                 const randomChange = (Math.random() - 0.5) * 200;
  426.                 const newValue = Math.max(100, baseValue + randomChange);
  427.                
  428.                 // 添加新数据点
  429.                 chartData.datasets[index].data.push(newValue);
  430.                
  431.                 // 限制数据点数量
  432.                 if (chartData.datasets[index].data.length > maxDataPoints) {
  433.                     chartData.datasets[index].data.shift();
  434.                 }
  435.                
  436.                 // 更新表格数据
  437.                 const orders = Math.floor(Math.random() * 50) + 10;
  438.                 const avgValue = newValue / orders;
  439.                 const previousValue = chartData.datasets[index].data.length > 1
  440.                     ? chartData.datasets[index].data[chartData.datasets[index].data.length - 2]
  441.                     : newValue;
  442.                 const changePercent = ((newValue - previousValue) / previousValue * 100).toFixed(1);
  443.                
  444.                 document.getElementById(`sales-${index}`).textContent = newValue.toFixed(2);
  445.                 document.getElementById(`orders-${index}`).textContent = orders;
  446.                 document.getElementById(`avgValue-${index}`).textContent = avgValue.toFixed(2);
  447.                
  448.                 const trendElement = document.getElementById(`trend-${index}`);
  449.                 trendElement.textContent = (changePercent >= 0 ? '+' : '') + changePercent + '%';
  450.                 trendElement.className = changePercent >= 0 ? 'positive' : 'negative';
  451.                
  452.                 // 更新总统计值
  453.                 totalSales += newValue;
  454.                 totalOrders += orders;
  455.             });
  456.             
  457.             // 计算平均订单价值
  458.             avgOrderValue = totalOrders > 0 ? totalSales / totalOrders : 0;
  459.             
  460.             // 更新统计卡片
  461.             updateStatCards();
  462.             
  463.             // 更新图表
  464.             salesChart.update();
  465.         }
  466.         
  467.         // 更新统计卡片
  468.         function updateStatCards() {
  469.             // 计算变化百分比
  470.             const salesChangePercent = previousTotalSales > 0
  471.                 ? ((totalSales - previousTotalSales) / previousTotalSales * 100).toFixed(1)
  472.                 : 0;
  473.             const ordersChangePercent = previousTotalOrders > 0
  474.                 ? ((totalOrders - previousTotalOrders) / previousTotalOrders * 100).toFixed(1)
  475.                 : 0;
  476.             const avgValueChangePercent = previousAvgOrderValue > 0
  477.                 ? ((avgOrderValue - previousAvgOrderValue) / previousAvgOrderValue * 100).toFixed(1)
  478.                 : 0;
  479.             
  480.             // 更新显示
  481.             document.getElementById('totalSales').textContent = '¥' + totalSales.toFixed(2);
  482.             document.getElementById('totalOrders').textContent = totalOrders;
  483.             document.getElementById('avgOrderValue').textContent = '¥' + avgOrderValue.toFixed(2);
  484.             
  485.             // 更新变化百分比
  486.             const salesChangeElement = document.getElementById('totalSalesChange');
  487.             salesChangeElement.textContent = (salesChangePercent >= 0 ? '+' : '') + salesChangePercent + '%';
  488.             salesChangeElement.className = salesChangePercent >= 0 ? 'card-change positive' : 'card-change negative';
  489.             
  490.             const ordersChangeElement = document.getElementById('totalOrdersChange');
  491.             ordersChangeElement.textContent = (ordersChangePercent >= 0 ? '+' : '') + ordersChangePercent + '%';
  492.             ordersChangeElement.className = ordersChangePercent >= 0 ? 'card-change positive' : 'card-change negative';
  493.             
  494.             const avgValueChangeElement = document.getElementById('avgOrderValueChange');
  495.             avgValueChangeElement.textContent = (avgValueChangePercent >= 0 ? '+' : '') + avgValueChangePercent + '%';
  496.             avgValueChangeElement.className = avgValueChangePercent >= 0 ? 'card-change positive' : 'card-change negative';
  497.             
  498.             // 保存当前值作为下一次比较的基础
  499.             previousTotalSales = totalSales;
  500.             previousTotalOrders = totalOrders;
  501.             previousAvgOrderValue = avgOrderValue;
  502.         }
  503.         
  504.         // 切换数据集可见性
  505.         toggleDatasetBtn.addEventListener('click', function() {
  506.             // 如果所有数据集都可见,则隐藏除了第一个之外的所有数据集
  507.             if (visibleDatasets.size === products.length) {
  508.                 visibleDatasets.clear();
  509.                 visibleDatasets.add(0);
  510.                
  511.                 chartData.datasets.forEach((dataset, index) => {
  512.                     dataset.hidden = index !== 0;
  513.                 });
  514.             } else {
  515.                 // 否则显示所有数据集
  516.                 visibleDatasets = new Set(products.map((_, index) => index));
  517.                
  518.                 chartData.datasets.forEach(dataset => {
  519.                     dataset.hidden = false;
  520.                 });
  521.             }
  522.             
  523.             salesChart.update();
  524.         });
  525.         
  526.         // 导出图表
  527.         exportChartBtn.addEventListener('click', function() {
  528.             const link = document.createElement('a');
  529.             link.download = '销售趋势图_' + new Date().toISOString().slice(0, 10) + '.png';
  530.             link.href = salesChart.toBase64Image();
  531.             link.click();
  532.         });
  533.         
  534.         // 暂停/继续更新
  535.         pauseBtn.addEventListener('click', function() {
  536.             isPaused = !isPaused;
  537.             this.textContent = isPaused ? '继续更新' : '暂停更新';
  538.             this.className = isPaused ? 'btn btn-success' : 'btn btn-warning';
  539.         });
  540.         
  541.         // 初始化
  542.         initTable();
  543.         
  544.         // 立即更新一次数据
  545.         updateData();
  546.         
  547.         // 设置定时更新
  548.         dataUpdateInterval = setInterval(updateData, 2000);
  549.     </script>
  550. </body>
  551. </html>
复制代码

项目功能解析

这个实时销售监控系统包含以下主要功能:

1. 数据统计卡片:显示总销售额、订单数量和平均订单价值显示相对于上一次更新的变化百分比使用颜色区分正负变化
2. 显示总销售额、订单数量和平均订单价值
3. 显示相对于上一次更新的变化百分比
4. 使用颜色区分正负变化
5. 实时折线图:显示多个产品的销售趋势支持切换数据集可见性支持导出图表为PNG图片支持暂停/继续数据更新
6. 显示多个产品的销售趋势
7. 支持切换数据集可见性
8. 支持导出图表为PNG图片
9. 支持暂停/继续数据更新
10. 产品数据表格:显示每个产品的实时数据包含状态指示器、销售额、订单数量等信息显示变化趋势
11. 显示每个产品的实时数据
12. 包含状态指示器、销售额、订单数量等信息
13. 显示变化趋势
14. 响应式设计:适配不同屏幕尺寸在移动设备上自动调整布局
15. 适配不同屏幕尺寸
16. 在移动设备上自动调整布局
17. 自动数据更新:每2秒自动更新一次数据模拟实时数据流
18. 每2秒自动更新一次数据
19. 模拟实时数据流

数据统计卡片:

• 显示总销售额、订单数量和平均订单价值
• 显示相对于上一次更新的变化百分比
• 使用颜色区分正负变化

实时折线图:

• 显示多个产品的销售趋势
• 支持切换数据集可见性
• 支持导出图表为PNG图片
• 支持暂停/继续数据更新

产品数据表格:

• 显示每个产品的实时数据
• 包含状态指示器、销售额、订单数量等信息
• 显示变化趋势

响应式设计:

• 适配不同屏幕尺寸
• 在移动设备上自动调整布局

自动数据更新:

• 每2秒自动更新一次数据
• 模拟实时数据流

常见问题与解决方案

问题1:图表不显示或显示空白

可能原因:

• Canvas元素尺寸问题
• 数据格式错误
• Chart.js未正确加载

解决方案:
  1. // 确保Canvas元素有正确的尺寸
  2. <canvas id="myChart" width="400" height="400"></canvas>
  3. // 或者使用CSS设置容器尺寸
  4. .chart-container {
  5.     width: 800px;
  6.     height: 400px;
  7. }
  8. // 检查数据格式是否正确
  9. const data = {
  10.     labels: ['一月', '二月', '三月'], // 确保有标签
  11.     datasets: [{
  12.         label: '数据集',
  13.         data: [10, 20, 30], // 确保有数据
  14.         borderColor: 'rgb(75, 192, 192)',
  15.         backgroundColor: 'rgba(75, 192, 192, 0.2)',
  16.     }]
  17. };
  18. // 确保Chart.js已加载
  19. console.log(typeof Chart); // 应该输出'function'
复制代码

问题2:图表响应式问题

可能原因:

• 容器尺寸变化时图表未自适应
• 窗口大小变化时图表未更新

解决方案:
  1. const config = {
  2.     type: 'line',
  3.     data: data,
  4.     options: {
  5.         responsive: true, // 启用响应式
  6.         maintainAspectRatio: false, // 不保持纵横比
  7.         // 其他配置...
  8.     }
  9. };
  10. // 如果容器尺寸变化,手动调用resize方法
  11. window.addEventListener('resize', function() {
  12.     myChart.resize();
  13. });
复制代码

问题3:性能问题,图表更新卡顿

可能原因:

• 数据点过多
• 动画效果复杂
• 频繁更新图表

解决方案:
  1. const config = {
  2.     type: 'line',
  3.     data: data,
  4.     options: {
  5.         animation: {
  6.             duration: 0 // 禁用动画以提高性能
  7.         },
  8.         elements: {
  9.             point: {
  10.                 radius: 0 // 隐藏数据点以提高性能
  11.             }
  12.         },
  13.         // 其他配置...
  14.     }
  15. };
  16. // 限制数据点数量
  17. const maxDataPoints = 100;
  18. function addData() {
  19.     // 添加新数据...
  20.    
  21.     // 限制数据点数量
  22.     if (myChart.data.labels.length > maxDataPoints) {
  23.         myChart.data.labels.shift();
  24.         myChart.data.datasets.forEach(dataset => {
  25.             dataset.data.shift();
  26.         });
  27.     }
  28.    
  29.     myChart.update('none'); // 使用'none'模式更新,不使用动画
  30. }
复制代码

问题4:多轴配置问题

可能原因:

• 轴ID不匹配
• 轴配置错误

解决方案:
  1. const data = {
  2.     datasets: [
  3.         {
  4.             label: '数据集1',
  5.             data: [10, 20, 30],
  6.             yAxisID: 'y', // 关联到第一个Y轴
  7.         },
  8.         {
  9.             label: '数据集2',
  10.             data: [50, 60, 70],
  11.             yAxisID: 'y1', // 关联到第二个Y轴
  12.         }
  13.     ]
  14. };
  15. const config = {
  16.     type: 'line',
  17.     data: data,
  18.     options: {
  19.         scales: {
  20.             y: {
  21.                 type: 'linear',
  22.                 display: true,
  23.                 position: 'left',
  24.             },
  25.             y1: {
  26.                 type: 'linear',
  27.                 display: true,
  28.                 position: 'right',
  29.                 // 确保右侧Y轴不与左侧重叠
  30.                 grid: {
  31.                     drawOnChartArea: false,
  32.                 },
  33.             },
  34.         }
  35.     }
  36. };
复制代码

问题5:工具提示自定义问题

解决方案:
  1. const config = {
  2.     type: 'line',
  3.     data: data,
  4.     options: {
  5.         plugins: {
  6.             tooltip: {
  7.                 callbacks: {
  8.                     title: function(tooltipItems) {
  9.                         // 自定义标题
  10.                         return '时间: ' + tooltipItems[0].label;
  11.                     },
  12.                     label: function(context) {
  13.                         // 自定义标签
  14.                         let label = context.dataset.label || '';
  15.                         if (label) {
  16.                             label += ': ';
  17.                         }
  18.                         if (context.parsed.y !== null) {
  19.                             label += context.parsed.y + ' 单位';
  20.                         }
  21.                         return label;
  22.                     },
  23.                     afterLabel: function(context) {
  24.                         // 添加额外信息
  25.                         return '变化率: ' + (Math.random() * 10).toFixed(2) + '%';
  26.                     }
  27.                 }
  28.             }
  29.         }
  30.     }
  31. };
复制代码

总结与展望

Chart.js作为一个功能强大且易于使用的JavaScript图表库,为开发者提供了创建各种类型图表的便捷方式。本文全面介绍了Chart.js折线图的绘制方法,从基础入门到高级应用,包括:

1. 基础折线图创建:从零开始创建第一个折线图,理解数据结构和配置选项。
2. 折线图配置详解:深入探讨了标题、图例、坐标轴、工具提示等各个方面的配置方法。
3. 高级功能:介绍了多轴折线图、堆叠折线图、渐变填充折线图和阶梯折线图等高级功能。
4. 动态数据可视化:展示了如何创建实时更新的折线图,以及如何实现交互式数据控制。
5. 实战案例:通过一个完整的实时销售监控系统项目,综合应用了前面所学知识。

Chart.js的优势在于其简单易用的API、丰富的配置选项和良好的性能表现。它特别适合需要快速实现数据可视化的项目,无论是简单的静态图表还是复杂的动态数据可视化。

未来,Chart.js仍在不断发展,我们可以期待:

1. 更好的性能优化:随着数据量的增加,Chart.js将继续优化渲染性能,提供更流畅的用户体验。
2. 更多图表类型:可能会增加更多专业的图表类型,满足不同领域的需求。
3. 增强的交互功能:提供更多交互方式,如更丰富的缩放、平移、数据选择等功能。
4. 更好的移动端支持:随着移动设备的普及,Chart.js将进一步优化移动端的体验。
5. 与更多框架的集成:提供与React、Vue、Angular等前端框架更紧密的集成。

通过本文的学习,你应该已经掌握了使用Chart.js创建折线图的基本技能,并能够根据实际需求进行扩展和定制。希望这些知识能够帮助你在实际项目中创建出美观、实用的数据可视化效果。
「七転び八起き(ななころびやおき)」
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则