(从0到1非常详细步骤)flask+ajax+echarts 53万条招聘信息可视化系统的实现
本篇blog默认:您已经入门echarts、html、js、json、python,知道一些大概的东西,但是具体细节部分需要上网查找资料解决。如果您满足上述条件,那么这篇博客是适合您的
由于编码时间限制(3.5天,从零开始),代码写得比较匆忙,本篇blog涉及的代码可能存在很多的不严谨,有问题的地方还请大家不吝赐教。
由于是类似手把手的流程,故该blog可能会异常冗杂、篇幅很长,还请大家谅解。
如果您还未入门echarts,建议学习“pink老师”的echarts数据可视化教学视频,非常好非常详细,老师也非常逗。pink老师在B站有账号,可以直接到B站搜索查看
交互提示:
- 最下面的分布柱状图和右上角的平行坐标系无内对外交互
- 地图无外对内交互
- 词云既有内对外交互,也有外对内交互
地图:
- 原始地图可以点击到县,但是在此例子中数据的最细坐标仅达到了市、区,因此限制地图点击到市、区;
- 散点标识该市、地区的岗位数量;
- 当点击到市、区时,右下角词云刷新,显示该地区的公司一览;
- 左上角“返回”按钮可以使地图回退到上一级;
词云:
- 词云显示的内容:公司 & 公司的介绍
- 词云交互方式:点击左上角标题、点击词云内部的内容
- 如果点击的是公司(第一级词云),则进入对该公司的词云介绍,最下面的分布柱状图刷新,显示该公司的岗位在不同地区的分布
- 进入公司的介绍(第二级词云)后,点击词云标题(此时是公司名称)即可返回上一级
- 公司介绍(第二级词云)中点击职位或岗位(点击其他标签则无反应),右上角平行坐标系刷新,显示该职位或岗位的相关信息的
0. 前言
《数据可视化》上课期间,课程大作业一共有5个课题,此课题为其中之一,特点是数据量大,有53万条。但是每一项数据的含义都很浅层,单条数据处理起来也相对简单。
这个系统的flask后台目前还未实现多线程,再加上数据量有53万条,因此卡顿现象严重(每一次操作都要等待几秒钟,不过在加入python多线程后,卡顿问题会得到解决,不过目前因时间原因,还没有加上)
一、数据处理
1.1 数据预览
我们首先预览一下数据,利用python中的pandas库读取原始数据 “附件1:招聘信息.csv”(115MB),使用工具jupyter notebook。
(我是在课上用ipad air3远程连接寝室的电脑上运行的jupyter notebook进行的数据分析)
其实用什么工具无所谓,用python自带的IDE都能做数据分析,不过就是涉及到我们自己想用什么的问题了罢,我这里用jupyter notebook并不是想显得我多高大上什么的,主要是太好用了啊555我能怎么办。。。关于jupyter,以及如何配置jupyter使其能够远程在手机平板*问,我会专门写一篇博客介绍,敬请期待(平板上也能测试代码,import各种包哦,很香的)
# 1.初步预览前9条数据
file_path = './课题1/附件1:招聘信息.csv'
file = open(file_path, 'r')
# 打印前9条数据
for i in range(10):
print(file.readline())
file.seek(0)
file.readline()
data_num = 0
for i in file:
data_num+=1
print('数据总量:', data_num)
以下是输出:
"City","CompanyName","CompanyShortName","CompanySize","CreateTime","Education","FinanceStage","IndustryField","JobNature","PositionAdvantage","PositionFirstType","PositionName","PositionType","Salary","WorkYear","CompanyId","PositionId"
"北京","虫二公舍","北京虫二公舍网络科技有限公司","15-50人","2016-03-03 18:40:39","本科","初创型(天使轮)","移动互联网","全职","高额的年终奖金+团队项目股权","技术","Android","移动开发","10k-20k","3-5年"," 1"," 1"
"杭州","阿里巴巴","阿里巴巴(中国)网络技术有限公司","2000人以上","2016-03-03 18:40:32","本科","上市公司","移动互联网","全职","股票期权 五险一金 扁平空间 技术大牛","技术","前端工程师","前端开发","20k-40k","3-5年"," 2"," 2"
"广州","亿航智能无人机Ehang","广州亿航智能技术有限公司","150-500人","2016-03-03 18:40:30","大专","成长型(B轮)","移动互联网 · 硬件","全职","双休、五险一金、美味午餐、零食任食....","职能","资深出纳","财务","6k-10k","5-10年"," 3"," 3"
"广州","亿航智能无人机Ehang","广州亿航智能技术有限公司","150-500人","2016/1/4 19:47","大专","成长型(B轮)","移动互联网 · 硬件","全职","双休、五险一金、美味午餐、零食任食....","职能","资深出纳","财务","6k-10k","5-10年"," 3"," 3"
"广州","亿航智能无人机Ehang","广州亿航智能技术有限公司","150-500人","2016-03-03 18:40:18","大专","成长型(B轮)","移动互联网 · 硬件","全职","双休、五险一金、美味午餐、零食任食....","技术","射频通信工程师","后端开发","8k-12k","不限"," 3"," 4"
"广州","亿航智能无人机Ehang","广州亿航智能技术有限公司","150-500人","2016/1/4 19:47","大专","成长型(B轮)","移动互联网 · 硬件","全职","双休、五险一金、美味午餐、零食任食....","技术","射频通信工程师","后端开发","8k-12k","不限"," 3"," 4"
"北京","尚德机构","北京尚佳崇业教育科技有限公司","2000人以上","2016-03-03 18:40:00","大专","成熟型(C轮)","电子商务 · 教育","全职","意向客户资源 做五休二 带薪培训 免费学习","市场与销售","高薪课程咨询师(六险+均薪7k+半年晋升+带薪年假)","销售","8k-12k","不限"," 4"," 5"
"北京","一网天行","北京一网天行科技有限公司","15-50人","2016-03-03 18:39:31","学历不限","初创型(不需要融资)","企业服务","全职","技术培训,节日福利, 员工旅游,项目奖励","技术","PHP中高级开发工程师(中高级)","后端开发","8k-16k","3-5年"," 5"," 6"
"上海","mo9","上海佰晟通信息科技有限公司","50-150人","2016-03-03 18:39:27","本科","成熟型(C轮)","金融 · 移动互联网","全职","晋升空间,c轮融资,股权,奖金","技术","MySQL数据库工程师(DBA)","dba","15k-25k","3-5年"," 6"," 7"
数据总量: 530444
1.2 数据获取
上一步预览数据,我们只是用readlines()打印了前几条数据,初步看了一下数据,大致了解了数据的格式后,接下来我们要将读出来的每一行line进行分解打包,转换为我们熟悉的list,之后再通过dict转化为更加方便操作的DataFrame。
# 2. 数据获取
# 输出数据的标签,保存到label(list)中
label = []
file.seek(0) # 回到文档刚开始的位置
temp = file.readline().split(',')
for item in temp:
label.append(item.split('"')[1])
print(label)
# 获取所有数据,封装成dict
dict_temp = {}
line = []
line_temp = []
for i in range(data_num):
line.append([])
line_temp = file.readline().split(",")#标签分开
if i == 508975:
print(line_temp)
#break
string_temp = ''
for j in line_temp:
try:
#标签内部去掉空格和 "字符
string_temp = j.split('"')[1]
if string_temp == '':
string_temp = j.split('"')[0]
string_temp = string_temp.split(" ")[-1]
if string_temp == '':
string_temp = j.split(" ")[0]
line[i].append(string_temp)
except:
line[i].append(j)
print('处理完成')
# 预览后5条数据
for i in range(-6, -1):
print(line[i])
以下是输出:
['City', 'CompanyName', 'CompanyShortName', 'CompanySize', 'CreateTime', 'Education', 'FinanceStage', 'IndustryField', 'JobNature', 'PositionAdvantage', 'PositionFirstType', 'PositionName', 'PositionType', 'Salary', 'WorkYear', 'CompanyId', 'PositionId']
['"北京"', '"微博英才"', '"微博英才(北京)科技发展有限公司"', '"50-150人"', '"2016/1/14 14:14"', '"大专"', '"成长型(A轮)"', '"其他"', '"全职"', '"五险一金,7天年假 午餐\\交通补助', '', '旅行"', '"技术"', '"web前端"', '"前端开发"', '"15k-26k"', '"3-5年"', '"30005"', '"381159"\n']
处理完成
['广州', '韩后电商', '广州市韩后电子商务有限公司', '150-500人', '11:18', '学历不限', '成长型(A轮)', '电子商务', '全职', '节日福利', '职能', '招聘经理/主管', '人力资源', '6k-8k', '3-5年', '3193', '402622']
['北京', '火娃科技', '上海火娃网络科技有限公司', '50-150人', '11:18', '学历不限', '初创型(天使轮)', '移动互联网', '兼职', '发展前景广阔', '设计', '兼职摄影师\\探店达人', '视觉设计', '1k-2k', '不限', '26948', '402623']
['北京', '中数创新', '北京中数创新技术有限公司', '50-150人', '11:18', '大专', '初创型(未融资)', '企业服务', '全职', '员工旅游', '市场与销售', '软件销售', '销售', '5k-8k', '不限', '698', '402624']
['深圳', '无何有', '深圳市无何有网络科技有限公司', '15-50人', '11:18', '大专', '初创型(天使轮)', '移动互联网', '全职', '+旅游+双休', '设计', '2D插画师', '视觉设计', '5k-10k', '1-3年', '2520', '402625']
['北京', '阿里云', '北京阿里巴巴云计算技术有限公司', '2000人以上', '11:18', '本科', '成熟型(D轮及以上)', '文化娱乐', '全职', '来,一起站在云计算爆发的潮头!', '市场与销售', '市场推广', '市场/营销', '10k-20k', '1-3年', '2918', '402626']
可以看到,处理出来的list还算不错,第一次打印的第508975条数据是未经过“毛刺字符”处理的list,后面的5条数据是经过内部元素处理之后的,处理效果还算不错,但是由于数据量较大,不能保证每一条数据都是像后面5条数据那样的完美。
实际上,进过上述处理后,还是有不完美的数据存在,只不过在之后的后台处理程序中将之忽略了,这是数据处理算法的不足(仅仅是简单的分词),还有待改进。
1.3 数据封装为DataFrame
这一步非常简单,目前我们已经获得了两个变量:数据标签label和每一行数据line ,我们先定义一个temp_dict,用作list和标签label转到DataFrame的中间过渡变量,然后直接将temp_dict用作定义新DataFrame函数的参数,即可。
# 3.数据封装成DataFrame
#dict键值初始化
for i in label:
dict_temp[i] = []
for count in range(data_num):
for l, i in zip(label, range(len(label))):
dict_temp[l].append(line[count][i])
#print(dict_temp)
import pandas as pd
frame_test = pd.DataFrame(dict_temp)
frame_test
jupyter notebook中的打印:
1.4 总结
该数据处理过程极为简单,因此成品中会有数据和标签错位的现象存在,为了实现更加严谨的系统,此数据处理算法还需要改进。
二、静态网页
我们在任意一个位置创建目录css、js、img、json,在相对根目录下创建一个index.html,开始制作静态网页。
注意:我使用的是VSCode,如果就这样用Chrome打开index网页,后期在js代码中引用文件时会报错,因此使用火狐Firfox浏览器进行本项目的Debug和最终展示(报错原因:浏览器无权调用本地文件,目前暂未找到本质上的解决方法),但是在不涉及在js代码中引入其他文件的前期调试阶段我们使用edge浏览器。
2.1 VSCode插件准备
VSCode插件名称 | 作用 |
---|---|
Easy LESS | 用于更方便地编写CSS,这个插件可以自动将.less文件编译为.css文件,引用此插件的目的是为了css内容的美观整洁。 |
cssrem | 用于将px单位转化为rem单位,用于网页的动态大小变化,而不是用px把网页元素大小定死了(在插件设置中将Root Font Size选项改为了80,意思是将网页整齐地分割成80份) |
2.2 网页布局
一共四个图,左上角地图,右上角平行坐标系,右下角词云,最下面是柱状分布图,用这个思路进行布局。
在css文件夹下创建一个index.less文件进行css的编写,由于安装了EasyLESS插件,ctrl+s的时候vscode会自动将less文件转化为css文件。
(记得将编译好的css引进来)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>hello</title>
<link rel="stylesheet" href="CSS/index.css" />
</head>
<body>
<div class='left'></div>
<div class='right_up'></div>
<div class='right_bottom'></div>
<div class='bottom'></div>
</body>
</html>
.less内容:
.left{
top:8%;
position:absolute;
float: left;
margin-left: 5px;
height: 8.7rem;
width: 11rem;
//margin-left: 0.2rem;
background-color: antiquewhite;
border: #1B6D85 dotted;
}
.bottom{
position: absolute;
bottom: 0.5%;
width: 23.65rem;
height: 1.2rem;
background-color: rgb(34, 193, 233);
border: #1B6D85 dotted;
}
.right_bottom{
position: absolute;
right: 1%;
bottom: 13%;
width: 12rem;
height: 4rem;
background-color: rgb(184, 193, 233);
border: #1B6D85 dotted;
}
.right_up{
position: absolute;
top:8%;
right: 1%;
width: 12rem;
height: 4.5rem;
background-color: rgb(34, 193, 233);
border: #1B6D85 dotted;
}
发现网页并没有自适应,原来是忘了引进一个js文件,名为flexible,我们将flexible引进:
(各种文件在文章末尾以网盘形链接式提供)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>hello</title>
<link rel="stylesheet" href="CSS/index.css" />
</head>
<body>
<div class='left'></div>
<div class='right_up'></div>
<div class='right_bottom'></div>
<div class='bottom'></div>
<script src='./js/flexible.js'></script>
</body>
</html>
再刷新网页,成功!
2.3 地图的初步引进
2.3.1 素材寻找
既然要使用echarts了,那么就要引进echarts.min.js文件,我们将该文件引入:(位置紧接上一个引入文件代码行)
<script src='./js/echarts.min.js'></script>
<script src="./js/jquery-3.5.1.min.js"></script>
我们的地图功能是要点击到市,而不只是一个全国地图就完了,那怎么办呢???去社区啊!!对于我们这种小白,echarts社区赛高!!
我们打开Chrome,进入社区https://www.makeapie.com/,找到了一个可点击到县的地图https://www.makeapie.com/editor.html?c=x0-uCepnPl*(感谢大佬提供的例子,感谢感谢!)*,我们在js目录下创建一个我们自己的js文件,名为map.js,地图相关的js代码就放这里面了。
2.3.2 融入我们自己的网页
- js文件:拿到地图源码后,咱们引入这个地图,在js目录下创建一个map.js的文件,将源码全部直接粘贴到这个js文件中。
- 绑定容器:使用echarts的知识,将地图和left这个容器绑定起来,测试一下能否成功显示。在初始化echarts图表的函数前面添加一句代码,如下
js > map.js > initEchartMap()
//渲染echarts
function initEchartMap(mapData, sum, pointData) {
var myChart = echarts.init(document.querySelector('.left'))
......
}
- 在网页中Debug测试,发现图表并没有预期出现,F12查看控制台,发现报错:
2.3.3 问题解决
根据控制台输出,这个问题是由于没有定义方法AMapUI()。
分析得到,此类问题大多是作者隐含了另一个js文件,并没有在这个地图的js代码中给出,我们仅仅将地图的核心代码拿过来了,但是隐含的代码还没有,因此需要在网页中找到该js代码。
我们在Chrome里面F12继续走起,进入“Network”标签,刷新一下原网页,筛选js文件,慢慢找下面的js文件。
最终在一个名为main.js的文件中找到了该函数的定义,毫不留情,在preview窗口中ctrl+A+C全部复制过来,我们自己创建这个文件,将以下代码复制进去,再在html中引入(注意引入顺序,要在map.js文件的前面,因为map.js要调用里面的AMapUI函数)。
<script src='./js/flexible.js'></script>
<script src='./js/echarts.min.js'></script>
<script src="./js/jquery-3.5.1.min.js"></script>
<script src="./js/main.js"></script>
<script src="./js/map.js"></script>
引入之后我们再尝试一下,发现多了一项报错:
提示我们引入高德地图的api,我通过资料查找到了高德地图的引入方法,不过具体的引入方法请自行搜索查找。
在高德官网申请到了key后,我们在html中加入这样一段代码:
(放在div定义后面、js文件引入前面)
(注意:由于是在线调用api,因此电脑需要连网,否则地图无法显示)
<div class='left'></div>
<div class='right_up'></div>
<div class='right_bottom'></div>
<div class='bottom'></div>
<!-- 引入高德API AMap-->
<script src="https://webapi.amap.com/maps?v=1.4.15&key= 你申请的高德API"></script>
<script type="text/javascript">
var map = new AMap.Map('container', {
center: [117.000923, 36.675807],
zoom: 11
});
AMap.plugin('AMap.ToolBar', function () {
var toolbar = new AMap.ToolBar();
map.addControl(toolbar)
})
</script>
<script src='./js/flexible.js'></script>
<script src='./js/echarts.min.js'></script>
<script src="./js/jquery-3.5.1.min.js"></script>
<script src="./js/main.js"></script>
<script src="./js/map.js"></script>
至此,地图成功显示:
(连网!连网!连网!重要的事情说三遍,我最终答辩的时候忘了连网,场面你懂得,尴尬的一批!)
2.3.4 “返回”哪去了?
当我们满心欢喜点击地图查看省市的时候,想退回上一级的时候发现没办法回退,这个例子的缺陷?并不是,原例子左上角明显有返回按钮,在社区那个地图预览界面,左上角有个“返回”按钮。
什么原因造成了返回按钮不见呢?源代码是在js中添加的div容器,初步推测,是因为我们绑定了容器,导致myChart在赋予option后将“返回”按钮给遮盖住了,因此,我们需要修改一下,我们将map.js中关于“返回”按钮的声明代码注释掉,移动到html中
<div class="back">返 回</div>
可以看到,返回出现并可以使用了,但是位置好像不太对,因此这里需要修改它的css,在map.js文件中修改,自行修改距离左边、上面的距离。(可以使用rem单位,以让网页动态适配)
成功显示,并且可以交互,快去试试吧:
2.3.5 修改地图
我们知道,目前这个地图仅仅引用过来了,下面的时间轴、右边的柱状图是我们不需要的,进入map.js,进入函数initEchartMap(mapData, sum, pointData)中,修改echarts的设置option。
- 去除时间线TimeLine:
- option -> timeline:{…} timeline整个注释或直接删除掉;(包括timeline字母还有大括号)
- 去除柱状图bar:
- option -> baseOption -> xAxis 注释或删除掉;(包括xAxis 字母还有大括号)
- option -> baseOption -> yAxis 注释或删除掉;(包括yAxis 字母还有大括号)
- option -> baseOption -> series -> {type:‘bar’, …},最后一个配置项带"bar"的,注释或直接删除掉。(包括大括号)
- 修改标题,将第二个标题删除
option -> baseOption -> title,可以看到这里镶嵌了一个字典数组,将数组中最后一个元素注释或删除掉,即可
- 取消右上角的图片保存按钮
option -> baseOption -> toolbox,将之注释或删除掉。
经过以上步骤,我们就得到了一个相对干净的地图,如下图:
2.3.5 地图的微调
- 修改初始化缩放大小
我们每次刷新网页,地图初始化后,总感觉这个初始的地图好像有点小,没有充分利用空间,那怎么办呢?
进入option -> baseOption -> geo,zoom参数即为默认缩放大小,源代码是1.1,我们改为1.5。
改完后刷新网页,地图是放大了,但是发现地图的头跑到外面去了,右边也有点空,因此我们修改两个参数,left参数直接注释掉,top从15%改为20%,再刷新网页,效果不错。
- 修改标题
进入option -> baseOption -> title,text参数即为我们要修改的标题,暂时将原来的那个注释保留,我们在下面新添加一行*(别忘了后面的逗号)*,地图的标题暂时取“全国岗位供应地图”,效果如下:
不过那个“销售额”有点抢眼。。。
- 修改鼠标悬停的字符提示
进入option -> baseOption -> series -> 第一个元素 -> tooltip,自己找一下,看懂格式就知道怎么改了。(其实主要是因为我懒)
效果如下:
为什么有小数???这个例子的源数据里面本来就有小数,那怎么格式化为整形呢?还是修改字符那里,自己摸索摸索吧,我又懒了,伸个懒腰先。。。。啊 ~ 好舒服
好吧,我估计你们要打我了,我说还不行吗,val.toFixed(2) 改为val.toFixed(0)就可以了
Perfect!!
2.3.6 地图小结
我们作为初学者,不可能自己凭空写出echarts图表,先从网上找案例,借鉴复制一下,成功移植过来后,对代码进行研究,研究过程中进行反复地修改试错,验证自己的想法,从此弄懂某一行代码的含义,从而学习到新知识并将其巩固,成为自己知识体系中的一环。
(毕竟刚开始学习,我们啥也不知道,我们虽然站在巨人的肩膀上看世界,我们仍然需要慢慢向下生长,当我们能够双脚着地,我们又成为了另一个巨人)
本次引进地图过程中的知识小结:
-
调用高德API;
-
修改tooltip中的显示格式和内容;
-
添加与删除时间线timeline;
-
添加与删除echarts中的多图表(这次例子是删除地图中的柱状图);
-
地图一些参数的微调,比如地图板块的缩放、地图板块距离容器上部的距离等等;
-
修改echarts图表中的标题title;
2.4 词云的初步引进
同样的,我们在官网或者社区中找词云的例子,ctrl + C + V,读书人的事情那叫偷吗?那叫借鉴。(斜眼笑)
为了做到模块化,我们新定义一个js文件,名为wordCloud.js,用于存放以上的代码(已经经过融合),不要忘了在html里面引用这个js文件。
其实方法都差不多,这里我使用的是我们组里的一位同学提供的词云,代码如下:
需要注意的是,该段代码的第11行,意为将img目录下的WorldCloudShape.png图片文件指定成该词云的遮罩,不同的遮罩可以使词云的形状不同。遮罩怎么制作呢?在文件中我会提供这个文件,大家看一眼就知道怎么做了(使用PS,十几秒钟完事)。
//参数解释:
//presents:要显示的文字元素,数组
//presents_value:每一个元素的大小,数组,数组长度与文字元素数组长度相同
//title:词云的标题
function worder(presents, presents_value, title) {
myChart = echarts.init(document.querySelector(".right_bottom"));
var W = $('.right_bottom').width(),
H = $('.right_bottom').height();
var treeUrl = '../img/WorldCloudShape.png';
const colorList = [
'#3a96f5',
'#5faaf7',
'#78bafe',
'#1563f2',
'#add2f8',
'#e24bf4',
'#1acaff',
'#ffde00',
'#89fda5'
];
var data = [];
for (var i = 0; i < presents.length; ++i) {
data.push({
name: presents[i],
value: presents_value[i],
});
}
var maskImage = new Image();
maskImage.onload = function () {
myChart.setOption({
//backgroundColor: '#0A2E5D',
title:{
left: 'top',
top: 0,
text: title,
textStyle: {
color: 'rgb(179, 239, 255)',
fontSize: 18
}
},
tooltip: {
show: false
},
series:
[
{
type: 'wordCloud',
gridSize: 1,
sizeRange: [16, 24],
//symbolSize: ['45.2%', '35.2%'],
rotationRange: [0, 30],
rotationStep: 1,
maskImage: maskImage,
textStyle: {
normal: {
color: function () {
return 'rgb(' + [
Math.round(Math.random() * 160)+80,
Math.round(Math.random() * 160)+80,
Math.round(Math.random() * 160)+80
].join(',') + ')';
}
},
emphasis: {
shadowBlur: 8,
shadowColor: '#8AD2FA'
}
},
width: W,
height: H,
data: data
}
],
}
)
};
maskImage.src = treeUrl;
myChart.off('click'); //关闭点击事件
window.addEventListener("resize", function () {
myChart.resize();
});
}
//调用词云函数
worder(['你好', 'hello', 'hi'], [1,2,3], '测试标题');
好了,让我们去网页中看看词云出来没有。
2.4.1 小失误
词云并没有出来!我们老套路,按F12看控制台输出,哦,原来是忘了引进一个js文件!echarts词云要用一个专门的js文件,名为echarts.wordcloud.min.js,这个文件我同样会在文件合集中给出,我们在html中再将这个文件引入,引入位置在wordCloud.js引入之前
<script src='./js/flexible.js'></script>
<script src='./js/echarts.min.js'></script>
<script src="./js/jquery-3.5.1.min.js"></script>
<script src="./js/main.js"></script>
<script src="./js/map.js"></script>
<script src='./js/echarts.wordcloud.min.js'></script>
<script src='./js/wordCloud.js'></script>
2.4.2 VSCode JS文件读取权限问题
引入后我们再看看:
我这里使用的是VSCode,当我使用edge或者chrome时,系统会报错,没有权限调用文件,报错如下图:
如果使用的是HBuilder而不是VSCode,则应该没有这个问题。
我换用FireFox火狐浏览器,js代码中本地文件的读取问题解决了,但是。。。词云还是不显示。。。控制台也没有报错,一番查找后,发现是词云遮罩有问题。
2.4.3 词云遮罩的“坑”
代码的第9行即为指定该词云的遮罩,下图是我自己制作的遮罩,不同的遮罩可以使词云的形状不同。
遮罩怎么制作呢?在文件中我会提供这个文件,大家看一眼就知道怎么做了(使用PS,十几秒钟完事)。
左上角的空白部分是给标题预留的位置,总不能让词云把标题给挡住了吧
我这里使用遮罩的时候出了问题,使用目录形式var treeUrl = ‘…/img/WorldCloudShape.png’ 时词云本身都不显示了,网上的代码使用的是base64编码,因此我去在线把图片转成了base64的编码,然后使用,词云成功再次显示,问题解决。
2.4.4 词云小结
同样,作为初学者,我们还是先从网上找例子,先借鉴学习。
本次词云的学习知识点有:
- 词云函数传参:
- 元素本身(字符串数组)
- 元素要显示的大小(数组)
- 标题(字符串)
- echarts词云参数的格式及其含义;
- 修改词云遮罩;
- 设置echarts图表的背景颜色;
- echarts标题的添加与自定义样式;
- 词云随机颜色的添加(需要echarts版本支持)。
2.5 柱状分布图的初步引进
最初想法是要一个图表来显示这个公司提供的职业在不同地区的分布,所以需要地区名和岗位数量这两个元素,最初的理想图表是类似一个柱状图,但是能够分段分颜色,不同的段有不同的颜色,底部标上地区名和岗位数量。
2.5.1 素材寻找
还是去echarts社区碰碰运气,毕竟咱才刚开始,小白中的小白,刚开始只能寄希望于各位大佬了。运气不错,果然碰到一个几乎满足所有需求的图表https://www.makeapie.com/editor.html?c=xtWKI3jP7Q,名称为单柱。
老规矩,新建一个js文件,这个文件专门存放这个图表。名字什么的可以自定义随便取,我这里就取BarLine.js了,函数名取Bar。
把源代码复制进Bar函数里面,上下两头添加代码:
var myChart = echarts.init(document.querySelector('.bottom')); //指定一个echarts图表对象
/*源代码
......
*/
myChart.setOption(option); //将设置附给这个echarts图表对象
window.addEventListener("resize", function () {//使图表的尺寸能随网页改变而改变
myChart.resize();
});
对了,不要忘了两件事
- html中引用这个js文件(放在最后一个引用即可)
- js文件中运行这个函数Bar(); (不然就是定义了一个Bar函数但是没有调用它)
让我们看看,成功与否:
成功引入!
2.6 平行坐标系的初步引进
与常规图表引入一样,我们先在官方example或者社区中找例子,然后将大神们提供的代码和我们的网页融合起来,限定在一个div容器中。
这里我们就用一个基础的平行坐标系吧, 网址:https://echarts.apache.org/examples/zh/editor.html?c=parallel-simple(官方示例),代码非常简单。引入方式也是和上面一模一样。
//配置项
option = {
parallelAxis: [
{dim: 0, name: 'Price'},
{dim: 1, name: 'Net Weight'},
{dim: 2, name: 'Amount'},
{
dim: 3,
name: 'Score',
type: 'category',
data: ['Excellent', 'Good', 'OK', 'Bad']
}
],
series: {
type: 'parallel',
lineStyle: {
width: 4
},
data: [
[12.99, 100, 82, 'Good'],
[9.99, 80, 77, 'OK'],
[20, 120, 60, 'Excellent']
]
}
};
这样我们就初步引进了平行坐标系。
三、数据的交换 - flask
前端部分已经大致就位了,接下来就是如何实现后端和前端相互交换数据,注意是相互交换数据。
还是要补充一下,由于时间短暂,flask框架也是现学现卖,因此,存在的不合理或者不严谨编码,还请大家多多担待。
如何使用flask实现前后端相互通信?我们先在网页的根目录下创建一个py文件,名为FlaskTest.py(也可先创建txt文件然后改后缀名)。
到时候交换什么样子的数据?字符串?好像有点不对劲。python中的字典和json非常相似,使用json来交换数据似乎非常合适,我们就用json格式来作为数据的标准交换格式。
要使用flask,需要知道的是,它有一个类似于模板的代码,我们只需要往模板里面填充我们自己的元素即可。如下面的代码所示:
# 简单模板
from flask import Flask
from flask import request
from flask import render_template
import json #把字典封装成json
app = Flask(__name__, template_folder='',static_folder="",static_url_path="")
@app.route('/')
def index():
return render_template('index.html')
# 数据交换测试
@app.route('/getDataTest', methods=['GET'])
def GetData():
return json.dumps({"data":1}) #这里将字典转化为了json数据格式之后再发送给网页端
if __name__ == '__main__':
app.run(debug=False, threaded=True)
其中index()函数是用于初始化网页的,我们将网页所在的目录路径以字符串形式绑定在return里面,当我们双击运行这个py文件的时候,打开的网页就是这里绑定好的。
此例中,我们将FlaskTest.py和index.html放在了同一目录下,因此这里只需要填写“index.html”。
定义的GetData函数是用于测试的数据交换函数,其上面的@app.route(…)这里代表的是该函数的引用参数:
- /getDataTest:该函数的“路径”,当前端需要调用后端的函数时,前端需要使用这个“化名”来映射到后端对应的函数,比如,这里 /getDataTest 就是函数 GetData(),只不过前端要调用GetData函数必须要使用化名“/getDataTest ”
- methods:有GET和POST两种,这两种方法都是对于前端而言,而非对于后端,什么意思呢?
- GET意为只调用后端的某一函数,而不传递数据给后端(单向通信)。
- POST意为前端向后端发送一个数据,然后期待后端返回一个数据(双向通信)。
3.1 单向通信 - GET
我们先来说简单的,单向传递数据,我们先在js文件目录下创建一个新的js文件:MessageTranser.js,然后定义一个函数,如下:
function GetDataTest()
{
//ajax获取数据
$.ajax({
url:'/getDataTest',
data:{},
type:'GET',
async:false,
dataType:'json',
success:function(data){
alert("Data: " + data.data + "\nStatus: " + status);
},
error: function(msg){
console.log(msg);
alert('系统错误');
}
})
}
我们调用这个函数(如果你复制过去就不管了,那它也不管你了,记住还要加一行调用该函数的代码),如果成功的话(success代码部分),我们会接到弹窗提示,格式如代码所示,如果错误的话(error代码部分),会进行弹窗报警。(别忘了在html中引入这个文件,引入位置是图表之上,jquery和echarts之后)
还有一件事!这次以及以后,网页的打开方式是双击这个py文件,而不是用浏览器打开html文件。不出意外的话你应该出现这样的画面:(和jupyter有点像有木有)
如果闪退,则极大可能是你的模块包没有安装完全,可以看一下上面哪个模块没有安装,使用pip去安装吧!
假如你已经成功出现上述画面,我们在浏览器中输入这个网址http://127.0.0.1:5000/,进入网页后,你应该看到这样的结果:
说明我们的后端和前端已经成功实现单向通信。(py代码中,返回的就是字典{‘data’:1}封装成了json的数据,你可以试试将1改为字符,或者其他,这里提示都是和后端一样的)
3.2 双向通信 - POST
这里所谓的双向通信(POST方式),实际上是网页页面向后台发送一个数据包,后台可以查看这个数据包,获得一些数据信息。
前面介绍的单向通信(GET方式)是指浏览器不向后台发送数据,直接调用后台代码。
对于GET和POST的解释:都是对于前端网页而言,GET英文意为接收(前端接收数据),POST英文意为发送(前端发送数据)。这两种方式都会有后台发送数据给前端这一环节,不同之处仅仅是前端是否发送数据给后台。
这里我们做一个示范,看看前端发送给后端的信息能否被接收并在后台python的控制台中打印出来。
3.2.1 python后端处理代码
我们在flask框架中写入一个函数PostTest,写入位置在__main__函数前面,import后面,以作为响应函数。代码如下:
@app.route('/postTest', methods=['POST'])
def postTest():
print(request)
return json.dumps({'status':'成功接收'})
注意这段代码,其中的request是import时引入的(from flask import request),以我目前的知识,如果没有这个则python后台无法接收来自前端的信息。
另外注意返回值,意为返回一个json格式的数据,提示前端我们已经接收到了数据。
3.2.2 js前端处理函数
我们在MessageTranser.js文件中写入以下代码:
function PostTest(message)
{
$.ajax({
url:'/postTest',
//要与python中函数头上面的@app.route地址一样
//不是python中的函数名,是地址,python中的函数名可以任意取,不影响的
data:message,
type:'POST',
async:false,
datatype:'json',
success:function(data)
{
alert("Status:" + data.status);
}
error:function(msg)
{
console.log(msg);
alert('系统错误');
}
})
}
PostTest({'city':'北京'})
相对于GET方式,以上函数仅仅是修改了type、data、url这三个地方,而且有传参的动作。
如果接收没有问题的话,后台的控制台输出是会打印“北京”二字的。
我们再次双击运行或者重启这个py文件,刷新一下网页127.0.0.1:5000,可以看到,前端显示成功接收(数据来自后台),后台的控制台中也已经打印出了“北京”二字(数据来自前端),说明后台接收数据成功并且成功返回数据给前端。
至此,我们已经成功引入图表,并且实现了前后端的相互通信。
ps:如果你按照上述方式做了并且准确无误,但是还是遇到问题,网页提示“系统错误”,请检查你使用的浏览器,博主使用的是火狐firefox 84.0.1 64位版本,博主使用edge时就会报错,但是Chrome和FireFox就不会报错。
四、综合处理
我们已经实现了前端的显示、前后端的通信,因此,接下来就是要将后端处理的数据显示在前端上,此部分内容涉及:
- 前后端数据通信
- python数据处理(pandas)
- echart图表的微调
4.1 地图
4.1.1 数量显示
这里地图是需要显示各个省市有多少个岗位,是静态的,为了加快网页的加载,我们就用静态数组来作为数据,当然这个静态数组还是需要我们自己获取。
我的获取方式是用pandas读取xlsx文件(又涉及到数据文件的处理了,忘记了的可以回到第一章)遍历生成“城市-数量”字典,然后匹配到相同的城市的数量加一,字典生成完毕后生成json格式的代码,再复制过来成为js中的字典数组。(我会提供jupyter notebook文件)
//map.js文件中
var jobNum = [
{city:'北京', value:197258},
{city:'杭州', value:41654},
{city:'广州', value:46370},
{city:'上海', value:88408},
{city:'深圳', value:64516},
{city:'武汉', value:9960},
{city:'西安', value:4950},
{city:'福州', value:1984},
{city:'南京', value:8559},
{city:'佛山', value:1303},
{city:'合肥', value:2380},
{city:'天津', value:3683},
{city:'太原', value:358},
{city:'厦门', value:5901},
{city:'长沙', value:4935},
{city:'成都', value:15521},
{city:'重庆', value:3105},
{city:'济南', value:1919},
......
];
现在我们已经获取到了各城市的岗位数量,那么怎么替换掉之前的数量并且在地图上显示出来呢?我们需要找到提供数量信息的这一段代码,我们浏览map.js,可以看到:
这里明摆着“获取数据”,之前的数据应该就是这里生成的,我们找找看,发现其中有个Math.random()*3000,成为了嫌疑代码,我们修改一下,把random去掉,用一个确定的数试试,就3000吧,修改完后刷新网页,再去看看那地图的数量情况:
果然是这里搞的鬼,我们只需要按照地名将对应的数量填进去即可,那么具体如何做呢?我们先把原来的value注释掉,我们自己写。
请注意以下代码中的name,这里不就是我们的城市名称吗,一般来说,城市名称都是两个字以上的,我们在正式赋值前比较一下item的城市名称,如果前两个字相同,则说明这是一个城市,我们就把值附给这个城市,让地图显示出来。
我们让之前的value先初始化为0,遍历一下jobNum(可能有其他更高效的方法,但是这里为了简便还是使用了笨方法),和传参的item里面存的城市名称对比一下,如果前两个字相同,就吧value赋值,这样修改代码后我们再刷新网页看看。
var value = 0;
for (var i=0; i<jobNum.length; i++)
{
if (item.properties.name[0] == jobNum[i].city[0] &&
item.properties.name[1] == jobNum[i].city[1])
{
//console.log(jobNum[i].city, jobNum[i].value)
value = jobNum[i].value;
break;
}
};
mapData.push({
name: item.properties.name,
value: value,
cityCode: item.properties.adcode
})
成功!不过感觉怪怪的,为什么这个小数点又出来了?原来我们鼠标放在黄色点和地图板块上显示的面板是不同的,我们需要再修改一下鼠标放在点上时面板中数量的显示格式,在代码路径initEchartMap -> option -> baseOption -> geo -> tooltip -> formatter,把那个toFixed中的2改为0即可,表示0个小数点,修改完后就正常了。
4.1.2 地图点击下钻深度
当我们满心欢喜地点击地图的时候,会发现这样一个BUG,点击到县或者区的时候岗位数量为0,如下图:
再往下也还是0,这是因为我们的数据本身就没有县及以下的统计,但是地图他支持点击到县及以下地区,因此我们需要做些修改,只让点击到省,省以下的直接忽略,那么怎么做呢?
我们观察到源代码中的点击事件代码部分,“如果当前是最后一级,那就直接return”,这里与我们的目标相关,我们想要的就是这样的效果,只不过将条件从“最后一级”改到了“忽略点击市、区”
原代码:
//echarts点击事件
function echartsMapClick(params) {
if (!params.data) {
return
}
else {
//如果当前是最后一级,那就直接return
if (parentInfo[parentInfo.length - 1].code == params.data.cityCode) {
return
}
let data = params.data
parentInfo.push({
cityName: data.name,
code: data.cityCode
})
init(data.cityCode)
}
}
观察上面的原代码细节部分,它判断不是最后一级后直接就更新地图了,我们再套一个if,在更新前再判断一下是不是市、区。如果不是市、区再更新地图也不妨,思路清晰后我们这样修改点击事件的代码:
修改后的代码:
//echarts点击事件
function echartsMapClick(params) {
if (!params.data) {
return
}
else
{
//如果当前是最后一级,那就直接return
if (parentInfo[parentInfo.length - 1].code == params.data.cityCode) {
return
}
//如果是市或区则跳过地图变换
//console.log(params.data.name, params.data.name[params.data.name.length-1])
if (params.data.name[params.data.name.length-1]=='市' || params.data.name[params.data.name.length-1]=='区'|| params.data.name[params.data.name.length-1]=='州') {
return
}
let data = params.data
parentInfo.push({
cityName: data.name,
code: data.cityCode
})
init(data.cityCode)
}
}
修改完成后我们刷新下网页,看看是不是点击市或者区就没反应了?目的达成。
4.2 词云
此部分对词云的改动:
- 从后台获取数据以作为显示的元素
- 词云标签可点击,点击后刷新显示新词云
- 可返回上一级词云(实现方式:点击左上角词云标题)
- 词云遮罩微调
4.2.1 后台数据获取
首先进入messageTranser.js文件中,我们修改一下之前的代码,将测试代码修改为正式代码,如下所示:
原代码:
function GetDatas()
{
//ajax获取数据
$.ajax({
url:'/getDataTest',
data:{},
type:'GET',
async:false,
dataType:'json',
success:function(data){
alert("Data: " + data.data + "\nStatus: " + status);
},
error: function(msg){
console.log(msg);
alert('系统错误');
}
})
}
修改后的代码:
function GetDatas(message) //变动0
{
//ajax获取数据
var tempData; //变动1
$.ajax({
url:'/getData', //变动2
data:message, //变动0
type:'POST', //变动3
async:false,
dataType:'json',
success:function(data){ //变动4
//alert("Data: " + data.data + "\nStatus: " + status);
console.log(data);
tempData = data;
},
error: function(msg){
console.log(msg);
alert('系统错误');
}
})
return tempData; //变动5
}
五个变动的解释:
- 变动0:传参,用于发送消息给后台
- 变动1:添加一个临时变量,以返回接收到的数据,否则无法返回数据
- 变动2:“Test”删除,表示正式使用
- 变动3:POST方法以实现双线数据传送
- 变动4:将接收到的值附给临时变量,以返回
- 变动5:返回接收到的数据
其次进入我们的FlaskTest.py,这样修改:
原代码:
@app.route('/getDataTest', methods=['GET'])
def GetData():
return json.dumps({"data":1})
修改后:
@app.route('/getData', methods=['POST'])
def GetData():
time_a = time.time() #记录耗时时间
print('WordCloudMessage 收到!!!!!')
valueType = request.form.get('type')
value = request.form.get('value')
print("接收到的类型:",valueType)
print("接收到的值:",value)
# 初始化返回值
result = {}
result['company'] = []
result['value'] = []
result['type'] = 'UnKnown'
time_b = time.time()
print("此次处理耗时:" + str(time_b - time_a))
return json.dumps(result)
除此之外,我们的数据还在jupyter上,并没有转移到我们的FlaskTest.py中,因为处理代码部分已经做过介绍了,这里我直接提供整理之后的数据处理函数,如下:
提示:数据源文件放在了"./源数据" 目录下
##########################--2.数据处理部分--##########################
@app.route('/No/DataProcessing', methods=['Post'])
def getEmployData():
# 1.初步预览前9条数据
file_path = './源数据/附件1:招聘信息.csv'
file = open(file_path, 'r')
#打印前9条数据
for i in range(10):
print(file.readline())
file.seek(0)
file.readline()
data_num = 0
for i in file:
data_num+=1
print('数据总量:', data_num)
# 2. 数据获取
#输出数据的标签,保存到label(list)中
label = []
file.seek(0) # 回到文档刚开始的位置
temp = file.readline().split(',')
for item in temp:
label.append(item.split('"')[1])
print(label)
#获取所有数据,封装成dict
dict_temp = {}
line = []
line_temp = []
for i in range(data_num):
line.append([])
line_temp = file.readline().split(",")#标签分开
string_temp = ''
for j in line_temp:
try:
#标签内部去掉空格和 "字符
string_temp = j.split('"')[1]
if string_temp == '':
string_temp = j.split('"')[0]
string_temp = string_temp.split(" ")[-1]
if string_temp == '':
string_temp = j.split(" ")[0]
line[i].append(string_temp)
except:
line[i].append(j)
print('原格式处理完成')
# 3.数据封装成DataFrame
#dict键值初始化
print('正在封装为DataFrame..')
for i in label:
dict_temp[i] = []
for count in range(data_num):
for l, i in zip(label, range(len(label))):
dict_temp[l].append(line[count][i])
#print(dict_temp)
frame_test = pd.DataFrame(dict_temp)
print(frame_test.values[0:10])
return frame_test, data_num
这个函数的作用就是加载源数据后进行一系列比较细节的处理,得到的结果封装为pandas中的DataFrame,然后关闭文件,将得到的DataFrame进行返回。
再者,我们需要将main函数修改一下,如下:
原代码:
if __name__ == '__main__':
app.run(debug=False, threaded=True)
修改后:
if __name__ == '__main__':
time_a = time.time()
print('正在处理数据...')
frame, data_num = getEmployData()
# 获取岗位数量
print('正在预处理数据..')
JobNum = {}
for i in frame.values[:,12]:
if i not in JobNum.keys():
JobNum[i] = 1
else:
JobNum[i] += 1
for i in frame.values[:,11]:
if i not in JobNum.keys():
JobNum[i] = 1
else:
JobNum[i] += 1
print('数据预处理完成')
time_b = time.time()
print('耗时%.3fs'%(time_b-time_a))
app.run(debug=False, threaded=True)
这样我们就把frame获取为了全局变量,就可以在后台中对其进行任意的访问。
到目前为止,我们的py后台代码如下:
from flask import Flask
from flask import request
from flask import render_template
import json
import time
import pandas as pd
##########################--1.前后端通信部分--##########################
app = Flask(__name__, template_folder='',static_folder="",static_url_path="")
@app.route('/')
def index():
return render_template('index.html')
@app.route('/getData', methods=['POST'])
def GetData():
time_a = time.time() #记录耗时时间
print('WordCloudMessage 收到!!!!!')
valueType = request.form.get('type')
value = request.form.get('value')
print("接收到的类型:",valueType)
print("接收到的值:",value)
# 初始化返回值
result = {}
result['company'] = []
result['value'] = []
result['type'] = 'UnKnown'
time_b = time.time()
print("此次处理耗时:" + str(time_b - time_a))
return json.dumps(result)
##########################--2.数据处理部分--##########################
@app.route('/No/DataProcessing', methods=['Post'])
def getEmployData():
# 1.初步预览前9条数据
file_path = './源数据/附件1:招聘信息.csv'
file = open(file_path, 'r')
#打印前9条数据
for i in range(10):
print(file.readline())
file.seek(0)
file.readline()
data_num = 0
for i in file:
data_num+=1
print('数据总量:', data_num)
# 2. 数据获取
#输出数据的标签,保存到label(list)中
label = []
file.seek(0) # 回到文档刚开始的位置
temp = file.readline().split(',')
for item in temp:
label.append(item.split('"')[1])
print(label)
#获取所有数据,封装成dict
dict_temp = {}
line = []
line_temp = []
for i in range(data_num):
line.append([])
line_temp = file.readline().split(",")#标签分开
string_temp = ''
for j in line_temp:
try:
#标签内部去掉空格和 "字符
string_temp = j.split('"')[1]
if string_temp == '':
string_temp = j.split('"')[0]
string_temp = string_temp.split(" ")[-1]
if string_temp == '':
string_temp = j.split(" ")[0]
line[i].append(string_temp)
except:
line[i].append(j)
print('原格式处理完成')
# 3.数据封装成DataFrame
#dict键值初始化
print('正在封装为DataFrame..')
for i in label:
dict_temp[i] = []
for count in range(data_num):
for l, i in zip(label, range(len(label))):
dict_temp[l].append(line[count][i])
#print(dict_temp)
frame_test = pd.DataFrame(dict_temp)
print(frame_test.values[0:10])
return frame_test, data_num
@app.route('/postTest', methods=['POST'])
def postTest():
print(request.form.get('city'))
return json.dumps({'status':'成功接收'})
if __name__ == '__main__':
time_a = time.time()
print('正在处理数据...')
frame, data_num = getEmployData()
# 获取岗位数量
print('正在预处理数据..')
JobNum = {}
for i in frame.values[:,12]:
if i not in JobNum.keys():
JobNum[i] = 1
else:
JobNum[i] += 1
for i in frame.values[:,11]:
if i not in JobNum.keys():
JobNum[i] = 1
else:
JobNum[i] += 1
print('数据预处理完成')
time_b = time.time()
print('耗时%.3fs'%(time_b-time_a))
app.run(debug=False, threaded=True)
让我们回到词云代码中,主动提出数据的获取,进入wordCloud.js,添加一个初始化词云的函数,然后调用这个初始化函数,如下:
//只调用一次(初始化函数)
function initWorldCloud()
{
//初始化
var message_word = {
type: 'City',
value: '北京'
}
present = GetDatas(message_word);
worder(present['company'], present['value'], '北京');
}
initWorldCloud();
接下来我们关掉后台服务exe,重新双击运行FlaskTest.py,不出意外的话你会看见这样的画面:
我们刷新网页(如果你无法顺利进入网页一直白屏,关闭后台进程后重新运行这个py文件,再重新刷新网页),你会看到词云里面没有东西,我们Debug看看,如下所示:
我们按F12查看console,发现返回的东西是空白的,我们还没有在后台完善代码!
于是乎,我们又跑到py文件中,到GetData()函数中加入数据的读取部分代码,大体上是:根据前端需要的数据类型和数据本身来确定返回的内容。但是由于内容稍微有点多,这里暂不作过多细节上的解释,代码如下:
@app.route('/getData', methods=['POST'])
def GetData():
time_a = time.time() #记录耗时时间
print('WordCloudMessage 收到!!!!!')
valueType = request.form.get('type')
value = request.form.get('value')
print("接收到的类型:",valueType)
print("接收到的值:",value)
# 初始化返回值
result = {}
result['company'] = []
result['value'] = []
result['type'] = 'UnKnown'
# 城市类型的数据
if valueType=='City':
result['type'] = 'City'
value = value.split('省')[0]
value = value.split('市')[0]
value = value.split('县')[0]
print(value)
if value not in frame.values[:, 0]:
print('该地区不提供岗位!')
else:
index = frame.values[:, 0].tolist().index(value)
result['company'].append(frame.values[index][1])# 名称
result['value'].append(1)# 数值
# 添加新值
while value in frame.values[index+1:,0]:
index = frame.values[index+1:,0].tolist().index(value) + index + 1
if frame.values[index][1] not in result['company']:
result['company'].append(frame.values[index][1])# 名称
result['value'].append(1)# 数值
else:
result['value'][result['company'].index(frame.values[index][1])]+=1# 数值
index +=1
if len(result['company']) > 70:
break
# 公司类型的数据
elif valueType=='Company':
result['type'] = 'Company'
if value in frame.values[:,1]:
# 初始化返回值
index = frame.values[:,1].tolist().index(value)
result = {}
result['companyDetail'] = [frame.values[index][1],
'公司规模'+frame.values[index][3],
frame.values[index][5],
frame.values[index][6],
frame.values[index][7],
frame.values[index][8],
frame.values[index][9],
frame.values[index][10],
frame.values[index][11],
frame.values[index][12],
frame.values[index][13],
'工作年限'+frame.values[index][14]]
result['value'] = [5, 1, 2, 1, 1, 2, 1, 1, 1, 1, 3, 2]
# 添加新值
while value in frame.values[index+1:,1]:
index = frame.values[index+1:,1].tolist().index(value) + index + 1
if frame.values[index][13] not in result['companyDetail']:
result['companyDetail'].append(frame.values[index][13])
result['value'].append(2)
if frame.values[index][12] not in result['companyDetail']:
result['companyDetail'].append(frame.values[index][12])
result['value'].append(2)
if frame.values[index][11] not in result['companyDetail']:
result['companyDetail'].append(frame.values[index][11])
result['value'].append(2)
if frame.values[index][10] not in result['companyDetail']:
result['companyDetail'].append(frame.values[index][10])
result['value'].append(2)
if frame.values[index][7] not in result['companyDetail']:
result['companyDetail'].append(frame.values[index][7])
result['value'].append(2)
if frame.values[index][5] not in result['companyDetail']:
result['companyDetail'].append(frame.values[index][5])
result['value'].append(2)
index +=1
if len(result['companyDetail']) > 70:
break
else:
# 初始化返回值
result = {}
result['companyDetail'] = []
result['value'] = []
# Job类型的数据
elif valueType == 'Job':
temp_type = 0
if value in frame.values[:,11]:
temp_type = 11
if value in frame.values[:,12]:
temp_type = 12
print(temp_type)
if temp_type > 0:
# 初始化返回值
count = 0
result = {}
result['paralellValue'] = [[]]
result['type'] = 'Job'
result['JobNum'] = 0
index = frame.values[:, temp_type].tolist().index(value)
result['paralellValue'][count].append(frame.values[index][13])
result['paralellValue'][count].append(frame.values[index][3])
result['paralellValue'][count].append(frame.values[index][5])
result['paralellValue'][count].append(frame.values[index][14])
result['paralellValue'][count].append(frame.values[index][8])
result['paralellValue'][count].append(frame.values[index][0])
result['paralellValue'].append([])
count += 1
# 添加新值
while value in frame.values[index+1:,temp_type]:
index = frame.values[index+1:,temp_type].tolist().index(value) + index + 1
if 'k' not in frame.values[index][13] or\
frame.values[index][14]=='应届毕业生' or\
frame.values[index][14] in frame.values[:,5]:
index +=1
continue
else:
result['paralellValue'][count].append(frame.values[index][13])
result['paralellValue'][count].append(frame.values[index][3])
result['paralellValue'][count].append(frame.values[index][5])
result['paralellValue'][count].append(frame.values[index][14])
result['paralellValue'][count].append(frame.values[index][8])
result['paralellValue'][count].append(frame.values[index][0])
if count > 70:
break
result['paralellValue'].append([])
count += 1
#print(frame.values[index][temp_type])
index +=1
# 获取岗位数量
result['JobNum'] += JobNum[value]
elif valueType=='CompanyArea':
# 初始化返回值
result = {}
result['value'] = []
count = -1
valueble = 0#记录有效数据个数,防止过早退出循环
time_a = time.time()#记录处理时间,防止死循环
isAdded = False
if value in frame.values[:,1]:
index = frame.values[:,1].tolist().index(value)
result['value'].append({})
count +=1
result['value'][count]['name'] = frame.values[index][0]
result['value'][count]['data'] = [1]
valueble += 1
# 添加新值
while value in frame.values[index+1:,1]:
index = frame.values[index+1:,1].tolist().index(value) + index + 1
for i in range(len(result['value'])):
if frame.values[index][0] == result['value'][i]['name']:
result['value'][i]['data'][0] += 1
valueble += 1
isAdded = True
break
if isAdded==False:#新的城市
result['value'].append({})
count += 1
result['value'][count]['name'] = frame.values[index][0]
result['value'][count]['data'] = [1]
valueble += 1
index +=1
time_b = time.time()
#print(result)
if (len(result['value']) > 15 and valueble>70): #or (time_b-time_a)>10:#10秒强制退出
break
print(str(result)+'\n')
time_b = time.time()
print('耗时%.3fs'%(time_b-time_a))
return json.dumps(result)
添加数据处理代码后,再次重启服务,我们重启服务后再刷新网页:
成功了!
4.2.2 点击响应事件
但是现在的词云还没有点击事件的响应,我们加入点击事件,并继续进一步获取数据,但是为了实现回退功能,我们需要考虑点击历史这个东西,每点一下就要记录一下。我们先在MessageTranser中添加一个全局变量,用于记录点击历史。
MessageTranser.js最前面:
//全局变量
var clickHistory = [{type:'Start', value:'Start'}];
var last_click = {};
回到worldCloud.js中,进入函数worder,代码倒数第几行中找到myChart.off(‘click’),紧接着添加如下代码:
myChart.on('click', function (params) {
//console.log(params.data.name)
var message_word = {
type: 'Company',
value: params.name
}
//更新、记录点击历史
if (clickHistory[clickHistory.length-1]['type']=='Start' || clickHistory[clickHistory.length-1]['type']=='City')
{
last_click = { type: 'Company', value: params.name };
clickHistory.push(last_click);
}
else if (clickHistory[clickHistory.length-1]['type']=='Company')
{
//clickHistory.pop();//删除最后一个点击的公司
last_click = { type: 'Job', value: params.name };
clickHistory.push(last_click);
console.log(clickHistory);
console.log(params.name);
message_word.type = 'Job'
}
else if(clickHistory[clickHistory.length-1]['type']=='Job')
{
clickHistory.pop();
last_click = { type: 'Job', value: params.name };
clickHistory.push(last_click);
console.log(clickHistory);
console.log(params.name);
message_word.type = 'Job'
}
var present = GetWord(message_word);
if (message_word.type == 'Company')
{
console.log(present['companyDetail']);
console.log(present['value']);
if (present['companyDetail'].length>0)
{
worder(present['companyDetail'], present['value'], message_word.value);
}
//更新下面的条状图
message_word.type = 'CompanyArea';
var temp_present = GetDatas(message_word);
if (temp_present['value'].length>0)
{
//temp_present['value'].pop() //去除最后一个无用数据
LinePie(temp_present['value'], params.name);
}
}
else if (message_word.type == 'Job')//点击了包含职位的其他东西
{
console.log(typeof(present['paralellValue']));
if (present['paralellValue'].length>0)
{
console.log(params.name, ' 岗位数量:', present['JobNum'])
paralellChart(present['paralellValue'], params.name, present['JobNum']);
}
}
})
修改完成后我们再刷新网页,我们随便点击一个公司,成功:
4.2.3 词云与地图联动
我们期望点击地图后,词云所显示的公司会随点击的地区而改变,为了和地图联动,我们需要在map.js地图的点击事件中加入以下代码,加入位置是我们之前判断市、区、州那个部分,判断到我们点击了市区州后,之前的代码是直接return了,我们在return前加入此段代码。(由于后台处理部分已经全部给出,因此后台中不需要再行添加代码):
var temp_city = '';
for (var i=0; i<jobNum.length; i++)
{
if (params.name[0] == jobNum[i].city[0] &&
params.name[1] == jobNum[i].city[1])
{
//console.log(jobNum[i].city, jobNum[i].value)
temp_city = jobNum[i].city;
break;
}
};
var message_word = {
type: 'City',
value: temp_city
}
present = GetDatas(message_word);
if (present['company'].length > 0) {//有数据
clickHistory = [{type:'Start', value:'Start'}];
last_click = {};
last_words={
text: present['company'],
value: present['value'],
title: message_word.value
}
worder(present['company'], present['value'], message_word.value);
//更新、记录点击历史
if (last_click['type'] == 'City') {
if (params.name != last_click['value']) {
clickHistory.pop();//删除最后一个点击的公司
last_click = { type: 'City', value: params.name };
clickHistory.push(last_click);
console.log(clickHistory);
console.log(params.name);
}
}
else {
if (params.name != last_click['value']) {
last_click = { type: 'City', value: params.name };
clickHistory.push(last_click);
console.log(clickHistory);
console.log(params.name);
}
}
}
else {
console.log('不存在岗位')
}
当我们点击四川省的时候,词云不发生变化,当我们点击成都市的时候就能让词云显示成都市的公司信息,如下所示:
4.2.4标题点击回退功能的实现
加入我们想探索性的浏览,这个时候我们向让词云具备回退功能,我们就不得不需要一个全局变量来存储上一级的词云,于是乎我们要在worder中定义一个变量,这个全局变量存储上一级的标题和内容,我们每进一步点击有效的时候就刷新这个全局变量。
标题点击怎么做?我们进入以下代码路径:worder->maskImage.onload->myChart.setOption->title,在里面添加两行代码,如下:
link: "javascript: backwards()",//标题点击事件
target: "self",// 保证不会在新的窗口弹出,
这样就将标题点击事件绑定到了名为“backwards”的函数中去,另一行代码则是防止点击后跳转到另一个空白网页中去,如果不加这行代码,网页会跳转,这是我们不希望看到的。
接下来就是编写回退的代码部分了,把以下代码追加到wordCloud.js的最后面:
//记录上一个词云,用于回溯上一级
var last_words={
title: '北京',
text: present['company'],
value: present['value']
}
//标题点击回退
function backwards()
{
worder(last_words.text, last_words.value, last_words.title);
if(clickHistory[clickHistory.length-1]['type']!='Start')
{
if(clickHistory[clickHistory.length-1]['type']=='Job')
{
clickHistory.pop();
clickHistory.pop();
}
else
{
clickHistory.pop();
}
console.log(clickHistory);
}
}
这样我们就实现了词云标题的点击以及词云的回退,里面一些具体的细节部分这里不作解释,感兴趣的可以在评论区进行评论,里面肯定有许多不合理的地方,还请大家不吝赐教。
4.3 平行坐标系
由于修改的地方过多,这里我直接提供代码了。
function paralellChart(paralellValue, titleText, totalNum)
{
paralellValue.pop();
var data_0 = noRepeat(getColumn(paralellValue, 0));
var data_1 = noRepeat(getColumn(paralellValue, 1));
var data_2 = noRepeat(getColumn(paralellValue, 2));
var data_3 = noRepeat(getColumn(paralellValue, 3));
var data_4 = noRepeat(getColumn(paralellValue, 4));
var data_5 = noRepeat(getColumn(paralellValue, 5));
//图表排序
data_0.sort(function(a, b){
return Number(getLastNumberStr(a)) - Number(getLastNumberStr(b));
})
data_1.sort(function(a, b){
return Number(getLastNumberStr(a)) - Number(getLastNumberStr(b));
})
data_3.sort(function(a, b){
return Number(getLastNumberStr(a)) - Number(getLastNumberStr(b));
})
var myChart = echarts.init(document.querySelector('.right_up'));
var option = {
left: '0',
parallel: {
left: 20,
bottom: 10,
//top: ,
// top: 150,
// height: 300,
//width: ,
//layout: 'vertical',
parallelAxisDefault: {
type: 'value',
name: 'nutrients',
nameLocation: 'end',
//nameGap: 20,
nameTextStyle: {
color: 'rgb(179, 239, 255)',
fontSize: 14
},
axisLine: {
lineStyle: {
color: 'rgb(179, 239, 255)'
}
},
axisTick: {
lineStyle: {
color: '#rgb(179, 239, 255)'
}
},
splitLine: {
show: false,
lineStyle: {
color: '#rgb(179, 239, 255)'
}
},
axisLabel: {
color: '#fff'
},
realtime: false
}
},
parallelAxis:
[
{
dim: 0,
name: '工资',
type: 'category',
data: data_0
},
{
dim: 1,
name: '公司规模',
type: 'category',
data: data_1
},
{
dim: 2,
name: '学历',
//data: ['A', 'B']
type: 'category',
data: data_2
},
{
dim: 3,
name: '工作年限',
type: 'category',
data: data_3
},
{
dim: 4,
name: '工作性质',
//data: ['中', '良', '优']
type: 'category',
data: data_4
},
{
dim: 5,
name: '城市',
type: 'category',
data: data_5
}
],
title:[
{
//link: "javascript: backwards()",
//target: "self",// 保证不会在新的窗口弹出,
left: 'top',
top: 0,
text: titleText+' 岗位数量: ' + totalNum,
textStyle: {
color: 'rgb(179, 239, 255)',
fontSize: 18
}
},
// {
// //link: "javascript: backwards()",
// //target: "self",// 保证不会在新的窗口弹出,
// bottom: 10,
// left: 'center',
// text: '招聘信息数据可视化',
// textStyle: {
// color: 'rgb(40, 44, 52)',
// fontSize: 26
// },
// border:'line'
// }
],
series: {
type: 'parallel',
lineStyle: {
width: 2,
color: 'rgb(78, 195, 216)'
},
data: paralellValue
}
};
// console.log( [
// [1, 55, 'A', 9, 'sd', 1],
// [2, 25, 'B', 11, '123', 2],
// [3, 56, 'B', 7, '良', 2],
// [4, 33, 'A', 7, 'sd', 2],
// [4, 33, 'A', 7, '优', 2]
// ]);
myChart.setOption(option);
//点击前解绑,防止点击事件触发多次
myChart.off('click');
//myChart.on('click', echartsMapClick);
window.addEventListener("resize", function () {
myChart.resize();
});
}
function noRepeat(array){
return Array.from(new Set(array));
//这里的 Array.from()方法是将两类对象转为真正的数组:类似数组的对象和可遍历的对象(包括es6新增的数据结构Set和Map)
}
function getColumn(arr, column)
{
var temp = [];
for(var i=0; i<arr.length;i++)
{
temp.push(arr[i][column])
}
return temp;
}
function initParalell()
{
var message_word = {
type: 'Job',
value: 'dba'
}
present = GetDatas(message_word);
//console.log(present['company']);
//console.log(present['value']);
//present = ['圣诞树', '贺卡', '圣诞礼盒', '围巾', '袜子', '苹果', '手链', '巧克力', '玫瑰', '香水', '乐高', '芭比', '项链', '抱枕', '变形金刚', '摆件', '魔方', '文具', '棒棒糖', '蓝牙耳帽', '超级飞侠', '暖手宝', '夜灯', '堆袜', '耳钉', '公仔', '手机壳', '八音盒', '剃须刀', '打火机', '手表', '巴克球', '模型', '音响', '蒙奇奇', '保温杯']
console.log(present['JobNum'])
paralellChart(present['paralellValue'], 'dba', present['JobNum']);
}
//获取字符串最后一个数字串
function getLastNumberStr(str)
{
var result = ''
for(var i=str.length-1; i>0; i--)
{
if(isNumber(str[i]))
{
result+=str[i]
}
}
result = result.split('').reverse().join('');
return result
}
//判断是否为数字
function isNumber(val){
var regPos = /^\d+(\.\d+)?$/; //非负浮点数
var regNeg = /^(-(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*)))$/; //负浮点数
if(regPos.test(val) || regNeg.test(val)){
return true;
}else{
return false;
}
}
initParalell();
总体思路:点击词云中的职位后,平行坐标系显示该职位的一些基本情况。因此需要在词云的点击事件中添加另一个判断:
else if (message_word.type == 'Job')//点击了包含职位的其他东西
{
console.log(typeof(present['paralellValue']));
if (present['paralellValue'].length>0)
{
console.log(params.name, ' 岗位数量:', present['JobNum'])
paralellChart(present['paralellValue'], params.name, present['JobNum']);
}
}
4.4 柱状分布图
同平行坐标系,这里我直接提供代码:
function LinePie(datas, title)
{
var myChart = echarts.init(document.querySelector('.bottom'))
myChart.clear()
var option = {
//backgroundColor: '#fff',
grid: {
containLabel: true,
left: 20,
right: 20,
top: 15,
bottom:8,
},
tooltip: {
show: false,
},
title:{
left: 'center',
top: 0,
text: title+' 岗位地区分布',
textStyle: {
color: 'rgb(179, 239, 255)',
fontSize: 18
}
},
legend: {
show: false,
},
xAxis: {
axisLine: {
show: false,
},
axisTick: {
show: false,
},
splitLine: {
show: false,
},
axisLabel: {
show: false,
},
},
yAxis: {
data: ["sss"],
axisLabel: {
show: false,
},
axisLine: {
show: false,
},
axisTick: {
show: false,
},
splitLine: {
show: false,
},
},
color: [
"#5292FD",
"#3DBB33 ",
"#FCAD2C",
"#EF6B6E",
"#7F6AAD",
"#585247",
"#5292FD",
"#3DBB33 ",
"#FCAD2C",
"#EF6B6E",
"#7F6AAD",
"#585247",
],
series: [],
}
datas.forEach((item, index) => {
option.series.push({
type: "bar",
name: item.name,
stack: "1",
label: {
normal: {
show: true,
position: [5, 5],
formatter: `{value|${item.data}}\n\n{name|${item.name}}`,
align: "left",
textStyle: {
color: "#fff",
rich: {
name: {
fontSize: "14",
fontWeight: 500,
color: "#8AD2FA",
},
value: {
fontSize: "18",
fontWeight: 500,
color: "#fff",
},
},
},
},
},
barWidth: 30,
data: item.data,
itemStyle: {
normal: {
barBorderRadius: [0],
},
},
});
if (index === 0) {
option.series[index].itemStyle.normal.barBorderRadius = [
5,
0,
0,
5,
];
} else if (index === datas.length - 1) {
option.series[index].itemStyle.normal.barBorderRadius = [
0,
5,
5,
0,
];
} else {
return;
}
});
myChart.setOption(option);
window.addEventListener("resize", function () {
myChart.resize();
});
}
// var echartData= [
// { name: "电解锌", data: [36] },
// { name: "硅锰合金", data: [19] },
// { name: "磷酸氢钙", data: [11] },
// { name: "硫酸", data: [9] },
// ];
//初始化
function initLineBar()
{
message_word = {
type: 'CompanyArea',
value: '北京智慧图'
}
var temp_present = GetDatas(message_word);
if (temp_present['value'].length>0)
{
//temp_present['value'].pop() //去除最后一个无用数据
LinePie(temp_present['value'], '北京智慧图');
}
}
initLineBar();
4.5 网页背景
我们去除各div的背景颜色,添加网页背景图片,目前的less文件如下:
body{
background: url(../img/bg.jpg) no-repeat top center
}
.left{
top:8%;
position:absolute;
float: left;
margin-left: 5px;
height: 8.7rem;
width: 11rem;
//margin-left: 0.2rem;
//background-color: antiquewhite;
border: #1B6D85 dotted;
}
.bottom{
position: absolute;
bottom: 0.5%;
width: 23.65rem;
height: 1.2rem;
//background-color: rgb(3{}4, 193, 233);
border: #1B6D85 dotted;
}
.right_bottom{
position: absolute;
right: 1%;
bottom: 13%;
width: 12rem;
height: 4rem;
//background-color: rgb(184, 193, 233);
border: #1B6D85 dotted;
}
.right_up{
position: absolute;
top:8%;
right: 1%;
width: 12rem;
height: 4.5rem;
//background-color: rgb(34, 193, 233);
border: #1B6D85 dotted;
}
至此,我们得到如下的显示效果并且图与图之间之间可以联动,若要消除词云与地图的背景色,可以在option里面将background注释掉即可。
效果如下: