跳到主要内容
基于 ECharts 与 Three.js 的碳排放可视化大屏实战 | 极客日志
JavaScript 大前端
基于 ECharts 与 Three.js 的碳排放可视化大屏实战 基于 ECharts 和 Three.js 构建碳排放可视化大屏,涵盖 HTML 布局、CSS 响应式适配及 JS 图表交互逻辑。项目采用 Flask 模板引擎渲染,包含柱状图、折线图、饼图及 3D 地球组件。通过 rem 单位实现多端适配,动态加载 Three.js 库以优化性能。代码结构清晰,支持 Tab 切换数据源,适合企业级数据展示场景参考。
黑客帝国 发布于 2026/4/8 更新于 2026/4/25 2 浏览效果预览
技术选型与架构
这个大屏项目主要采用了前端三件套(HTML/CSS/JS)配合两个强大的可视化库:
ECharts :处理各类统计图表,如柱状图、折线图、饼图等。
Three.js :用于构建中间的 3D 地球模型,增加视觉冲击力。
后端模板 :代码中出现了 {{ url_for(...) }},说明这是基于 Flask 或类似 Python 框架的 Jinja2 模板渲染。
整体布局采用 Flexbox 实现响应式排布,适配不同分辨率的屏幕。
核心代码解析
1. 页面结构 (view.html)
主界面由三个垂直列组成:左侧数据区、中间 3D 展示区、右侧数据区。时间显示和头部导航是独立的模块。
<!DOCTYPE html >
<html lang ="en" >
<head >
<meta charset ="UTF-8" >
<title > Title</title >
<link rel ="stylesheet" href ="{{ url_for('static', filename='css/view.css') }}" >
</head >
<body >
<header >
<div class ="container" >
<h1 > 碳排放可视化大屏</ >
教室
实验室
宿舍
无纸化考试次数
12
没想好
419
没想好
10.5
没想好
4.7
没想好
教室
实验室
宿舍
早
晚
h1
</div >
<div class ="showTime" >
</div >
<script >
var t = null ;
t = setTimeout (time, 1000 );
function time ( ) {
clearTimeout (t);
a = new Date ();
var y = a.getFullYear ();
var mt = a.getMonth () + 1 ;
var day = a.getDate ();
var h = a.getHours ();
var m = a.getMinutes ();
var s = a.getSeconds ();
document .querySelector (".showTime" ).innerHTML = '当前时间:' + y + "年" + mt + "月" + day + "-" + h + "时" + m + "分" + s + "秒" ;
t = setTimeout (time, 1000 );
}
</script >
</header >
<section class ="box" >
<div class ="column" >
<div class ="public bar right-chart-module" >
<div class ="chart-title" >
<h2 >
</h2 >
<div class ="chart-tabs" >
<button class ="tab-btn active" data-type ="classroom" >
</button >
<button class ="tab-btn" data-type ="lab" >
</button >
<button class ="tab-btn" data-type ="dormitory" >
</button >
</div >
</div >
<div class ="char" id ="leftBarChart" >
</div >
<div class ="public-one" >
</div >
</div >
<div class ="public bar" >
<h2 >
</h2 >
<div class ="chart-tabs" >
<div class ="char" id ="horizontalBarChart" >
</div >
</div >
<div class ="public-one" >
</div >
</div >
</div >
<div class ="column" >
<div class ="num" >
<div class ="globe-container" >
<canvas id ="globe-canvas" >
</canvas >
</div >
<div class ="globe-data" >
<div class ="data-row" >
<div class ="data-item" >
<div class ="data-value" id ="global-temp" >
</div >
<div class ="data-label" >
</div >
</div >
<div class ="data-item" >
<div class ="data-value" id ="co2-level" >
</div >
<div class ="data-label" >
</div >
</div >
</div >
<div class ="data-row" >
<div class ="data-item" >
<div class ="data-value" id ="china-emission" >
</div >
<div class ="data-label" >
</div >
</div >
<div class ="data-item" >
<div class ="data-value" id ="world-emission" >
</div >
<div class ="data-label" >
</div >
</div >
</div >
</div >
</div >
</div >
<div class ="column" >
<div class ="public bar right-chart-module" >
<div class ="chart-title" >
<h2 >
</h2 >
<div class ="chart-tabs" >
<button class ="tab-btn active" data-type ="classroom" >
</button >
<button class ="tab-btn" data-type ="lab" >
</button >
<button class ="tab-btn" data-type ="dormitory" >
</button >
</div >
</div >
<div class ="char" id ="vehicleChart" >
</div >
<div class ="public-one" >
</div >
</div >
<div class ="public pie" >
<div class ="chart-title" >
<h2 >
</h2 >
<div class ="chart-tabs" >
<button class ="tab-btn active" data-time ="morning" >
</button >
<button class ="tab-btn" data-time ="evening" >
</button >
</div >
</div >
<div class ="char" id ="pieChart" >
</div >
<div class ="public-one" >
</div >
</div >
</div >
</section >
<script src ="{{ url_for('static', filename='js/echarts.js') }}" >
</script >
<script src ="{{ url_for('static', filename='js/echarts.min.js') }}" >
</script >
<script src ="{{ url_for('static', filename='js/view.js') }}" >
</script >
<script src ="{{ url_for('static', filename='js/flexible.js') }}" >
</script >
<script src ="{{ url_for('static', filename='js/jiazai.js') }}" >
</script >
<script >
document .addEventListener ('DOMContentLoaded' , () => {
setTimeout (() => window .dispatchEvent (new Event ('resize' )), 300 );
});
</script >
</body >
</html >
2. 样式适配 (view.css) 这里使用了 rem 单位配合媒体查询来实现响应式布局。背景图设置了固定定位,确保滚动时保持视觉效果。图表容器通过伪元素添加了科技感边框装饰。
* { margin : 0 ; padding : 0 ; box-sizing : border-box; }
li { list-style : none; }
@font-face { font-family : electronicFont; src : url (/static/font/ziti.TTF ); }
html { font-size : 100px ; overflow : hidden; width : 100% ; height : 100% ; }
body {
font-family : Arial, Helvetica, sans-serif;
margin : 0 ; padding : 0 ;
background : url (/static/images/bei.jpg ) no-repeat;
background-size : cover;
background-attachment : fixed;
line-height : 1.15 ;
width : 100% ; height : 100% ; overflow : hidden;
}
header { position : relative; height : 1.11rem ; background : url (/static/images/head.png ) no-repeat top center; background-size : 100% 100% ; width : 100% ; }
header .container h1 { font-size : 0.51rem ; color : #fff ; text-align : center; line-height : 1.07rem ; -webkit-text -stroke : 2px #00f7ff ; text-shadow : 0 0 10px rgba (0 , 247 , 255 , 0.5 ); }
header .showTime { position : absolute; right : 0.4rem ; top : 0 ; line-height : 1rem ; color : #2680ce ; font-size : 0.27rem ; font-family : 'electronicFont' ; white-space : nowrap; }
.box { display : flex; min-width : 1024px ; max-width : 1920px ; width : 100% ; height : calc (100% - 1.11rem ); margin : 0 auto; padding : 0.13rem ; overflow : hidden; }
.box .column { flex : 3 ; height : 100% ; display : flex; flex-direction : column; }
.box .column :nth-child (2 ) { flex : 5 ; margin : 0 0.13rem ; }
.box .public { position : relative; height : 5.6rem ; border : 1px solid rgba (25 , 186 , 139 , 0.17 ); background : url (/static/images/bian.png ) rgba (255 , 255 , 255 , 0.04 ); padding : 0 0.2rem 0.53rem ; margin-bottom : 0.2rem ; width : 100% ; }
.box .public ::before { position : absolute; top : 0 ; left : 0 ; width : 10px ; height : 10px ; border-left : 2px solid #02a6b5 ; border-top : 2px solid #02a6b5 ; content : "" ; }
.box .public ::after { position : absolute; top : 0 ; right : 0 ; width : 10px ; height : 10px ; border-right : 2px solid #02a6b5 ; border-top : 2px solid #02a6b5 ; content : "" ; }
@media screen and (max-width : 1024px ) { html { font-size : 42px !important ; } }
@media screen and (min-width : 1920px ) { html { font-size : 80px !important ; } }
.globe-container { flex : 2 ; position : relative; width : 100% ; }
#globe-canvas { width : 100% ; height : 100% ; display : block; }
.data-value { font-family : 'electronicFont' ; font-size : 0.45rem ; color : #00f7ff ; margin-bottom : 0.1rem ; }
3. 图表逻辑 (view.js) 这部分是核心。我们封装了多个 IIFE(立即执行函数)来隔离作用域,避免全局变量污染。每个图表模块都独立配置数据源和 ECharts 选项。
柱状图与折线图切换 通过点击 Tab 按钮动态更新 dataset 和 option。注意在 updateChart 函数中,我们重用了 colors 配置,保证同一场景下色调一致。
(function ( ){
const chartData = {
classroom : {
title : '教室每日用电量统计' ,
data : {
categories : ['周一' ,'周二' ,'周三' ,'周四' ,'周五' ,'周六' ,'周日' ],
series : [
{name :'早' , data :[6.28 ,9.55 ,8.69 ,8.63 ,6.64 ,4.17 ,2.69 ]},
{name :'晚' , data :[5.71 ,8.65 ,5.32 ,5.91 ,5.77 ,3.17 ,1.85 ]}
]
},
colors : ['#5470C6' ,'#91CC75' ]
},
lab : { },
dormitory : { }
};
document .addEventListener ('DOMContentLoaded' , function ( ){
var barChartDom = document .querySelector ('.bar .char' );
var tabButtons = document .querySelectorAll ('.bar .tab-btn' );
if (barChartDom && tabButtons.length > 0 ){
var barChart = echarts.init (barChartDom);
var currentType = 'classroom' ;
function updateChart (type ){
const data = chartData[type].data ;
const colors = chartData[type].colors ;
const barOption = {
color : colors,
title : { text : chartData[type].title , left :'center' , textStyle :{fontSize :16 ,color :'#fff' } },
tooltip : { trigger :'axis' , axisPointer :{type :'shadow' } },
legend : { data :['早' ,'晚' ], textStyle :{color :'#fff' }, top :20 },
grid : { top :'20%' , left :'3%' , right :'4%' , bottom :'3%' , containLabel :true },
xAxis : { type :'category' , data : data.categories , axisLine :{lineStyle :{color :'#fff' }}, axisLabel :{color :'#fff' , interval :0 } },
yAxis : { type :'value' , name :'用电量 (千瓦)' , nameTextStyle :{color :'#fff' , padding :[0 ,0 ,0 ,30 ]}, splitLine :{lineStyle :{color :'rgba(255, 255, 255, 0.2)' }} },
series : [
{name :'早' , type :'bar' , barWidth :'30%' , barGap :'50%' , data : data.series [0 ].data },
{name :'晚' , type :'bar' , barWidth :'30%' , data : data.series [1 ].data }
]
};
barChart.setOption (barOption);
}
updateChart (currentType);
tabButtons.forEach (button => {
button.addEventListener ('click' , function ( ){
tabButtons.forEach (btn => btn.classList .remove ('active' ));
this .classList .add ('active' );
currentType = this .dataset .type ;
updateChart (currentType);
});
});
window .addEventListener ('resize' , function ( ){ barChart.resize (); });
}
});
})();
3D 地球实现 (Three.js) 为了加载 Three.js 而不依赖本地安装,代码中使用了动态脚本注入的方式。如果全局没有 THREE 对象,则从 CDN 加载。这保证了项目的轻量级和便携性。
function initGlobe ( ){
const scene = new THREE .Scene ();
const camera = new THREE .PerspectiveCamera (75 , 1 , 0.1 , 1000 );
const renderer = new THREE .WebGLRenderer ({ canvas : document .getElementById ('globe-canvas' ), antialias : true , alpha : true });
renderer.setSize (400 , 300 );
renderer.setClearColor (0x000000 , 0 );
const geometry = new THREE .SphereGeometry (5 , 64 , 64 );
const material = new THREE .MeshPhongMaterial ({
color : 0x156289 , emissive : 0x072534 , shininess : 5 , transparent : true , opacity : 0.8 , wireframe : false
});
const textureLoader = new THREE .TextureLoader ();
material.map = textureLoader.load ('/static/images/img.png' );
material.bumpMap = textureLoader.load ('https://threejs.org/examples/textures/planets/earth_normal_2048.jpg' );
material.specularMap = textureLoader.load ('https://threejs.org/examples/textures/planets/earth_specular_2048.jpg' );
const earth = new THREE .Mesh (geometry, material);
scene.add (earth);
const ambientLight = new THREE .AmbientLight (0x333333 );
scene.add (ambientLight);
const directionalLight = new THREE .DirectionalLight (0xffffff , 1 );
directionalLight.position .set (5 , 3 , 5 );
scene.add (directionalLight);
camera.position .z = 12 ;
function animate ( ){
requestAnimationFrame (animate);
earth.rotation .y += 0.002 ;
renderer.render (scene, camera);
}
animate ();
function resizeGlobe ( ){
const container = document .querySelector ('.globe-container' );
const width = container.clientWidth ;
const height = container.clientHeight ;
camera.aspect = width / height;
camera.updateProjectionMatrix ();
renderer.setSize (width, height);
}
window .addEventListener ('resize' , resizeGlobe);
resizeGlobe ();
}
if (typeof THREE === 'undefined' ){
const script = document .createElement ('script' );
script.src = 'https://cdn.jsdelivr.net/npm/[email protected] /build/three.min.js' ;
script.onload = initGlobe;
document .head .appendChild (script);
}else {
initGlobe ();
}
4. 辅助工具
flexible.js : 负责根据视口宽度动态计算 html 的 font-size,实现基于 rem 的自适应布局。
jiazai.js : 简单的资源重载机制,防止 CSS 缓存导致样式不生效的问题。
总结 这套方案的核心在于将静态页面结构与动态 JS 逻辑分离,同时利用 ECharts 丰富的图表类型满足业务需求,用 Three.js 提升视觉表现力。在实际部署时,只需将静态资源放入 Web 服务器目录,并配置好后端模板路径即可运行。对于大屏开发来说,这种模块化拆分的思路非常值得借鉴。
相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online