概述
本笔记将详细记录如何使用 ECharts 和 ECharts-GL 创建一个具有旋转控制功能的三维轨迹图表。
环境准备
1. 引入必要的库
首先需要引入 ECharts 核心库和 ECharts-GL 扩展库:
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts-gl@2.0.9/dist/echarts-gl.min.js"></script>2. HTML 结构
创建基本的 HTML 结构:
<div class="container">
<div id="chart" style="width: 100%; height: 600px;"></div>
<div class="controls">
<!-- 控制按钮 -->
</div>
</div>核心技术解析
1. 三维坐标系统
ECharts-GL 使用右手坐标系:
oX轴:水平方向(左右)oY轴:垂直方向(上下)oZ轴:深度方向(前后)
2. 视角控制参数
viewControl: {
alpha: 20, // 上下旋转角度
beta: 40, // 左右旋转角度
distance: 200, // 视角距离
autoRotate: true, // 自动旋转
autoRotateSpeed: 10 // 旋转速度
}3. 动画实现原理
使用 requestAnimationFrame 实现平滑动画:
function rotateChart() {
if (isRotating) {
currentAngle += rotationSpeed;
// 更新视角
chart.setOption({...});
}
animationId = requestAnimationFrame(rotateChart);
}代码实现详解
1. 初始化图表
// 创建 ECharts 实例
const chart = echarts.init(document.getElementById('chart'));
// 基础配置
const option = {
backgroundColor: '#000',
grid3D: {
boxWidth: 100,
boxHeight: 100,
boxDepth: 100,
environment: 'auto',
light: {
main: {
intensity: 1.2,
shadow: true
},
ambient: {
intensity: 0.3
}
}
}
};2. 数据生成算法
螺旋线轨迹生成
function generateSpiralData() {
const data = [];
for (let i = 0; i <= 1000; i++) {
const t = i * 0.01;
const x = Math.cos(t) * (10 + t * 0.1); // 螺旋半径递增
const y = Math.sin(t) * (10 + t * 0.1); // 螺旋半径递增
const z = t * 0.5; // 高度线性增长
data.push([x, y, z]);
}
return data;
}数学原理:
o使用参数方程 [x = r(t) \cos(t)],[y = r(t) \sin(t)],[z = h(t)]o[r(t) = 10 + t \times 0.1] 实现螺旋半径递增o[h(t) = t \times 0.5] 实现高度线性增长
随机散点生成
function generateScatterData() {
const data = [];
for (let i = 0; i < 200; i++) {
const x = (Math.random() - 0.5) * 50;
const y = (Math.random() - 0.5) * 50;
const z = (Math.random() - 0.5) * 50;
const value = Math.sqrt(x*x + y*y + z*z); // 计算到原点距离
data.push([x, y, z, value]);
}
return data;
}3. 旋转控制实现
核心旋转函数
let isRotating = true;
let rotationSpeed = 1;
let currentAngle = 0;
function rotateChart() {
if (isRotating) {
currentAngle += rotationSpeed;
chart.setOption({
grid3D: {
viewControl: {
alpha: 20 + Math.sin(currentAngle * 0.01) * 10, // 上下摆动
beta: currentAngle, // 水平旋转
autoRotate: false
}
}
});
}
animationId = requestAnimationFrame(rotateChart);
}事件处理
// 图表点击事件
chart.on('click', function(params) {
isRotating = !isRotating;
updateStatus();
});
// 按钮控制
document.getElementById('toggleBtn').addEventListener('click', function() {
isRotating = !isRotating;
updateStatus();
});4. 系列配置详解
线条系列(螺旋轨迹)
{
name: '螺旋轨迹',
type: 'line3D',
data: generateSpiralData(),
lineStyle: {
width: 4,
color: '#ff6b6b'
}
}散点系列
{
name: '散点数据',
type: 'scatter3D',
data: generateScatterData(),
symbolSize: 8,
itemStyle: {
color: function(params) {
const value = params.data[3];
const colors = ['#4ecdc4', '#45b7d1', '#96ceb4', '#feca57', '#ff9ff3'];
return colors[Math.floor(value / 10) % colors.length];
},
opacity: 0.8
}
}5. 状态管理
function updateStatus() {
document.getElementById('statusText').textContent =
isRotating ? '旋转中' : '已暂停';
document.getElementById('speedText').textContent = rotationSpeed + 'x';
document.getElementById('toggleBtn').textContent =
isRotating ? '暂停旋转' : '继续旋转';
}功能扩展
1. 添加轨迹点动画
function animateTrajectoryPoint() {
let pointIndex = 0;
const trajectoryData = generateSpiralData();
setInterval(() => {
if (pointIndex < trajectoryData.length) {
// 更新当前点位置
chart.setOption({
series: [{
// ... 其他配置
markPoint: {
data: [{
coord: trajectoryData[pointIndex],
symbol: 'circle',
symbolSize: 15,
itemStyle: { color: 'yellow' }
}]
}
}]
});
pointIndex++;
} else {
pointIndex = 0; // 循环播放
}
}, 50);
}2. 添加数据筛选功能
function filterDataByRange(data, minValue, maxValue) {
return data.filter(item => {
const value = item[3]; // 假设第4个值是筛选依据
return value >= minValue && value <= maxValue;
});
}
// 使用滑块控制数据筛选
document.getElementById('rangeSlider').addEventListener('input', function(e) {
const range = e.target.value;
const filteredData = filterDataByRange(originalData, 0, range);
chart.setOption({
series: [{
data: filteredData
}]
});
});3. 添加轨迹回放功能
class TrajectoryPlayer {
constructor(chart, data) {
this.chart = chart;
this.data = data;
this.currentIndex = 0;
this.isPlaying = false;
this.playSpeed = 100; // ms
}
play() {
this.isPlaying = true;
this.playInterval = setInterval(() => {
if (this.currentIndex < this.data.length) {
this.updateTrajectory();
this.currentIndex++;
} else {
this.stop();
}
}, this.playSpeed);
}
pause() {
this.isPlaying = false;
clearInterval(this.playInterval);
}
reset() {
this.currentIndex = 0;
this.updateTrajectory();
}
updateTrajectory() {
const currentData = this.data.slice(0, this.currentIndex + 1);
this.chart.setOption({
series: [{
data: currentData
}]
});
}
}性能优化
1. 数据量优化
// 对大数据集进行采样
function sampleData(data, sampleRate = 0.1) {
return data.filter((_, index) => index % Math.floor(1 / sampleRate) === 0);
}
// 使用 Web Worker 处理大量数据
const worker = new Worker('dataProcessor.js');
worker.postMessage({ data: largeDataSet });
worker.onmessage = function(e) {
const processedData = e.data;
chart.setOption({
series: [{ data: processedData }]
});
};2. 渲染优化
// 使用 ECharts 的渐进渲染
const option = {
progressive: 1000, // 渐进渲染阈值
progressiveThreshold: 3000, // 启用渐进渲染的数据量阈值
// ... 其他配置
};3. 内存管理
// 清理动画资源
function cleanup() {
if (animationId) {
cancelAnimationFrame(animationId);
}
if (chart) {
chart.dispose();
}
}
// 页面卸载时清理
window.addEventListener('beforeunload', cleanup);常见问题与解决方案
1. 图表不显示
问题:图表容器没有内容显示
解决方案:
// 确保容器有明确的宽高
#chart {
width: 800px;
height: 600px;
}
// 确保在 DOM 加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
const chart = echarts.init(document.getElementById('chart'));
// ... 其他代码
});2. 旋转不流畅
问题:旋转动画卡顿或不连续
解决方案:
// 使用 requestAnimationFrame 替代 setInterval
function smoothRotate() {
if (isRotating) {
currentAngle += rotationSpeed * 0.5; // 调整步长
updateViewAngle();
}
requestAnimationFrame(smoothRotate);
}
// 优化更新频率
let lastUpdateTime = 0;
function throttledUpdate(timestamp) {
if (timestamp - lastUpdateTime > 16) { // 约60fps
updateChart();
lastUpdateTime = timestamp;
}
requestAnimationFrame(throttledUpdate);
}3. 内存泄漏
问题:长时间运行后页面变慢
解决方案:
// 正确清理事件监听器
function removeEventListeners() {
chart.off('click');
window.removeEventListener('resize', resizeHandler);
}
// 使用 WeakMap 存储临时数据
const chartData = new WeakMap();
chartData.set(chart, { rotationState: isRotating });4. 响应式适配
问题:在不同屏幕尺寸下显示异常
解决方案:
// 响应式调整
function resizeChart() {
if (chart) {
chart.resize();
// 根据屏幕尺寸调整配置
const isMobile = window.innerWidth < 768;
chart.setOption({
grid3D: {
boxWidth: isMobile ? 50 : 100,
boxHeight: isMobile ? 50 : 100,
boxDepth: isMobile ? 50 : 100
}
});
}
}
window.addEventListener('resize', debounce(resizeChart, 300));
// 防抖函数
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}总结
1.基础知识:ECharts-GL 的三维图表基础概念和配置2.数据处理:如何生成和处理三维轨迹数据3.动画控制:实现流畅的旋转动画和交互控制4.性能优化:处理大数据量和提升渲染性能的方法5.问题解决:常见问题的诊断和解决方案
综上代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ECharts 三维轨迹旋转控制演示</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts-gl@2.0.9/dist/echarts-gl.min.js"></script>
<style>
body {
margin: 0;
padding: 20px;
font-family: Arial, sans-serif;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
padding: 20px;
}
.title {
text-align: center;
color: #333;
margin-bottom: 20px;
}
.chart-container {
width: 100%;
height: 600px;
border: 1px solid #ddd;
border-radius: 4px;
position: relative;
}
.controls {
margin-top: 20px;
text-align: center;
}
.btn {
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
margin: 0 10px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.btn:hover {
background: #0056b3;
}
.btn.pause {
background: #dc3545;
}
.btn.pause:hover {
background: #c82333;
}
.status {
margin-top: 10px;
font-size: 14px;
color: #666;
}
.info {
margin-top: 20px;
padding: 15px;
background: #e9ecef;
border-radius: 4px;
font-size: 14px;
line-height: 1.6;
}
</style>
</head>
<body>
<div class="container">
<h1 class="title">ECharts 三维轨迹旋转控制演示</h1>
<div class="chart-container">
<div id="chart" style="width: 100%; height: 100%;"></div>
</div>
<div class="controls">
<button id="toggleBtn" class="btn">暂停旋转</button>
<button id="resetBtn" class="btn">重置视角</button>
<button id="speedUpBtn" class="btn">加速</button>
<button id="speedDownBtn" class="btn">减速</button>
</div>
<div class="status">
<div>状态: <span id="statusText">旋转中</span></div>
<div>旋转速度: <span id="speedText">1x</span></div>
</div>
<div class="info">
<strong>操作说明:</strong><br>
o 点击图表任意位置可暂停/继续旋转<br>
o 使用鼠标拖拽可手动调整视角<br>
o 滚轮可缩放图表<br>
o 按钮可控制旋转状态和速度
</div>
</div>
<script>
// 初始化 ECharts 实例
const chart = echarts.init(document.getElementById('chart'));
// 旋转控制变量
let isRotating = true;
let rotationSpeed = 1;
let currentAngle = 0;
let animationId;
// 生成三维轨迹数据(螺旋线)
function generateSpiralData() {
const data = [];
for (let i = 0; i <= 1000; i++) {
const t = i * 0.01;
const x = Math.cos(t) * (10 + t * 0.1);
const y = Math.sin(t) * (10 + t * 0.1);
const z = t * 0.5;
data.push([x, y, z]);
}
return data;
}
// 生成随机散点数据
function generateScatterData() {
const data = [];
for (let i = 0; i < 200; i++) {
const x = (Math.random() - 0.5) * 50;
const y = (Math.random() - 0.5) * 50;
const z = (Math.random() - 0.5) * 50;
const value = Math.sqrt(x*x + y*y + z*z);
data.push([x, y, z, value]);
}
return data;
}
// 图表配置
const option = {
backgroundColor: '#000',
grid3D: {
boxWidth: 100,
boxHeight: 100,
boxDepth: 100,
environment: 'auto',
light: {
main: {
intensity: 1.2,
shadow: true
},
ambient: {
intensity: 0.3
}
},
viewControl: {
alpha: 20,
beta: 40,
autoRotate: true,
autoRotateSpeed: 10
}
},
xAxis3D: {
type: 'value',
name: 'X轴',
nameTextStyle: { color: '#fff' },
axisLabel: { color: '#fff' }
},
yAxis3D: {
type: 'value',
name: 'Y轴',
nameTextStyle: { color: '#fff' },
axisLabel: { color: '#fff' }
},
zAxis3D: {
type: 'value',
name: 'Z轴',
nameTextStyle: { color: '#fff' },
axisLabel: { color: '#fff' }
},
series: [
{
name: '螺旋轨迹',
type: 'line3D',
data: generateSpiralData(),
lineStyle: {
width: 4,
color: '#ff6b6b'
}
},
{
name: '散点数据',
type: 'scatter3D',
data: generateScatterData(),
symbolSize: 8,
itemStyle: {
color: function(params) {
const value = params.data[3];
const colors = ['#4ecdc4', '#45b7d1', '#96ceb4', '#feca57', '#ff9ff3'];
return colors[Math.floor(value / 10) % colors.length];
},
opacity: 0.8
}
}
]
};
// 设置图表配置
chart.setOption(option);
// 旋转函数
function rotateChart() {
if (isRotating) {
currentAngle += rotationSpeed;
chart.setOption({
grid3D: {
viewControl: {
alpha: 20 + Math.sin(currentAngle * 0.01) * 10,
beta: currentAngle,
autoRotate: false
}
}
});
}
animationId = requestAnimationFrame(rotateChart);
}
// 开始旋转
rotateChart();
// 更新状态显示
function updateStatus() {
document.getElementById('statusText').textContent = isRotating ? '旋转中' : '已暂停';
document.getElementById('speedText').textContent = rotationSpeed + 'x';
document.getElementById('toggleBtn').textContent = isRotating ? '暂停旋转' : '继续旋转';
document.getElementById('toggleBtn').className = isRotating ? 'btn pause' : 'btn';
}
// 图表点击事件
chart.on('click', function(params) {
isRotating = !isRotating;
updateStatus();
});
// 按钮事件
document.getElementById('toggleBtn').addEventListener('click', function() {
isRotating = !isRotating;
updateStatus();
});
document.getElementById('resetBtn').addEventListener('click', function() {
currentAngle = 0;
chart.setOption({
grid3D: {
viewControl: {
alpha: 20,
beta: 40,
autoRotate: false
}
}
});
});
document.getElementById('speedUpBtn').addEventListener('click', function() {
rotationSpeed = Math.min(rotationSpeed + 0.5, 5);
updateStatus();
});
document.getElementById('speedDownBtn').addEventListener('click', function() {
rotationSpeed = Math.max(rotationSpeed - 0.5, 0.5);
updateStatus();
});
// 窗口大小改变时重新调整图表
window.addEventListener('resize', function() {
chart.resize();
});
// 初始化状态显示
updateStatus();
</script>
</body>
</html>