关于地图渲染加20w数据展示和地图动画怎么做
<h1 data-id="heading-0">🧑💻 写在开头</h1><p>点赞 + 收藏 === 学会🤣🤣🤣</p>
<div>
<div>
<h2 data-id="heading-0">前端性能优化实战:ECharts地图渲染12万+数据动态动画方案</h2>
<blockquote>
<p>本文记录了在实际项目中,使用ECharts地图组件渲染12万+设备安装数据的性能优化实战经验,包含完整的技术方案和代码实现。</p>
</blockquote>
<h3 data-id="heading-1">项目背景</h3>
<p>公司需要将全年设备安装量通过旗帜的形式展示在全国地图上,实现数据可视化大屏。主要技术挑战:</p>
<ul>
<li><strong>数据量大</strong>:全年设备安装数据约20万条</li>
<li><strong>实时更新</strong>:通过WebSocket实时接收数据</li>
<li><strong>动画效果</strong>:需要展示数据逐条添加的动态效果</li>
<li><strong>性能要求</strong>:需要保持60fps的流畅动画</li>
</ul>
<h3 data-id="heading-2">一、初始实现与性能瓶颈</h3>
<h4 data-id="heading-3">1.1 基础地图配置</h4>
<p>首先使用ECharts搭建基础地图框架:</p>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">initChart() {
this.chart = echarts.init(this.$refs.chart);
let option = {
geo: {
map: 'china',
roam: true,
zoom: 1.1,
scaleLimit: { min: 1, max: 10 },
itemStyle: {
areaColor: 'rgba(91,97,141,.3)',
borderColor: 'rgba(0,0,0,.2)'
}
},
series: [
// 基础地图层
{
type: 'map',
map: 'china',
itemStyle: {
areaColor: 'rgba(0,0,0,0)',
borderColor: 'rgba(255,255,255,1)',
}
},
// 设备点图层
{
id: 'scatter',
type: 'scatter',
coordinateSystem: 'geo',
data: [],
symbol: 'image://flag.png',// 旗帜图标
symbolSize: ,
animation: false// 关闭内置动画
}
]
};
this.chart.setOption(option);
}</pre>
</div>
<h4 data-id="heading-4">1.2 动画设计</h4>
<p>设计旗帜生长动画效果,通过随机数实现多样化的动画展示:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 旗帜动画效果设计
const flagAnimations = [
'scaleUp', // 从小变大
'fadeIn', // 淡入
'bounceIn', // 弹跳进入
'rotateIn' // 旋转进入
];
function getRandomAnimation() {
return flagAnimations;
}</pre>
</div>
<div>
<div>
<h4 data-id="heading-5">1.3 遇到的性能瓶颈</h4>
<p>当数据量达到3-5万条时,开始出现明显卡顿:</p>
<ul>
<li>动画帧率下降到30fps以下</li>
<li>内存占用持续增长</li>
<li>缩放平移操作卡顿</li>
<li>WebSocket数据堆积</li>
</ul>
<h3 data-id="heading-6">二、分层策略优化方案</h3>
<p>经过调研,我们采用了<strong>分层策略</strong>来优化性能,根据不同的缩放级别采用不同的渲染策略。</p>
<h4 data-id="heading-7">2.1 分层策略设计</h4>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">const zoomConfigs = {
low: {// 低缩放级别:全国视图
zoom: 2,
sampleRate: 0.1, // 10%抽样显示
precision: 2, // 经纬度精度:小数点后2位
symbolSize: // 缩小图标
},
mid: {// 中缩放级别:省级视图
zoom: 5,
sampleRate: 0.5, // 50%抽样显示
precision: 3, // 经纬度精度:小数点后3位
symbolSize:
},
high: { // 高缩放级别:市级视图
zoom: 10,
sampleRate: 1, // 100%显示
precision: 4, // 经纬度精度:小数点后4位
symbolSize:
}
};</pre>
</div>
<h4 data-id="heading-8">2.2 动态动画调度系统</h4>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">class AnimationScheduler {
constructor() {
this.pendingList = []; // 待处理数据队列
this.allDeviceList = []; // 所有数据存储
this.displayList = []; // 当前显示数据
this.deviceSet = new Set(); // 数据去重
this.displaySet = new Set(); // 显示去重
this.animationTimer = null;
// 帧率控制
this.frameInterval = 50; // 20fps
this.batchSize = 100; // 每批处理数量
}
// 启动动画调度
startAnimation() {
if (this.animationTimer) return;
let lastTime = 0;
const animate = (currentTime) => {
if (this.pendingList.length === 0) {
this.stopAnimation();
return;
}
// 帧率控制
if (currentTime - lastTime >= this.frameInterval) {
lastTime = currentTime;
this.processBatch();
}
this.animationTimer = requestAnimationFrame(animate);
};
this.animationTimer = requestAnimationFrame(animate);
}
// 处理一批数据
processBatch() {
const batch = this.pendingList.splice(0, this.batchSize);
const config = this.getCurrentZoomConfig();
let hasNewData = false;
batch.forEach(item => {
// 全局去重
const globalKey = `${item.lng},${item.lat}`;
if (this.deviceSet.has(globalKey)) return;
this.deviceSet.add(globalKey);
const point = {
value: ,
createTime: item.createTime
};
this.allDeviceList.push(point);
// 根据当前缩放级别抽样
if (this.shouldDisplay(point, config)) {
const displayKey = this.getDisplayKey(point, config);
if (!this.displaySet.has(displayKey)) {
this.displaySet.add(displayKey);
this.displayList.push(point);
hasNewData = true;
}
}
});
// 批量更新图表
if (hasNewData) {
this.updateChart();
}
}
}</pre>
</div>
<h4 data-id="heading-9">2.3 智能显示判断</h4>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 根据缩放级别判断是否显示
shouldDisplay(point, config) {
// 完全显示模式
if (config.sampleRate >= 1) return true;
// 抽样显示模式
const displayChance = Math.random();
return displayChance < config.sampleRate;
}
// 生成显示键(根据精度去重)
getDisplayKey(point, config) {
const lng = point.value.toFixed(config.precision);
const lat = point.value.toFixed(config.precision);
return `${lng},${lat}`;
}</pre>
</div>
<h4 data-id="heading-10">2.4 缩放级别变化处理</h4>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 监听缩放变化
setupZoomListener() {
this.chart.on('georoam', () => {
const option = this.chart.getOption();
if (option.geo && option.geo) {
const newZoom = option.geo.zoom;
if (Math.abs(newZoom - this.currentZoom) > 0.1) {
this.currentZoom = newZoom;
this.handleZoomChange();
}
}
});
}
// 处理缩放变化
handleZoomChange() {
const config = this.getCurrentZoomConfig();
// 只有层级变化时才重建显示数据
if (config.level !== this.currentZoomLevel) {
this.currentZoomLevel = config.level;
this.rebuildDisplayList();
}
}
// 重建显示数据
rebuildDisplayList() {
const config = this.getCurrentZoomConfig();
this.displayList = [];
this.displaySet = new Set();
if (config.sampleRate >= 1) {
// 全量显示模式
this.displayAllData(config);
} else {
// 抽样显示模式
this.displaySampledData(config);
}
this.updateChart();
}
// 全量显示(高精度去重)
displayAllData(config) {
for (const item of this.allDeviceList) {
const key = this.getDisplayKey(item, config);
if (!this.displaySet.has(key)) {
this.displaySet.add(key);
this.displayList.push(item);
}
}
}
// 抽样显示
displaySampledData(config) {
const step = Math.max(1, Math.floor(1 / config.sampleRate));
for (let i = 0; i < this.allDeviceList.length; i += step) {
const item = this.allDeviceList;
const key = this.getDisplayKey(item, config);
if (!this.displaySet.has(key)) {
this.displaySet.add(key);
this.displayList.push(item);
}
}
}</pre>
</div>
<h3 data-id="heading-11">三、其他优化技巧</h3>
<h4 data-id="heading-12">3.1 内存管理优化</h4>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 定期清理过期数据
setupMemoryManagement() {
setInterval(() => {
// 限制总数据量
const maxTotal = 150000;
if (this.allDeviceList.length > maxTotal) {
const removeCount = this.allDeviceList.length - 120000;
this.allDeviceList.splice(0, removeCount);
// 清理相关缓存
this.cleanCache();
// 重建显示
this.rebuildDisplayList();
}
}, 30000); // 每30秒检查一次
}
// WebSocket数据流控
setupWebSocketFlowControl() {
let buffer = [];
let processing = false;
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
buffer.push(...data);
// 流量控制:如果缓冲过多,暂停接收
if (buffer.length > 5000 && !processing) {
this.ws.pause();
}
if (!processing) {
this.processWebSocketBuffer();
}
};
}</pre>
</div>
<h4 data-id="heading-13">3.2 性能监控</h4>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 添加性能监控
setupPerformanceMonitor() {
let frames = 0;
let lastTime = performance.now();
const monitor = () => {
frames++;
const currentTime = performance.now();
if (currentTime - lastTime >= 1000) {
const fps = Math.round(frames * 1000 / (currentTime - lastTime));
console.log(`当前FPS: ${fps}`);
// 动态调整策略
this.adjustStrategyByFPS(fps);
frames = 0;
lastTime = currentTime;
}
requestAnimationFrame(monitor);
};
requestAnimationFrame(monitor);
}
// 根据FPS动态调整
adjustStrategyByFPS(fps) {
if (fps < 30) {
// 降低渲染质量
this.frameInterval = 100; // 10fps
this.batchSize = 50;
} else if (fps > 50) {
// 提高渲染质量
this.frameInterval = 33; // 30fps
this.batchSize = 150;
}
}</pre>
</div>
<div>
<div>
<h3 data-id="heading-14">四、效果对比</h3>
<h4 data-id="heading-15">优化前:</h4>
<ul>
<li>3万数据开始卡顿</li>
<li>内存占用500MB+</li>
<li>缩放操作延迟明显</li>
<li>动画掉帧严重</li>
</ul>
<h4 data-id="heading-16">优化后:</h4>
<ul>
<li>12万数据流畅运行</li>
<li>内存控制在200MB以内</li>
<li>缩放操作流畅</li>
<li>保持30fps以上动画</li>
</ul>
<h3 data-id="heading-17">五、总结</h3>
<p>通过分层策略优化,我们成功实现了:</p>
<ol>
<li><strong>智能显示</strong>:根据缩放级别动态调整显示策略</li>
<li><strong>性能平衡</strong>:在视觉效果和性能之间找到平衡点</li>
<li><strong>内存控制</strong>:有效管理大量数据的内存占用</li>
<li><strong>流畅动画</strong>:保持高帧率的动画效果</li>
</ol>
<p>这种分层策略不仅适用于地图可视化,也可以扩展到其他大规模数据可视化场景中。关键思想是:<strong>不同视角需要不同精度的数据展示</strong>。</p>
</div>
<div>
<h3 id="tid-D8HBxE">如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。</h3>
</div>
<p><em><img src="https://img2024.cnblogs.com/blog/2149129/202501/2149129-20250122165814748-630765389.png" alt="" loading="lazy"></em></p>
</div>
</div>
</div><br><br>
来源:https://www.cnblogs.com/smileZAZ/p/19776955
頁:
[1]