该 HTML 文件是一款纯前端交互式历史数据曲线图工具,基于 Canvas API 实现数据可视化,支持切换时间范围(7 天 / 30 天 / 90 天)、生成随机波动数据、鼠标悬浮查看详情,同时显示关键指标(当前值、最高值、最低值),视觉上采用渐变背景与玻璃拟物风格,兼顾美观与实用性。
大家复制代码时,可能会因格式转换出现错乱,导致样式失效。建议先少量复制代码进行测试,若未能解决问题,私信回复源码两字,我会发送完整的压缩包给你。
演示效果
HTML&CSS
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>历史曲线图表</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, #1a2a6c, #2a3c7f);
color: #fff;
padding: 20px;
}
.container {
max-width: 1000px;
width: 100%;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 30px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2);
}
header {
text-align: center;
margin-bottom: 30px;
}
h1 {
font-size: 2.5rem;
margin-bottom: 10px;
background: linear-gradient(to right, #00c6ff, #0072ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
color: #a0a0ff;
font-size: 1.1rem;
}
.chart-container {
position: relative;
width: 100%;
height: 400px;
margin-bottom: 20px;
}
canvas {
border-radius: 10px;
background: rgba(0, 0, 0, 0.2);
}
.chart-info {
display: flex;
justify-content: space-between;
margin-top: 20px;
padding: 15px;
background: rgba(0, 0, 0, 0.2);
border-radius: 10px;
}
.data-point {
display: flex;
align-items: center;
gap: 10px;
}
.color-indicator {
width: 15px;
height: 15px;
border-radius: 50%;
background: linear-gradient(to right, #00c6ff, #0072ff);
}
.controls {
display: flex;
gap: 15px;
margin-top: 20px;
}
button {
flex: 1;
padding: 12px;
border: none;
border-radius: 8px;
background: linear-gradient(to right, #00c6ff, #0072ff);
color: white;
font-weight: bold;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 114, 255, 0.4);
}
.tooltip {
position: absolute;
padding: 8px 12px;
background: rgba(0, 0, 0, 0.8);
border-radius: 6px;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s;
font-size: 0.9rem;
z-index: 10;
}
@media (max-width: 768px) {
.chart-info, .controls {
flex-direction: column;
}
h1 {
font-size: 2rem;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>历史数据曲线图</h1>
<p class="subtitle">展示过去30天的数据变化趋势</p>
</header>
<div class="chart-container">
<canvas id="historyChart"></canvas>
<div class="tooltip" id="tooltip"></div>
</div>
<div class="chart-info">
<div class="data-point">
<div class="color-indicator"></div>
<div>
<div>当前值</div>
<div id="currentValue">125.6</div>
</div>
</div>
<div class="data-point">
<div class="color-indicator" style="background: #ff6b6b;"></div>
<div>
<div>最高值</div>
<div id="maxValue">142.3</div>
</div>
</div>
<div class="data-point">
<div class="color-indicator" style="background: #4cd964;"></div>
<div>
<div>最低值</div>
<div id="minValue">98.7</div>
</div>
</div>
</div>
<div class="controls">
<button id="btnWeek">最近7天</button>
<button id="btnMonth">最近30天</button>
<button id="btnQuarter">最近90天</button>
<button id="btnRandom">生成随机数据</button>
</div>
</div>
<script>
// 获取Canvas元素和上下文
const canvas = document.getElementById('historyChart');
const ctx = canvas.getContext('2d');
const tooltip = document.getElementById('tooltip');
// 设置Canvas尺寸
function resizeCanvas() {
const container = canvas.parentElement;
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
drawChart();
}
// 初始数据
let dataPoints = generateRandomData(30, 80, 150);
let daysToShow = 30;
// 生成随机数据
function generateRandomData(count, min, max) {
const data = [];
let lastValue = (min + max) / 2;
for (let i = 0; i < count; i++) {
// 随机波动,但保持在合理范围内
const change = (Math.random() - 0.5) * 20;
lastValue = Math.max(min, Math.min(max, lastValue + change));
data.push(lastValue);
}
return data;
}
// 绘制图表
function drawChart() {
const width = canvas.width;
const height = canvas.height;
const padding = 50;
// 清除画布
ctx.clearRect(0, 0, width, height);
// 绘制网格
drawGrid(width, height, padding);
// 绘制坐标轴
drawAxes(width, height, padding);
// 绘制曲线
drawCurve(width, height, padding);
// 更新信息显示
updateInfo();
}
// 绘制网格
function drawGrid(width, height, padding) {
ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
ctx.lineWidth = 1;
// 水平网格线
const horizontalLines = 5;
for (let i = 0; i <= horizontalLines; i++) {
const y = padding + (height - 2 * padding) * (i / horizontalLines);
ctx.beginPath();
ctx.moveTo(padding, y);
ctx.lineTo(width - padding, y);
ctx.stroke();
// 添加Y轴标签
const value = getMaxValue() - (getMaxValue() - getMinValue()) * (i / horizontalLines);
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
ctx.font = '12px Arial';
ctx.textAlign = 'right';
ctx.fillText(value.toFixed(1), padding - 10, y + 4);
}
// 垂直网格线
const dataToShow = dataPoints.slice(-daysToShow);
const verticalLines = Math.min(daysToShow, 10);
for (let i = 0; i <= verticalLines; i++) {
const x = padding + (width - 2 * padding) * (i / verticalLines);
ctx.beginPath();
ctx.moveTo(x, padding);
ctx.lineTo(x, height - padding);
ctx.stroke();
// 添加X轴标签
const dayIndex = Math.floor((dataToShow.length - 1) * (i / verticalLines));
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.fillText(`第 ${dayIndex + 1}天`, x, height - padding + 20);
}
}
// 绘制坐标轴
function drawAxes(width, height, padding) {
ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
ctx.lineWidth = 2;
// X轴
ctx.beginPath();
ctx.moveTo(padding, height - padding);
ctx.lineTo(width - padding, height - padding);
ctx.stroke();
// Y轴
ctx.beginPath();
ctx.moveTo(padding, padding);
ctx.lineTo(padding, height - padding);
ctx.stroke();
}
// 绘制曲线
function drawCurve(width, height, padding) {
const dataToShow = dataPoints.slice(-daysToShow);
const maxValue = getMaxValue();
const minValue = getMinValue();
const valueRange = maxValue - minValue;
// 创建渐变
const gradient = ctx.createLinearGradient(0, padding, 0, height - padding);
gradient.addColorStop(0, 'rgba(0, 198, 255, 0.8)');
gradient.addColorStop(1, 'rgba(0, 114, 255, 0.3)');
ctx.strokeStyle = gradient;
ctx.fillStyle = gradient;
ctx.lineWidth = 3;
// 绘制曲线
ctx.beginPath();
for (let i = 0; i < dataToShow.length; i++) {
const x = padding + (width - 2 * padding) * (i / (dataToShow.length - 1));
const y = height - padding - ((dataToShow[i] - minValue) / valueRange) * (height - 2 * padding);
if (i === 0) {
ctx.moveTo(x, y);
} else {
// 使用二次贝塞尔曲线平滑连接点
const prevX = padding + (width - 2 * padding) * ((i - 1) / (dataToShow.length - 1));
const prevY = height - padding - ((dataToShow[i - 1] - minValue) / valueRange) * (height - 2 * padding);
const cpX = (prevX + x) / 2;
const cpY1 = prevY;
const cpY2 = y;
ctx.bezierCurveTo(cpX, cpY1, cpX, cpY2, x, y);
}
}
ctx.stroke();
// 填充曲线下方区域
ctx.lineTo(width - padding, height - padding);
ctx.lineTo(padding, height - padding);
ctx.closePath();
ctx.fill();
// 绘制数据点
ctx.fillStyle = '#ffffff';
for (let i = 0; i < dataToShow.length; i++) {
const x = padding + (width - 2 * padding) * (i / (dataToShow.length - 1));
const y = height - padding - ((dataToShow[i] - minValue) / valueRange) * (height - 2 * padding);
ctx.beginPath();
ctx.arc(x, y, 4, 0, Math.PI * 2);
ctx.fill();
}
}
// 获取数据最大值
function getMaxValue() {
const dataToShow = dataPoints.slice(-daysToShow);
return Math.max(...dataToShow) * 1.05; // 增加5%的顶部空间
}
// 获取数据最小值
function getMinValue() {
const dataToShow = dataPoints.slice(-daysToShow);
return Math.min(...dataToShow) * 0.95; // 增加5%的底部空间
}
// 更新信息显示
function updateInfo() {
const dataToShow = dataPoints.slice(-daysToShow);
const currentValue = dataToShow[dataToShow.length - 1];
const maxValue = Math.max(...dataToShow);
const minValue = Math.min(...dataToShow);
document.getElementById('currentValue').textContent = currentValue.toFixed(1);
document.getElementById('maxValue').textContent = maxValue.toFixed(1);
document.getElementById('minValue').textContent = minValue.toFixed(1);
}
// 处理鼠标移动事件
function handleMouseMove(e) {
const rect = canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
const width = canvas.width;
const height = canvas.height;
const padding = 50;
const dataToShow = dataPoints.slice(-daysToShow);
const maxValue = getMaxValue();
const minValue = getMinValue();
const valueRange = maxValue - minValue;
// 检查鼠标是否在图表区域内
if (mouseX >= padding && mouseX <= width - padding &&
mouseY >= padding && mouseY <= height - padding) {
// 计算最近的数据点
const dataIndex = Math.round(((mouseX - padding) / (width - 2 * padding)) * (dataToShow.length - 1));
const value = dataToShow[dataIndex];
const x = padding + (width - 2 * padding) * (dataIndex / (dataToShow.length - 1));
const y = height - padding - ((value - minValue) / valueRange) * (height - 2 * padding);
// 显示工具提示
tooltip.style.opacity = '1';
tooltip.style.left = `${x + 10}px`;
tooltip.style.top = `${y - 10}px`;
tooltip.innerHTML = `第 ${dataIndex + 1}天<br>数值: ${value.toFixed(1)}`;
// 高亮当前数据点
ctx.clearRect(0, 0, width, height);
drawChart();
ctx.fillStyle = '#ffeb3b';
ctx.beginPath();
ctx.arc(x, y, 6, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#ffeb3b';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(x, padding);
ctx.lineTo(x, height - padding);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(padding, y);
ctx.lineTo(width - padding, y);
ctx.stroke();
} else {
tooltip.style.opacity = '0';
}
}
// 初始化
window.addEventListener('resize', resizeCanvas);
canvas.addEventListener('mousemove', handleMouseMove);
// 按钮事件
document.getElementById('btnWeek').addEventListener('click', () => {
daysToShow = 7;
drawChart();
});
document.getElementById('btnMonth').addEventListener('click', () => {
daysToShow = 30;
drawChart();
});
document.getElementById('btnQuarter').addEventListener('click', () => {
daysToShow = 90;
dataPoints = generateRandomData(90, 80, 150);
drawChart();
});
document.getElementById('btnRandom').addEventListener('click', () => {
dataPoints = generateRandomData(daysToShow, 80, 150);
drawChart();
});
// 初始绘制
resizeCanvas();
</script>
</body>
</html>
HTML
- container:主功能容器:尺寸:最大宽度 1000px,100% 自适应宽度;视觉:玻璃拟物风格(background: rgba(255,255,255,0.1)+backdrop-filter: blur(10px))、圆角 20px、阴影(0 15px 35px rgba(0,0,0,0.2)),与背景形成层次;内边距:padding: 30px,隔离内部组件。
- header:头部信息区:显示图表标题与副标题,明确工具用途(“历史数据曲线图”“过去 30 天数据趋势”)
- h1:图表主标题:文字渐变效果(linear-gradient(to right, #00c6ff, #0072ff)),增强视觉吸引力,突出主题
- subtitle:副标题:浅紫灰色(#a0a0ff),补充说明图表内容,降低用户理解成本
- chart-container:图表核心容器: 定位:position: relative,为绝对定位的 Canvas 和 tooltip 提供基准;尺寸:宽 100%、高 400px,确保图表有足够展示空间; 包含 Canvas(图表载体)和 tooltip(鼠标悬浮提示)。
- canvas id="historyChart:图表绘制载体:基于 Canvas API 实现网格、坐标轴、曲线、数据点的绘制,是工具的技术核心
- tooltip:鼠标悬浮提示:默认隐藏(opacity: 0),鼠标移动到数据点时显示,包含 “天数 + 数值” 信息,提升交互体验
- chart-info:关键指标区:布局:flex 横向排列,显示 “当前值”“最高值”“最低值” 三个核心指标;视觉:浅黑背景(rgba(0,0,0,0.2))、圆角 10px、内边距 15px,与图表区区分,突出数据重点。
- data-point:指标项: 布局:flex 对齐,包含 “颜色指示器” 和 “指标文本”;颜色区分:当前值(蓝渐变)、最高值(红#ff6b6b)、最低值(绿#4cd964),通过颜色暗示指标属性(红 = 高、绿 = 低)。
- color-indicator:颜色指示器:15×15px 圆形,通过背景色区分指标类型,视觉上快速关联图表曲线
- controls:控制按钮容器:flex 横向排列四个功能按钮,gap: 15px 确保间距,方便用户切换数据范围
- button:功能按钮: 布局:flex: 1 平均分配宽度,适配不同屏幕;视觉:蓝渐变背景、白色文字、加粗字体、圆角 8px;功能:分别对应 “最近 7 天”“最近 30 天”“最近 90 天”“生成随机数据”,实现数据切换与重置。
CSS
- .container 主容器:玻璃拟物风格(backdrop-filter: blur(10px))+ 阴影,增强层次感;圆角 20px,柔化边缘,避免生硬。
- h1 标题文字: 渐变:background: linear-gradient(to right, #00c6ff, #0072ff); 文字裁剪:-webkit-background-clip: text+-webkit-text-fill-color: transparent,实现文字渐变效果,视觉突出。
- .subtitle 副标题:浅紫灰(#a0a0ff)、1.1rem 字体,作为标题的辅助说明,视觉上不抢焦点。
- .chart-container 图表容器:position: relative,确保 Canvas 和 tooltip 定位正确;高度 400px,为图表提供足够纵向空间。
- canvas 画布:圆角 10px、浅黑背景(rgba(0,0,0,0.2)),与主容器背景区分,突出图表内容。
- .tooltip 悬浮提示:定位:absolute,pointer-events: none(避免遮挡鼠标事件);视觉:深黑背景(rgba(0,0,0,0.8))、圆角 6px、内边距 8px 12px、白色文字、0.9rem 字体;过渡:transition: opacity 0.3s,显示 / 隐藏时平滑过渡,避免突兀。
- .chart-info 指标区:浅黑背景(rgba(0,0,0,0.2))、圆角 10px、内边距 15px,flex 布局使三个指标横向均匀分布,信息清晰。
- .data-point 指标项:display: flex+align-items: center+gap: 10px,颜色指示器与文字对齐,视觉连贯;文字区上下分布(指标名 + 数值),层级分明。
- .color-indicator 颜色指示器:15×15px 圆形(border-radius: 50%),通过背景色区分指标(当前值蓝渐变、最高值红、最低值绿),快速建立视觉关联。
- .controls 按钮容器:display: flex+gap: 15px,按钮横向排列且间距均匀;margin-top: 20px,与指标区分隔,布局整齐。
- button 功能按钮:基础样式:padding: 12px、无边框、圆角 8px、蓝渐变背景(linear-gradient(to right, #00c6ff, #0072ff))、白色加粗文字;交互:transition: transform 0.2s, box-shadow 0.2s,hover 时上移 2px(translateY(-2px))+ 蓝阴影(0 5px 15px rgba(0,114,255,0.4)),模拟 “浮起” 效果,反馈明确。
JavaScript
JS 核心目标是“实现数据可视化与交互”,通过Canvas 绘制+数据生成+事件监听,覆盖“图表渲染-数据切换-交互反馈”全流程,逻辑清晰且性能优化到位:
1. 初始化与核心变量
// 1. 获取Canvas与上下文(启用频繁读取优化)
const canvas = document.getElementById('historyChart');
const ctx = canvas.getContext('2d');
const tooltip = document.getElementById('tooltip');
// 2. 状态变量:数据与显示配置
let dataPoints = generateRandomData(30, 80, 150); // 初始30天随机数据(80-150范围)
let daysToShow = 30; // 默认显示30天数据
2. Canvas 尺寸适配(resizeCanvas)
确保 Canvas 尺寸随容器变化,避免窗口缩放导致图表变形:
function resizeCanvas() {
const container = canvas.parentElement;
canvas.width = container.clientWidth; // 宽度适配容器
canvas.height = container.clientHeight; // 高度固定(400px)
drawChart(); // 尺寸变化后重新绘制图表
}
// 窗口resize时触发适配,初始加载时执行一次
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
3. 随机数据生成(generateRandomData)
生成有“波动合理性”的随机数据(非完全随机),模拟真实数据趋势:
function generateRandomData(count, min, max) {
const data = [];
let lastValue = (min + max) / 2; // 初始值设为范围中间值,避免极端值
for (let i = 0; i < count; i++) {
const change = (Math.random() - 0.5) * 20; // 随机波动(-10~+10)
// 限制值在[min, max]范围内,避免数据异常
lastValue = Math.max(min, Math.min(max, lastValue + change));
data.push(lastValue);
}
return data;
}
4. 图表核心绘制(drawChart)
统筹图表绘制流程,调用网格、坐标轴、曲线的绘制函数,是视觉呈现的核心:
function drawChart() {
const width = canvas.width;
const height = canvas.height;
const padding = 50; // 图表内边距(预留轴标签空间)
ctx.clearRect(0, 0, width, height); // 清除画布,避免重绘重叠
drawGrid(width, height, padding); // 绘制网格与轴标签
drawAxes(width, height, padding); // 绘制坐标轴
drawCurve(width, height, padding); // 绘制曲线与数据点
updateInfo(); // 更新关键指标(当前/最高/最低值)
}
(1)绘制网格与轴标签(drawGrid)
function drawGrid(width, height, padding) {
ctx.strokeStyle = 'rgba(255,255,255,0.1)'; // 浅白网格线(半透明)
ctx.lineWidth = 1;
// 1. 水平网格线(Y轴方向,显示数值标签)
const horizontalLines = 5; // 5条水平线(含上下边界)
for (let i = 0; i <= horizontalLines; i++) {
const y = padding + (height - 2 * padding) * (i / horizontalLines); // 计算Y坐标
// 绘制水平线
ctx.beginPath();
ctx.moveTo(padding, y);
ctx.lineTo(width - padding, y);
ctx.stroke();
// 绘制Y轴数值标签(从高到低)
const value = getMaxValue() - (getMaxValue() - getMinValue()) * (i / horizontalLines);
ctx.fillStyle = 'rgba(255,255,255,0.7)';
ctx.font = '12px Arial';
ctx.textAlign = 'right';
ctx.fillText(value.toFixed(1), padding - 10, y + 4); // 右对齐,避免遮挡
}
// 2. 垂直网格线(X轴方向,显示天数标签)
const dataToShow = dataPoints.slice(-daysToShow); // 截取当前要显示的数据
const verticalLines = Math.min(daysToShow, 10); // 最多10条垂直线,避免拥挤
for (let i = 0; i <= verticalLines; i++) {
const x = padding + (width - 2 * padding) * (i / verticalLines); // 计算X坐标
// 绘制垂直线
ctx.beginPath();
ctx.moveTo(x, padding);
ctx.lineTo(x, height - padding);
ctx.stroke();
// 绘制X轴天数标签
const dayIndex = Math.floor((dataToShow.length - 1) * (i / verticalLines));
ctx.fillStyle = 'rgba(255,255,255,0.7)';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.fillText(`第 ${dayIndex + 1}天`, x, height - padding + 20); // 位于轴下方
}
}
(2)绘制坐标轴(drawAxes)
function drawAxes(width, height, padding) {
ctx.strokeStyle = 'rgba(255,255,255,0.5)'; // 半透明白色坐标轴(比网格线粗)
ctx.lineWidth = 2;
// X轴(水平轴,底部)
ctx.beginPath();
ctx.moveTo(padding, height - padding);
ctx.lineTo(width - padding, height - padding);
ctx.stroke();
// Y轴(垂直轴,左侧)
ctx.beginPath();
ctx.moveTo(padding, padding);
ctx.lineTo(padding, height - padding);
ctx.stroke();
}
(3)绘制曲线与数据点(drawCurve)
function drawCurve(width, height, padding) {
const dataToShow = dataPoints.slice(-daysToShow);
const maxValue = getMaxValue();
const minValue = getMinValue();
const valueRange = maxValue - minValue; // 数据范围(用于映射到Canvas高度)
// 1. 创建曲线渐变(从深蓝到浅蓝)
const gradient = ctx.createLinearGradient(0, padding, 0, height - padding);
gradient.addColorStop(0, 'rgba(0, 198, 255, 0.8)'); // 顶部深
gradient.addColorStop(1, 'rgba(0, 114, 255, 0.3)'); // 底部浅
// 2. 绘制平滑曲线(二次贝塞尔曲线)
ctx.strokeStyle = gradient;
ctx.fillStyle = gradient;
ctx.lineWidth = 3;
ctx.beginPath();
for (let i = 0; i < dataToShow.length; i++) {
// 计算当前点的Canvas坐标(数据值映射到画布像素)
const x = padding + (width - 2 * padding) * (i / (dataToShow.length - 1));
const y = height - padding - ((dataToShow[i] - minValue) / valueRange) * (height - 2 * padding);
if (i === 0) {
ctx.moveTo(x, y); // 第一个点:移动到起点
} else {
// 后续点:用二次贝塞尔曲线连接,实现平滑效果
const prevX = padding + (width - 2 * padding) * ((i - 1) / (dataToShow.length - 1));
const prevY = height - padding - ((dataToShow[i - 1] - minValue) / valueRange) * (height - 2 * padding);
const cpX = (prevX + x) / 2; // 控制点X(两点中间)
const cpY1 = prevY; // 控制点Y1(前一点Y)
const cpY2 = y; // 控制点Y2(当前点Y)
ctx.bezierCurveTo(cpX, cpY1, cpX, cpY2, x, y);
}
}
ctx.stroke(); // 绘制曲线
// 3. 填充曲线下方区域(增强视觉层次)
ctx.lineTo(width - padding, height - padding); // 右下角
ctx.lineTo(padding, height - padding); // 左下角
ctx.closePath();
ctx.fill(); // 填充渐变
// 4. 绘制数据点(白色圆点,突出数据位置)
ctx.fillStyle = '#ffffff';
for (let i = 0; i < dataToShow.length; i++) {
const x = padding + (width - 2 * padding) * (i / (dataToShow.length - 1));
const y = height - padding - ((dataToShow[i] - minValue) / valueRange) * (height - 2 * padding);
ctx.beginPath();
ctx.arc(x, y, 4, 0, Math.PI * 2); // 4px半径圆形
ctx.fill();
}
}
5. 交互逻辑(鼠标移动+按钮切换)
(1)鼠标移动提示(handleMouseMove)
function handleMouseMove(e) {
const rect = canvas.getBoundingClientRect(); // 获取Canvas在页面中的位置
const mouseX = e.clientX - rect.left; // 鼠标在Canvas内的X坐标
const mouseY = e.clientY - rect.top; // 鼠标在Canvas内的Y坐标
const width = canvas.width;
const height = canvas.height;
const padding = 50;
const dataToShow = dataPoints.slice(-daysToShow);
const maxValue = getMaxValue();
const minValue = getMinValue();
// 检查鼠标是否在图表有效区域内(排除边距)
if (mouseX >= padding && mouseX <= width - padding && mouseY >= padding && mouseY <= height - padding) {
// 计算最近的数据点索引
const dataIndex = Math.round(((mouseX - padding) / (width - 2 * padding)) * (dataToShow.length - 1));
const value = dataToShow[dataIndex];
// 计算该数据点的Canvas坐标
const x = padding + (width - 2 * padding) * (dataIndex / (dataToShow.length - 1));
const y = height - padding - ((value - minValue) / (maxValue - minValue)) * (height - 2 * padding);
// 1. 显示tooltip(天数+数值)
tooltip.style.opacity = '1';
tooltip.style.left = `${x + 10}px`; // 右移10px,避免遮挡
tooltip.style.top = `${y - 10}px`; // 上移10px,避免遮挡
tooltip.innerHTML = `第 ${dataIndex + 1}天<br>数值: ${value.toFixed(1)}`;
// 2. 高亮数据点(黄色圆点+十字线)
ctx.clearRect(0, 0, width, height); // 清除画布
drawChart(); // 重新绘制基础图表
// 绘制高亮圆点(黄色,6px半径)
ctx.fillStyle = '#ffeb3b';
ctx.beginPath();
ctx.arc(x, y, 6, 0, Math.PI * 2);
ctx.fill();
// 绘制十字线(黄色,贯穿轴)
ctx.strokeStyle = '#ffeb3b';
ctx.lineWidth = 1;
// 竖线(垂直贯穿)
ctx.beginPath();
ctx.moveTo(x, padding);
ctx.lineTo(x, height - padding);
ctx.stroke();
// 横线(水平贯穿)
ctx.beginPath();
ctx.moveTo(padding, y);
ctx.lineTo(width - padding, y);
ctx.stroke();
} else {
// 鼠标离开有效区域,隐藏tooltip
tooltip.style.opacity = '0';
}
}
// 绑定鼠标移动事件
canvas.addEventListener('mousemove', handleMouseMove);
(2)按钮切换数据范围
// 最近7天
document.getElementById('btnWeek').addEventListener('click', () => {
daysToShow = 7;
drawChart(); // 重新绘制图表
});
// 最近30天
document.getElementById('btnMonth').addEventListener('click', () => {
daysToShow = 30;
drawChart();
});
// 最近90天(需生成90天数据)
document.getElementById('btnQuarter').addEventListener('click', () => {
daysToShow = 90;
dataPoints = generateRandomData(90, 80, 150); // 生成90天数据
drawChart();
});
// 生成随机数据(重置当前时间范围的数据)
document.getElementById('btnRandom').addEventListener('click', () => {
dataPoints = generateRandomData(daysToShow, 80, 150);
drawChart();
});
6. 辅助函数
// 获取当前显示数据的最大值(增加5%顶部空间,避免曲线顶到边界)
function getMaxValue() {
const dataToShow = dataPoints.slice(-daysToShow);
return Math.max(...dataToShow) * 1.05;
}
// 获取当前显示数据的最小值(增加5%底部空间,避免曲线贴到底部)
function getMinValue() {
const dataToShow = dataPoints.slice(-daysToShow);
return Math.min(...dataToShow) * 0.95;
}
// 更新关键指标显示(当前值、最高值、最低值)
function updateInfo() {
const dataToShow = dataPoints.slice(-daysToShow);
const currentValue = dataToShow[dataToShow.length - 1]; // 最后一个数据点(当前值)
const maxValue = Math.max(...dataToShow);
const minValue = Math.min(...dataToShow);
// 更新DOM文本
document.getElementById('currentValue').textContent = currentValue.toFixed(1);
document.getElementById('maxValue').textContent = maxValue.toFixed(1);
document.getElementById('minValue').textContent = minValue.toFixed(1);
}
各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!