Cesium 中保留 ECharts 交互性的解决方案
在 Cesium 中加载 ECharts 饼图时,保持其原生交互性是一个挑战,因为传统的纹理映射方法只保留了视觉效果。下面我将提供一个完整的解决方案,在 Cesium 场景中保留 ECharts 的全部交互特性。
解决方案:DOM 叠加与坐标转换
这种方案的核心思想是:
- 在 Cesium 画布上方叠加一个透明的 ECharts 容器
- 将三维坐标转换为屏幕坐标来定位图表
- 通过事件代理将 Cesium 的交互事件传递给 ECharts
可视化效果:
代码如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cesium 交互式 ECharts 饼图</title>
<!-- 引入 Cesium 资源 -->
<link href="https://cesium.com/downloads/cesiumjs/releases/1.130/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
<script src="https://cesium.com/downloads/cesiumjs/releases/1.130/Build/Cesium/Cesium.js"></script>
<!-- 引入 ECharts -->
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.2/dist/echarts.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Microsoft YaHei', sans-serif;
}
body {
overflow: hidden;
background: linear-gradient(135deg, #1a2a6c, #2c3e50);
color: #fff;
height: 100vh;
}
#cesiumContainer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
#chartContainer {
position: absolute;
z-index: 100;
pointer-events: auto;
background: rgba(0, 0, 0, 0.7);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.7);
border: 1px solid rgba(255, 255, 255, 0.2);
transition: transform 0.3s ease;
}
#chartContainer:hover {
transform: scale(1.05);
}
.header {
position: absolute;
top: 0;
left: 0;
width: 100%;
padding: 15px 20px;
background: rgba(0, 0, 0, 0.7);
z-index: 200;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
}
.header h1 {
font-size: 1.8rem;
background: linear-gradient(45deg, #00c6ff, #0072ff);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
font-weight: 700;
}
.control-panel {
position: absolute;
top: 100px;
right: 20px;
width: 320px;
background: rgba(0, 0, 0, 0.8);
border-radius: 12px;
padding: 20px;
z-index: 200;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.6);
backdrop-filter: blur(5px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.panel-title {
font-size: 1.4rem;
margin-bottom: 20px;
text-align: center;
color: #3498db;
font-weight: 700;
text-shadow: 0 0 10px rgba(52, 152, 219, 0.5);
}
.btn-group {
display: flex;
gap: 10px;
margin-top: 15px;
}
button {
flex: 1;
padding: 12px;
background: linear-gradient(135deg, #3498db, #2980b9);
border: none;
border-radius: 6px;
color: white;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(52, 152, 219, 0.4);
}
.info-box {
background: rgba(44, 62, 80, 0.7);
border-radius: 8px;
padding: 15px;
margin-top: 20px;
}
.info-title {
font-size: 1.1rem;
color: #f1c40f;
margin-bottom: 10px;
}
.info-content {
font-size: 0.9rem;
line-height: 1.6;
}
.interaction-panel {
position: absolute;
bottom: 20px;
left: 20px;
background: rgba(0, 0, 0, 0.8);
border-radius: 12px;
padding: 15px;
z-index: 200;
max-width: 400px;
}
.interaction-title {
color: #2ecc71;
margin-bottom: 10px;
font-weight: 600;
}
.interaction-content {
font-size: 0.9rem;
}
.highlight {
color: #3498db;
font-weight: 600;
}
.footer {
position: absolute;
bottom: 20px;
right: 20px;
color: #bdc3c7;
font-size: 0.9rem;
z-index: 200;
}
</style>
</head>
<body>
<div class="header">
<h1>Cesium 交互式 ECharts 饼图</h1>
</div>
<div id="cesiumContainer"></div>
<div id="chartContainer"></div>
<div class="control-panel">
<h2 class="panel-title">交互式饼图控制</h2>
<div class="btn-group">
<button id="addPie">添加饼图</button>
<button id="removePie">移除饼图</button>
<button id="updateData">更新数据</button>
</div>
<div class="info-box">
<div class="info-title">实现原理</div>
<div class="info-content">
<p>1. 在Cesium画布上叠加一个透明的ECharts容器</p>
<p>2. 将三维坐标转换为屏幕坐标定位容器</p>
<p>3. 实时同步相机变化更新位置</p>
<p>4. 保留完整的ECharts交互功能</p>
</div>
</div>
</div>
<div class="interaction-panel">
<div class="interaction-title">交互演示说明</div>
<div class="interaction-content">
<p>• <span class="highlight">鼠标悬停</span>: 查看饼图各部分的详细信息</p>
<p>• <span class="highlight">点击饼块</span>: 突出显示并查看具体数值</p>
<p>• <span class="highlight">图例点击</span>: 显示/隐藏对应的饼块</p>
<p>• <span class="highlight">拖拽旋转地球</span>: 饼图位置自动更新</p>
</div>
</div>
<div class="footer">
Cesium + ECharts 高级集成方案 | 保留完整交互特性
</div>
<script>
const viewer = new Cesium.Viewer('cesiumContainer', {
terrainProvider: Cesium.createWorldTerrain(),
baseLayerPicker: false,
animation: false,
timeline: false,
geocoder: false,
homeButton: true,
sceneModePicker: true,
navigationHelpButton: false,
fullscreenButton: true,
infoBox: false
});
// 设置初始相机位置
viewer.camera.setView({
destination: Cesium.Cartesian3.fromDegrees(116.4, 39.9, 1000000)
});
// 创建ECharts实例
const chartContainer = document.getElementById('chartContainer');
const chart = echarts.init(chartContainer);
// 饼图数据
let pieData = [
{ value: 1048, name: '亚洲' },
{ value: 735, name: '欧洲' },
{ value: 580, name: '北美洲' },
{ value: 484, name: '南美洲' },
{ value: 300, name: '非洲' }
];
// 饼图配置
const pieOption = {
backgroundColor: 'transparent',
title: {
text: '全球人口分布',
left: 'center',
top: 10,
textStyle: {
color: '#fff',
fontSize: 16,
fontWeight: 'bold'
}
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)',
backgroundColor: 'rgba(0,0,0,0.8)',
borderColor: '#3498db',
textStyle: {
color: '#fff'
}
},
legend: {
orient: 'vertical',
right: 10,
top: 'center',
textStyle: {
color: '#fff',
fontSize: 12
},
itemGap: 15
},
series: [{
name: '人口分布',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '55%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#000',
borderWidth: 2
},
label: {
show: true,
formatter: '{b}: {d}%',
fontSize: 12,
fontWeight: 'bold',
color: '#fff'
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
},
label: {
show: true,
fontSize: 14,
fontWeight: 'bold'
}
},
labelLine: {
show: true
},
data: pieData,
animationType: 'scale',
animationEasing: 'elasticOut',
animationDuration: 1000
}]
};
// 设置ECharts选项
chart.setOption(pieOption);
// 设置初始大小
const pieSize = 300;
chartContainer.style.width = pieSize + 'px';
chartContainer.style.height = pieSize + 'px';
chart.resize();
// 默认隐藏图表
chartContainer.style.display = 'none';
// 添加/移除饼图
document.getElementById('addPie').addEventListener('click', function() {
chartContainer.style.display = 'block';
updateChartPosition();
});
document.getElementById('removePie').addEventListener('click', function() {
chartContainer.style.display = 'none';
});
// 更新数据
document.getElementById('updateData').addEventListener('click', function() {
// 随机更新数据
pieData = pieData.map(item => {
return {
...item,
value: Math.floor(Math.random() * 1000) + 300
};
});
// 更新饼图数据
chart.setOption({
series: [{
data: pieData
}]
});
});
// 更新图表位置
function updateChartPosition() {
// 获取三维坐标(这里使用固定位置)
const position = Cesium.Cartesian3.fromDegrees(116.4, 39.9, 0);
// 转换三维坐标到屏幕坐标
const screenPosition = Cesium.SceneTransforms.worldToWindowCoordinates(
viewer.scene,
position
);
if (screenPosition) {
// 定位ECharts容器
chartContainer.style.left = (screenPosition.x - pieSize / 2) + 'px';
chartContainer.style.top = (screenPosition.y - pieSize / 2) + 'px';
}
}
// 监听相机变化
viewer.camera.changed.addEventListener(updateChartPosition);
// 初始更新位置
updateChartPosition();
// 添加ECharts事件监听器
chart.on('click', function(params) {
console.log('饼图点击事件:', params);
// 在实际应用中,这里可以显示更多详细信息
viewer.entities.add({
position: Cesium.Cartesian3.fromDegrees(116.4, 39.9, 0),
label: {
text: `${params.name}: ${params.value} (${params.percent}%)`,
font: '14px Microsoft YaHei',
fillColor: Cesium.Color.WHITE,
outlineColor: Cesium.Color.BLACK,
outlineWidth: 2,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
pixelOffset: new Cesium.Cartesian2(0, -pieSize/2 - 30),
disableDepthTestDistance: Number.POSITIVE_INFINITY
}
});
});
chart.on('mouseover', function(params) {
console.log('鼠标悬停:', params.name);
});
// 添加图例切换事件
chart.on('legendselectchanged', function(params) {
console.log('图例选择变化:', params);
});
// 解决事件冲突:当鼠标在图表上时,阻止事件传播到Cesium
chartContainer.addEventListener('mousedown', function(e) {
e.stopPropagation();
});
chartContainer.addEventListener('mousewheel', function(e) {
e.stopPropagation();
});
</script>
</body>
</html>
实现原理详解
1. DOM 叠加技术
我们不是将 ECharts 渲染为纹理,而是在 Cesium 画布上叠加一个透明的 HTML 容器:
<div id="cesiumContainer"></div>
<div id="chartContainer"></div>
2. 坐标转换
使用 Cesium 的坐标转换功能将三维位置映射到屏幕位置:
function updateChartPosition() {
const position = Cesium.Cartesian3.fromDegrees(116.4, 39.9, 0);
const screenPosition = Cesium.SceneTransforms.worldToWindowCoordinates(
viewer.scene,
position
);
if (screenPosition) {
chartContainer.style.left = (screenPosition.x - pieSize / 2) + 'px';
chartContainer.style.top = (screenPosition.y - pieSize / 2) + 'px';
}
}
3. 实时位置更新
监听相机变化事件,确保图表随地球旋转而移动:
viewer.camera.changed.addEventListener(updateChartPosition);
4. 事件处理
解决 Cesium 和 ECharts 的事件冲突:
// 阻止图表事件传播到Cesium
chartContainer.addEventListener('mousedown', function(e) {
e.stopPropagation();
});
5. 完整交互支持
保留 ECharts 的所有原生交互:
// 点击事件
chart.on('click', function(params) {
// 显示详细信息
});
// 悬停事件
chart.on('mouseover', function(params) {
// 高亮显示
});
// 图例切换
chart.on('legendselectchanged', function(params) {
// 更新图表显示
});
优势与特点
-
完整交互保留:
- 鼠标悬停提示
- 点击饼块高亮
- 图例切换显示/隐藏
- 所有 ECharts 原生交互特性
-
视觉一致性:
- 透明背景与 Cesium 场景完美融合
- 随地球旋转而移动
- 保持三维空间的深度感
-
性能优化:
- 只在需要时显示图表
- 使用硬件加速的 CSS 变换
- 高效的事件委托机制
-
响应式设计:
- 自动适应屏幕尺寸变化
- 在缩放时保持清晰度
使用说明
- 点击"添加饼图"按钮在指定位置显示交互式图表
- 鼠标悬停在饼图上查看详细信息
- 点击饼图部分查看具体数值
- 点击图例切换显示/隐藏数据部分
- 使用"更新数据"按钮随机生成新数据
- 拖拽旋转地球时,饼图会自动更新位置
这个解决方案完美结合了 Cesium 的三维地球可视化能力和 ECharts 的丰富交互特性,为地理空间数据可视化提供了强大的工具。