这一阵子被武汉肺炎搞得完全不敢出门,说好的要去重庆看某人也只能暂时搁置了[手动狗头]。不过身为程序员还是停不住折腾,就画个疫情地图吧。
当然我们没有一手数据,就先从腾讯这拿了,通过抓包可以知道,疫情地图的数据来自以下 URL:
$ curl 'https://view.inews.qq.com/g2/getOnsInfo?name=wuwei_ww_area_counts&callback=&_='
在 URL 最后跟上一个 13 位的时间戳就好了。
那么如何把这些数据变成地图来呈现呢,下面我们就来简单的做个项目吧
一、建立 Ktor 项目
如果你使用我以前写的 KtGen 来生成项目,是一点都不麻烦的,完事后可以将 application.conf
的内容改成以下,因为我们这个项目不需要 https 部署,也不需要数据库:
ktor {
deployment {
port = 80
port = ${?PORT}
}
application {
modules = [ com.rarnu.ncov.ApplicationKt.module ]
}
}
接着就可以直接把项目编译通过:
$ gradle build
二、获取疫情数据
这个也很简单了,上面已经给出了相关的 URL,我们只需要简单的请求,取回数据后进行包装即可:
private val dataUrl: String get() = "https://view.inews.qq.com/g2/getOnsInfo?name=wuwei_ww_area_counts&callback=&_=${System.currentTimeMillis()}"
get("/map") {
call.respondText {
try {
JSONArray(JSONObject(HttpClient().get(dataUrl)).optString("data")).filter { country ->
(country as JSONObject).optString("country") == "中国"
}.groupBy { area ->
(area as JSONObject).optString("area")
}.mapValues { confirm ->
confirm.value.sumBy { item ->
(item as JSONObject).optInt("confirm", 0)
}
}.stringIntToJson()
} catch (th: Throwable) {
"[]"
}
}
}
可能有一些同学对这种写法比较陌生,稍做解释:
// 请求获得疫情数据,解析 JSON 后获取其中的 data 数据,并再次解析为一个 JSONArray
JSONArray(JSONObject(HttpClient().get(dataUrl)).optString("data")).filter { country ->
// 过滤出中国的数据,最终得到 List
(country as JSONObject).optString("country") == "中国"
}.groupBy { area ->
// 按照区域分组,最终得到 Map>
(area as JSONObject).optString("area")
}.mapValues { confirm ->
// 对每个分组里的经确诊的感染人数进行求和,最终得到 Map
confirm.value.sumBy { item ->
(item as JSONObject).optInt("confirm", 0)
}
}.stringIntToJson()
最后一步将 Map
转换为 Json 字符串,用于返回给用户,转换函数如下:
fun Map.stringIntToJson() = """[${toList().joinToString(",") { """{"name":"${it.first}","value":${it.second}}""" }}]"""
现在把项目跑起来就可以在浏览器里获取到数据了:
$ gradle run
在浏览器里请求 http://0.0.0.0/map
就可以得到以下数据了:
[
{"name":"湖北","value":4523},
{"name":"广东","value":354},
{"name":"浙江","value":428},
{"name":"重庆","value":180},
{"name":"湖南","value":277},
{"name":"安徽","value":200},
{"name":"北京","value":114},
{"name":"上海","value":112},
{"name":"河南","value":278},
{"name":"四川","value":142},
{"name":"山东","value":158},
{"name":"广西","value":78},
{"name":"江西","value":168},
{"name":"福建","value":101},
{"name":"江苏","value":129},
{"name":"海南","value":46},
{"name":"辽宁","value":41},
{"name":"陕西","value":63},
{"name":"云南","value":70},
{"name":"天津","value":29},
{"name":"黑龙江","value":43},
{"name":"河北","value":65},
{"name":"山西","value":35},
{"name":"香港","value":10},
{"name":"贵州","value":11},
{"name":"吉林","value":9},
{"name":"甘肃","value":26},
{"name":"宁夏","value":12},
{"name":"台湾","value":9},
{"name":"新疆","value":14},
{"name":"澳门","value":7},
{"name":"内蒙古","value":18},
{"name":"青海","value":6},
{"name":"西藏","value":1}
]
三、数据可视化
只拿到 Json 还是太原始了,我们得把中国地图画出来,这里我选用 echarts.js
来实现,同时也已经有开源的 china.js
可供使用,所以这项工作就变得非常简单了。
首先完成一个页面,最简单的就好:
然后写一点 js 就完事了:
function showMap() {
$.ajax({
url: '/map',
dataType: 'json',
success: (res) => {
let optionMap = {
backgroundColor: '#FFFFFF',
title: {
text: '全国疫情数据',
x:'center'
},
tooltip: { trigger: 'item'},
visualMap: {
show: true,
x: 'right',
y: 'center',
splitList: [{start: 1000},{start: 500, end: 999},{start: 100, end: 499},{start: 10, end: 99},{start: 1, end: 9},{start: 0, end: 0}],
color: ['#7D0000','#D52F30','#F4664C','#FFA477','#FFD5C0','#FFF1D5']
},
series: [{
name: '确诊人数',
type: 'map',
mapType: 'china',
roam: false,
label: {
normal: { show: true},
emphasis: { show: false}
},
data:res
}]
};
let chart = echarts.init(document.getElementById('map'));
chart.setOption(optionMap);
}
});
}
好了,现在运行项目就可以看到页面啦:
四、每日疫情数折线图
同样的,再写一个接口用于获取数据:
private val dailyUrl: String get() = "https://view.inews.qq.com/g2/getOnsInfo?name=wuwei_ww_cn_day_counts&callback=&_=${System.currentTimeMillis()}"
get("/daily") {
call.respondText {
try {
val mDate = mutableListOf()
val mConfirm = mutableListOf()
val mSuspect = mutableListOf()
val mDead = mutableListOf()
val mHeal = mutableListOf()
// 请求获得每日情况数据,解析 JSON 后获取其中的 data 数据,并再次解析为一个 JSONArray
JSONArray(JSONObject(HttpClient().get(dailyUrl)).optString("data")).sortedBy { item ->
// 按日期进行排序,最终得到 List
(item as JSONObject).getString("date")
}.forEach { item ->
// 将数据填到列表里
with(item as JSONObject) {
mDate.add(getString("date").trim())
mConfirm.add(getString("confirm").trim().toInt())
mSuspect.add(getString("suspect").trim().toInt())
mDead.add(getString("dead").trim().toInt())
mHeal.add(getString("heal").trim().toInt())
}
}
// 将数据拼装成 json 返回
"""{"date":${mDate.stringListToJson()},"confirm":${mConfirm.intListToJson()},"suspect":${mSuspect.intListToJson()},"dead":${mDead.intListToJson()},"heal":${mHeal.intListToJson()}}"""
} catch (th: Throwable) {
"""{"date":[],"confirm":[],"suspect":[],"dead":[],"heal":[]}"""
}
}
}
同样的,前端依然用 echarts.js
来制作图表:
function showDaily() {
$.ajax({
url: '/daily',
dataType: 'json',
success: (res) => {
let optionMap = {
tooltip: {trigger: 'axis' },
legend: { data: ['确诊','疑似','死亡','治愈'] },
xAxis: [{
type: 'category',
boundaryGap: false,
data: res.date
}],
yAxis: [{type : 'value'}],
series: [
{name: '确诊',type: 'line',data: res.confirm,color: '#D52F30'},
{name: '疑似',type: 'line',data: res.suspect,color: '#FFA477'},
{name: '死亡',type: 'line',data: res.dead,color: '#848586'},
{name: '治愈',type: 'line',data: res.heal,color: '#64CC98'}
]
};
let chart = echarts.init(document.getElementById('daily'));
chart.setOption(optionMap);
}
});
}
完成后效果如下所示:
五、各城市数据列表
同样的,这个列表也来自于上面的 dataUrl
,发起请 求并获取数据即可,不同的地方在于地区下面要有城市列表,并且展示每个城市(区)所对应的数据。我们可以简单的予以处理:
get("/detail") {
call.respondText {
try {
// 请求获得疫情数据,解析 JSON 后获取其中的 data 数据,并再次解析为一个 JSONArray
JSONArray(JSONObject(HttpClient().get(dataUrl)).optString("data")).filter { country ->
// 过滤出中国的数据,最终得到 List
(country as JSONObject).optString("country") == "中国"
}.groupBy { area ->
// 按照区域分组,最终得到 Map>
(area as JSONObject).optString("area")
}.mapKeys { item ->
// 更改 map key,将 key 改为一个包含了求和后数据的 Json,最终得到 Map>
val sumConfirm = item.value.sumBy { i -> (i as JSONObject).optInt("confirm", 0) }
val sumDead = item.value.sumBy { i -> (i as JSONObject).optInt("dead", 0) }
val sumHeal = item.value.sumBy { i -> (i as JSONObject).optInt("heal", 0) }
DataArea(item.key, sumConfirm, sumDead, sumHeal)
}.mapValues { item ->
// 对 area 下属城市,按确诊人数进行逆序排序
item.value.sortedByDescending { i -> (i as JSONObject).optInt("confirm") }.map { i ->
// 更改 map value,将 value 改为一个包含了 area 对应下属城市的 List,最终得到 Map>
with(i as JSONObject) {
DataCity(getString("city"), getInt("confirm"), getInt("dead"), getInt("heal"))
}
}
}.dataToJson()
} catch (th: Throwable) {
"[]"
}
}
}
其中 dataToJson
扩展的代码如下:
fun List.cityToJson() = """[${joinToString(",") { """{"city":"${it.city}","confirm":${it.confirm},"dead":${it.dead},"heal":${it.heal}}""" }}]"""
fun Map>.dataToJson() = """[${toList().joinToString(",") { """{"area":"${it.first.area}","confirm":${it.first.confirm},"dead":${it.first.dead},"heal":${it.first.heal},"cities":${it.second.cityToJson()}}""" }}]"""
最后,同样用 js 写出一个获取数据并包装出 UI 的函数,此处不再赘述。
最终实现的效果如下:
最后我们只需要把页面拼成一个就结束了,当然为了保险起见,避免太多次请求,在真实项目里是需要做缓存的。
我在这里提供完整项目供大家下载参考,请移步去 Github/rarnu/nCoVMap 啦!