关于地图渲染加20w数据展示和地图动画怎么做

🧑‍💻 写在开头

点赞 + 收藏 === 学会🤣🤣🤣

前端性能优化实战:ECharts地图渲染12万+数据动态动画方案

本文记录了在实际项目中,使用ECharts地图组件渲染12万+设备安装数据的性能优化实战经验,包含完整的技术方案和代码实现。

项目背景

公司需要将全年设备安装量通过旗帜的形式展示在全国地图上,实现数据可视化大屏。主要技术挑战:

  • 数据量大:全年设备安装数据约20万条
  • 实时更新:通过WebSocket实时接收数据
  • 动画效果:需要展示数据逐条添加的动态效果
  • 性能要求:需要保持60fps的流畅动画

一、初始实现与性能瓶颈

1.1 基础地图配置

首先使用ECharts搭建基础地图框架:

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: [16, 22],
                animation: false  // 关闭内置动画
            }
        ]
    };
    this.chart.setOption(option);
}

1.2 动画设计

设计旗帜生长动画效果,通过随机数实现多样化的动画展示:

// 旗帜动画效果设计
const flagAnimations = [
    'scaleUp',      // 从小变大
    'fadeIn',       // 淡入
    'bounceIn',     // 弹跳进入
    'rotateIn'      // 旋转进入
];

function getRandomAnimation() {
    return flagAnimations[Math.floor(Math.random() * flagAnimations.length)];
}

1.3 遇到的性能瓶颈

当数据量达到3-5万条时,开始出现明显卡顿:

  • 动画帧率下降到30fps以下
  • 内存占用持续增长
  • 缩放平移操作卡顿
  • WebSocket数据堆积

二、分层策略优化方案

经过调研,我们采用了分层策略来优化性能,根据不同的缩放级别采用不同的渲染策略。

2.1 分层策略设计

const zoomConfigs = {
    low: {  // 低缩放级别:全国视图
        zoom: 2,
        sampleRate: 0.1,    // 10%抽样显示
        precision: 2,       // 经纬度精度:小数点后2位
        symbolSize: [8, 11] // 缩小图标
    },
    mid: {  // 中缩放级别:省级视图
        zoom: 5,
        sampleRate: 0.5,    // 50%抽样显示
        precision: 3,       // 经纬度精度:小数点后3位
        symbolSize: [12, 16]
    },
    high: { // 高缩放级别:市级视图
        zoom: 10,
        sampleRate: 1,      // 100%显示
        precision: 4,       // 经纬度精度:小数点后4位
        symbolSize: [16, 22]
    }
};

2.2 动态动画调度系统

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: [item.lng, item.lat],
                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();
        }
    }
}

2.3 智能显示判断

// 根据缩放级别判断是否显示
shouldDisplay(point, config) {
    // 完全显示模式
    if (config.sampleRate >= 1) return true;
    
    // 抽样显示模式
    const displayChance = Math.random();
    return displayChance < config.sampleRate;
}

// 生成显示键(根据精度去重)
getDisplayKey(point, config) {
    const lng = point.value[0].toFixed(config.precision);
    const lat = point.value[1].toFixed(config.precision);
    return `${lng},${lat}`;
}

2.4 缩放级别变化处理

// 监听缩放变化
setupZoomListener() {
    this.chart.on('georoam', () => {
        const option = this.chart.getOption();
        if (option.geo && option.geo[0]) {
            const newZoom = option.geo[0].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[i];
        const key = this.getDisplayKey(item, config);
        if (!this.displaySet.has(key)) {
            this.displaySet.add(key);
            this.displayList.push(item);
        }
    }
}

三、其他优化技巧

3.1 内存管理优化

// 定期清理过期数据
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();
        }
    };
}

3.2 性能监控

// 添加性能监控
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;
    }
}

四、效果对比

优化前:

  • 3万数据开始卡顿
  • 内存占用500MB+
  • 缩放操作延迟明显
  • 动画掉帧严重

优化后:

  • 12万数据流畅运行
  • 内存控制在200MB以内
  • 缩放操作流畅
  • 保持30fps以上动画

五、总结

通过分层策略优化,我们成功实现了:

  1. 智能显示:根据缩放级别动态调整显示策略
  2. 性能平衡:在视觉效果和性能之间找到平衡点
  3. 内存控制:有效管理大量数据的内存占用
  4. 流畅动画:保持高帧率的动画效果

这种分层策略不仅适用于地图可视化,也可以扩展到其他大规模数据可视化场景中。关键思想是:不同视角需要不同精度的数据展示

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

posted @ 2026-01-26 09:45  林恒  阅读(13)  评论(0)    收藏  举报