前面介绍了CMAQ(Community Multiscale Air Quality 通用多尺度空气质量)模型,可以进行空气质量预报,也可以进行污染物来源解析,之前文章介绍了空气质量预报分析的前端查询页面如何设计和开发,本文将介绍来源解析的前端页面如何设计和开发。
前端html代码如下:
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<title></title>
<!-- CSS only -->
<link href="/static/lib/bootstrap-5.1.3-dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.tableDiv {
height: 600px;
overflow: scroll;
}
[data-role="toolbar"] {
text-align: center;
}
#mapbar {
min-height: 700px;
}
#barpie {
width: 100%;
min-height: 700px;
}
td[data-group]{
background: white;
}
#pie_province,
#pie_type {
height: 300px;
}
thead {
border-width: 1px 1px;
background: #00BC80;
/* background: #BAD3F2;*/
}
th,
td {
text-align: center;
vertical-align: middle;
}
th{
min-width: 80px;
max-width: 80px;
}
th.factor{
min-width: 60px;
max-width: 60px;
}
.highlight{
background: pink;
}
td.factor b{
font-weight: normal;
}
#title {
text-align: center;
font-size: 28px;
margin: 12px;
}
</style>
<script type="text/template" id="contributeTableTemplate">
<table class="table table-bordered border-secondary">
<thead class='thead'>
<tr>
<th class="group region" rowspan="2">
<b>区域</b></th>
<th class="site city" rowspan="2">
<b>城市</b></th>
<% _.each(sources,function(source){ %>
<th class="factor" colspan="3">
<b><%=dictSource[source]%></b>
</th>
<% }) %>
<th class="factor" colspan="3">
<b>总和</b>
</th>
</tr>
</thead>
<tbody>
<% data1.reverse();%>
<% _.each(data1,function(site,rIndex){ %>
<tr data-site="<%=site['name']%>">
<%if(rIndex==0)%>
<td class="td" rowspan="<%=data1.length%>" data-group="">河南省</td>
<%;%>
<td class="td" class="td-site"><%=site['name']%></td>
<% _.each(sources,function(source){ %>
<td class="factor" colspan="3">
<b><%=site['values'][source]%>%</b>
</td>
<% }) %>
<td class="factor" colspan="3">
<b><%=site['values']['total'].toFixed(1)%>%</b>
</td>
</tr>
<% }); %>
<% _.each(data2,function(site,rIndex){ %>
<tr>
<td class="td" data-group=""><%=site['name']%></td>
<td class="td" class="td-site"></td>
<% _.each(sources,function(source){ %>
<td class="factor" colspan="3">
<b><%=site['values'][source]%>%</b>
</td>
<% }) %>
<td class="factor" colspan="3">
<b>
<%=site['values']['total'].toFixed(1)%>%</b>
</td>
</tr>
<% }); %>
</tbody>
</table>
</script>
<script type="text/template" id="optionsTemplate">
<% _.each(rows,function(item){ %>
<option value="<%=item['code']%>"><%=item['name']%></option>
<% }) %>
</script>
</head>
<body>
<div class="container-fluid">
<div class="row g-3" data-role="toolbar" data-menu="aqi-contribute-sources" style="display: none;">
<div class="col-1 right">
<label for="option_region" class="form-label">城市</label>
</div>
<div class="col-1">
<select id="option_region" class="form-select " aria-label="Default select example">
</select>
</div>
<div class="col-1 right">
<label for="option_date" class="form-label">起报时间:</label>
</div>
<div class="col-2">
<input type="date" id="option_date" class="form-control" aria-label="Default select example">
</div>
<div class="col-1 right">
<label for="option_factor" class="form-label">污染因子</label>
</div>
<div class="col-1">
<select id="option_factor" class="form-select " aria-label="Default select example">
<option value="PM10_24h">PM<sub>10</sub></option>
<option value="PM25_24h">PM<sub>2.5</sub></option>
<option value="SO2_24h">SO<sub>2</sub></option>
<option value="NO2_24h">NO<sub>2</sub></option>
<option value="CO_24h">CO</option>
<option value="O3_8h">O<sub>3</sub></option>
</select>
</div>
<div class="col-1 right">
<label for="option_date2" class="form-label">预报时间</label>
</div>
<div class="col-2">
<input id="option_date2" type="date" class="form-control">
</div>
<div class="col-1">
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="radioHtype" id="inlineRadio1" value="ground" checked>
<label class="form-check-label" for="inlineRadio1">近地层</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="radioHtype" id="inlineRadio2" value="atmosphere">
<label class="form-check-label" for="inlineRadio2">大气边界层</label>
</div>
</div>
<div class="col-1">
<button id="option_submit" type="submit" class="btn btn-primary mb-3">查询</button>
</div>
</div>
<div class="row"><div id="title"></div></div>
<div class="row justify-content-start">
<div class="col-7">
<div id="contributeDiv"></div>
</div>
<div class="col-5">
<div class="row">
<div id="pie_province"></div>
</div>
<div class="row">
<div id="pie_type"></div>
</div>
</div>
</div>
<div class="row">
<div class="col-6">
<div class="row">
<div id="barpie"></div>
</div>
</div>
<div class="col-6">
<div id="mapbar"></div>
</div>
</div>
</div>
<script src="/static/lib/jquery/jquery-1.11.3.js"></script>
<script src="/static/lib/underscore/underscore.js"></script>
<script src="/static/lib/bootstrap-5.1.3-dist/js/bootstrap.min.js"></script>
<script src="/static/lib/daterangepicker/daterangepicker.js"></script>
<script src="/static/lib/moment/moment.min.js"></script>
<script src="/static/lib/moment/zh-cn.js"></script>
<script src="/static/lib/chartjs/chart.min.js"></script>
<script src="/static/lib/echarts/echarts.min.js"></script>
<script src="/static/lib/d3/d3.v5.min.js"></script>
<script src="/static/js/base.js"></script>
<script src="/static/js/aqi-contribute-sources.js"></script>
</body></html>
之前使用的base.js文件外。aqi-contribute-sources.js文件JavaScript代码如下:
$(function () {
function initProvincePie(data) {
var date = moment($('#option_date').val()).format('YYYY年M月D日');
var date2 = moment($('#option_date2').val()).format('YYYY年M月D日');
var factor = $('#option_factor').val();
$('#title').html(`${city}${date2}${dictFactor[factor]}源解析预报<sub>(起报时间:${date})</sub>`);
var option = {
title: {
text: `地区分布`,
left: "center",
},
tooltip: {
trigger: 'item',
formatter: '{b} : {c}%'
},
legend: {
left: 'center',
top: 'bottom',
data: [
'rose1',
'rose2',
]
},
series: [
{
stillShowZeroSum: false,
name: 'Radius Mode',
type: 'pie',
radius: [20, 100],
// roseType: 'area',
itemStyle: {
borderRadius: 5
},
labelLine: {
show: true
},
label: {
edgeDistance: '25%',
show: true,
formatter: '{b}-{c}%',
position: 'outside'
},
emphasis: {
label: {
show: true
}
},
data: data
}]
};
var chartDom = document.getElementById('pie_province');
var myChart = echarts.init(chartDom);
myChart.setOption(option);
}
function initTypePie(data) {
var option = {
title: {
text: `行业分布`,
left: "center"
},
tooltip: {
trigger: 'item',
formatter: ' {b} : {c}%'
},
legend: {
left: 'center',
top: 'bottom',
data: [
'rose1',
'rose2',
]
},
series: [
{
name: '',
type: 'pie',
radius: [20, 100],
avoidLabelOverlap: true,
// roseType: 'radius',
itemStyle: {
borderRadius: 5
},
labelLine: {
show: true
},
label: {
show: true,
formatter: '{b}:{c}%',
position: 'outside'
},
emphasis: {
label: {
show: true
}
},
data: data
}]
};
var chartDom = document.getElementById('pie_type');
var myChart = echarts.init(chartDom);
myChart.setOption(option);
}
function initTable(data1, data2) {
var template = _.template($('#contributeTableTemplate').html());
var html = template({
data1: data1,
data2: data2,
sources: sources
});
$('#contributeDiv').html(html);
}
function initBar(data, city) {
var yData = data.map(x => x['name']);
var sources0 = sources.concat(['total']);
data = _.object(sources0, sources0.map(x => _.map(data, y => y['values'][x])));
var emphasisStyle = {
itemStyle: {
shadowBlur: 10,
shadowColor: 'rgba(0,0,0,0.3)'
}
};
var series = [];
series.push({
name: '',
type: 'bar',
color: 'transparent',
show: false,
label: {
show: true,
position: 'right',
formatter: x => {
return x.value + '%'
},
},
barGap: '-100%',
emphasis: emphasisStyle,
data: data['total']
});
_.map(_.omit(data, 'total'), (x, i) => {
series.push({
name: dictSource[i],
type: 'bar',
stack: 'one',
label: {
show: false,
position: 'inside',
formatter: x => {
return x.value + '%'
},
},
barCategoryGap: '50%',
emphasis: emphasisStyle,
data: x
});
})
yData = yData.map(x => {
if (x == city) {
return {
value: x,
// 突出周一
textStyle: {
fontWeight: 'bold',
fontSize: 16,
color: 'red',
}
}
}
return x;
});
option = {
title: {
text: `省内行业分布`,
left: "center"
},
legend: {
selectedMode: true,
top: '20px',
// right: '10px',
// orient : 'vertical'
},
tooltip: {
trigger: 'item',
formatter: '{a0}-{b0}: {c0}%',
textStyle: {
fontWeight: 'bold',
}
},
yAxis: {
data: yData,
type: 'category',
axisLine: {
onZero: true
},
splitLine: {
show: false
},
splitArea: {
show: false
}
},
xAxis: {
type: 'value',
show: false,
},
grid: {
bottom: 100
},
series: series
};
var chartDom = document.getElementById('barpie');
var myChart = echarts.init(chartDom);
myChart.setOption(option);
var invertSource = _.invert(dictSource);
myChart.on('legendselectchanged', function (params) {
var selected = _.map(_.filter(_.pairs(_.omit(params.selected, '')), x => x[1]), y => y[0]);
var sources0 = _.map(selected, x => invertSource[x]);
var option0 = this.getOption();
var data0 = _.pick(data, sources0);
var total0 = _.map(_.unzip(_.values(data0)), x => +d3.sum(x).toFixed(1));
option0.series[0].data = total0;
myChart.setOption(option0);
initMap(list_city, city, sources0)
return false;
});
}
function initMap(data, city, sources) {
data = data.map(x => {
var value = +d3.sum(_.values(_.pick(x['values'], sources))).toFixed(1);
return {
'name': x['name'],
'value': value
}
});
// 动态计算柱形图的高度(定一个max)
function lineMaxHeight() {
const maxValue = Math.max(...data.map(item => item.value))
return 0.9 / maxValue
}
// 柱状体的主干
function lineData() {
return data.map((item) => {
return {
coords: [geoCoordMap[item.name], [geoCoordMap[item.name][0], geoCoordMap[item.name][1] + item.value * lineMaxHeight()]]
}
})
}
// 柱状体的顶部
function scatterData() {
return data.map((item) => {
return [geoCoordMap[item.name][0], geoCoordMap[item.name][1] + item.value * lineMaxHeight(), item.value]
})
}
// 柱状体的底部
function scatterData2() {
return data.map((item) => {
return {
name: item.name,
value: geoCoordMap[item.name]
}
})
}
var chartDom = document.getElementById('mapbar');
var myChart = echarts.init(chartDom);
var center = henan.features.find(x => x.properties.name == city).properties.center;
const mapOption = {
title: {
text: `省内地区分布`,
left: "center"
},
tooltip: {
trigger: 'item',
formatter: x => {
if (!isNaN(x.value)) {
return x.name + ':' + x.value + '%'
} else {
return x.name
}
},
textStyle: {
fontWeight: 'bold',
}
},
// backgroundColor: 'lightgray',
visualMap: {
left: 'right',
show: false,
seriesIndex: 5,
inRange: {
color: ['#ffffbf', '#fee090', '#fdae61', '#f46d43', '#d73027', '#a50026']
},
text: ['高', 'L低'],
calculable: true
},
geo: {
map: 'henan',
aspectScale: 0.9,
roam: false,
zoom: 1,
itemStyle: {
normal: {
areaColor: 'transparent',
shadowColor: '#fdae61',
shadowOffsetX: 0,
shadowOffsetY: 0,
borderWidth: 1,
},
emphasis: {
areaColor: 'white',
borderWidth: 1,
color: 'green',
label: {
show: false
}
}
},
emphasis: {
itemStyle: {
areaColor: '#0160AD'
},
label: {
show: 0,
color: '#fff'
}
},
},
series: [
{
geoIndex: 0,
showLegendSymbol: true,
type: 'map',
roam: true,
label: {
normal: {
show: false,
textStyle: {
color: '#fff'
}
},
emphasis: {
show: false,
textStyle: {
color: '#fff'
}
}
},
itemStyle: {
normal: {
borderColor: '#2ab8ff',
borderWidth: 1.5,
areaColor: '#12235c'
},
emphasis: {
areaColor: '#2AB8FF',
borderWidth: 0,
color: 'red'
}
},
map: 'hanan', // 使用
data: data
},
// 柱状体的主干
{
type: 'lines',
zlevel: 5,
effect: {
show: false,
symbolSize: 5 // 图标大小
},
lineStyle: {
width: 20, // 尾迹线条宽度
color: 'rgb(255,0,0, 0.6)',
opacity: 1, // 尾迹线条透明度
curveness: 0 // 尾迹线条曲直度
},
label: {
show: 0,
position: 'end',
formatter: '245'
},
silent: true,
data: lineData()
},
// 柱状体的顶部
{
type: 'scatter',
coordinateSystem: 'geo',
geoIndex: 0,
zlevel: 5,
label: {
show: true,
formatter: function (params) {
return params.value[2] + '%'
},
position: "top",
color: 'red',
fontWeight: 'bold'
},
symbol: 'circle',
symbolSize: [20, 10],
itemStyle: {
color: 'rgb(255,0,0, 0.8)',
opacity: 1
},
silent: true,
data: scatterData()
},
// 柱状体的底部
{
type: 'scatter',
coordinateSystem: 'geo',
geoIndex: 0,
zlevel: 7,
label: {
// 这儿是处理的
formatter: '{b}',
position: 'bottom',
color: 'black',
fontSize: 12,
distance: 10,
show: true
},
symbol: 'circle',
symbolSize: [20, 10],
itemStyle: {
// color: '#F7AF21',
color: 'rgb(255,0,0, 0.6)',
opacity: 1
},
silent: true,
data: scatterData2()
},
// 底部外框
{
type: 'scatter',
coordinateSystem: 'geo',
geoIndex: 0,
zlevel: 4,
label: {
show: false
},
symbol: 'circle',
symbolSize: [40, 20],
itemStyle: {
color: {
type: 'radial',
x: 0.5,
y: 0.5,
r: 0.5,
colorStops: [
{
offset: 0,
color: 'rgb(255,0,0, 0)' // 0% 处的颜色
},
{
offset: .75,
color: 'rgb(255,0,0, 0)' // 100% 处的颜色
},
{
offset: .751,
color: 'rgb(255,0,0, 1)' // 100% 处的颜色
},
{
offset: 1,
color: 'rgb(255,0,0, 1)' // 100% 处的颜色
}
],
global: false // 缺省为 false
},
opacity: 1
},
silent: true,
data: scatterData2()
},
{
id: 'contribute',
type: 'map',
label: {
show: false,
},
aspectScale: 0.9, //长宽比
zoom: 1,
roam: false,
map: 'henan',
animationDurationUpdate: 1000,
universalTransition: true,
data: data
}, {
type: 'effectScatter',
coordinateSystem: 'geo',
showEffectOn: 'render',
rippleEffect: {
period: 15,
scale: 10,
color: 'rgb(255,0,0,0.6)',
brushType: 'fill'
},
hoverAnimation: true,
itemStyle: {
normal: {
areaColor: 'red',
shadowBlur: 10,
shadowColor: 'blue'
}
},
data: [{
name: city,
value: center
}]
}
]
};
myChart.setOption(mapOption);
}
function initContribute1(data) {
var cIndex = Math.min(11, Math.max(3, data.length));
var colors = d3.schemeSpectral[cIndex]
var ctx = document.getElementById('contribute1').getContext('2d');
var myChart = new Chart(ctx, {
type: 'doughnut',
rotation: 1.2,
data: {
labels: _.pluck(data, 'label'),
rotation: 1.2,
datasets: [
{
label: '',
data: _.pluck(data, 'value'),
backgroundColor: colors
}
]
},
options: {
layout: {
padding: 20
}
},
plugins: [plugin],
});
}
function initContributeOuter1(data) {
var cIndex = Math.min(11, Math.max(3, data.length));
var colors = d3.schemeSpectral[cIndex]
var ctx = document.getElementById('contributeOuter1').getContext('2d');
var myChart = new Chart(ctx, {
type: 'doughnut',
rotation: 1.2,
data: {
labels: _.pluck(data, 'label'),
rotation: 1.2,
datasets: [
{
label: '',
data: _.pluck(data, 'value'),
backgroundColor: colors
}
]
},
options: {
layout: {
padding: 20
}
},
plugins: [plugin],
});
}
function initContributeInner1(data) {
var cIndex = Math.min(11, Math.max(3, data.length));
var colors = d3.schemeSpectral[cIndex]
var ctx = document.getElementById('contributeInner1').getContext('2d');
var myChart = new Chart(ctx, {
type: 'doughnut',
rotation: 1.2,
data: {
labels: _.pluck(data, 'label'),
rotation: 1.2,
datasets: [
{
label: '',
data: _.pluck(data, 'value'),
backgroundColor: colors
}
]
},
options: {
layout: {
padding: 20
}
},
plugins: [plugin],
});
}
function initContribute2(labels, data) {
var dictLabel = {
'inner': '内部',
'outer': '外部'
};
var cIndex = Math.min(11, Math.max(3, data.length));
var colors = d3.schemeSpectral[cIndex]
var chartDom = document.getElementById('contribute2');
var myChart = echarts.init(chartDom);
var series = data.map((x, i) => {
return {
name: dictLabel[x['label']],
type: 'bar',
barGap: '0%',
barCategoryGap: '30%',
itemStyle: {
color: colors[i]
},
emphasis: {
focus: 'series'
},
data: x['value']
}
});
var option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {
data: Object.values(dictLabel)
},
xAxis: [
{
splitArea: {
show: true,
areaStyle: {
color: ['rgba(250,250,250,0.3)', 'rgba(200,200,200,0.3)']
}
},
type: 'category',
axisTick: {
show: false
},
data: labels
}
],
yAxis: [
{
type: 'value'
}
],
series: series
};
myChart.setOption(option);
}
function initContributeInner2(labels, data) {
var cIndex = Math.min(11, Math.max(3, data.length));
var colors = d3.schemeSpectral[cIndex]
var chartDom = document.getElementById('contributeInner2');
var myChart = echarts.init(chartDom);
var labels2 = _.pluck(data, 'label')
var series = data.map((x, i) => {
return {
name: x['label'],
type: 'bar',
barGap: '0%',
barCategoryGap: '30%',
itemStyle: {
color: colors[i]
},
emphasis: {
focus: 'series'
},
data: x['value']
}
});
var option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {
data: labels2
},
xAxis: [
{
splitArea: {
show: true,
areaStyle: {
color: ['rgba(250,250,250,0.3)', 'rgba(200,200,200,0.3)']
}
},
type: 'category',
axisTick: {
show: false
},
data: labels
}
],
yAxis: [
{
type: 'value'
}
],
series: series
};
myChart.setOption(option);
}
function initContributeOuter2(labels, data) {
var cIndex = Math.min(11, Math.max(3, data.length));
var colors = d3.schemeSpectral[cIndex]
var chartDom = document.getElementById('contributeOuter2');
var myChart = echarts.init(chartDom);
var labels0 = _.pluck(data, 'label')
var series = data.map((x, i) => {
return {
name: x['label'],
type: 'bar',
barGap: '0%',
barCategoryGap: '30%',
itemStyle: {
color: colors[i]
},
emphasis: {
focus: 'series'
},
data: x['value']
}
});
var option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {
data: labels0
},
xAxis: [
{
splitArea: {
show: true,
areaStyle: {
color: ['rgba(250,250,250,0.3)', 'rgba(200,200,200,0.3)']
}
},
type: 'category',
axisTick: {
show: false
},
data: labels
}
],
yAxis: [
{
type: 'value'
}
],
series: series
};
myChart.setOption(option);
}
$('.tableDiv').delegate('table td>a', 'click', function () {
$(this).parent().parent().next().fadeToggle()
})
initSelectCitys($('#option_region'));
window.renderReady = getPolygons().then(() => {
geoCoordMap = _.object(henan.features.map(x => [x.properties.name, x.properties.center]))
getRegions().then(function () {
refreshAll();
});
});
$('#option_submit').click(function () {
refreshAll();
})
function mockData0() {
list_city = _.sample(citys, 13).map(x => {
return {
'name': x['name'] + '市',
'values': _.object(sources, _.map(sources, x => {
return +(_.random(1, 200) / 100).toFixed(1);
}))
}
});
d3.sum(_.flatten(list_city.map(x => Object.values(x.values))));
var provinces = [{
'name': '河北省'
}, {
'name': '山西省'
}];
var list_province = provinces.map(x => {
return {
'name': x['name'],
'values': _.object(sources, _.map(sources, x => {
return +(_.random(1, 200) / 100).toFixed(1);
}))
}
});
var list_province = list_province.map(x => {
return {
'name': x['name'],
'values': {
...{
'total': +d3.sum(_.values(x['values'])).toFixed(1)
},
...x.values
}
}
});
var total_henan = _.object(sources, sources.map(y => +d3.sum(list_city.map(x => x.values[y])).toFixed(1)));
list_province.push({
'name': '河南省',
'values': total_henan
})
list_city = list_city.map(x => {
var total = +d3.sum(_.values(x['values'])).toFixed(1);
return {
'name': x['name'],
'values': {
...{
'total': total,
},
...x.values
},
'total': total
}
});
list_city = _.sortBy(list_city, 'total');
var total_province = list_province.map(x => {
return {
'name': x['name'],
'value': +d3.sum(_.values(x['values'])).toFixed(1)
}
});
var total_type = sources.map(x => {
return {
'name': dictSource[x],
'value': +d3.sum(_.map(list_province, y => y.values[x])).toFixed(1)
}
})
return {
list_city,
list_province,
total_province,
total_type
}
}
function refreshAll() {
window.city = $('#option_region').val() + '市';
$.get(`/static/mock/get_contribute_sources.json`, {
city: city,
factor: $('#option_factor').val(),
runAt: $('#option_date').val(),
predictAt: $('#option_date2').val(),
hType:$('[name=radioHtype]:checked').val()
}, function () {
var result = mockData0();
var list_city = result['list_city'];
var list_province = result['list_province'];
var total_province = result['total_province'];
var total_type = result['total_type'];
initBar(list_city, city);
initMap(list_city, city, sources);
initProvincePie(total_province);
initTypePie(total_type);
initTable(list_city, _.reject(list_province, {
'name': '河南省'
}));
refreshOptions();
$('[data-site=' + city + ']').addClass('highlight');
});
}
})
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。