用500行纯前端代码在浏览器中构建一个Tableau
2019独角兽企业重金招聘Python工程师标准>>>
在Gartner最新的对商务智能软件的专业分析报告中,Tableau持续领跑。Microsoft因为PowerBI表现出色也处于领导者象限。而昔日的领导者像SAP,SAS,IBM,MicroStrategy等逐渐被拉开了差距。
Tableau因为其灵活,出色的数据表现已经成为BI领域里无可争议的领头羊。而其数据驱动的可视化和核心思想是来自于Leland Wilkinson的The Grammar Of Graphics ,同样受到该思想影响的还有R的图形库ggplot。
在数据可视化开源领域里,大家对百度开发的echarts可谓耳熟能详,echarts经过多年的发展,其功能确实非常强大,可用出色来形容。但是蚂蚁金服开源的基于The Grammar Of Graphics的语法驱动的可视化库G2,让人眼前一亮。那我们就看看如何利用G2和500行左右的纯前端代码来实现一个的类似Tableau的数据分析功能。
- 演示参见 https://codepen.io/gangtao/full/OZvedx/
- 代码参见 https://gist.github.com/gangtao/e053cf9722b64ef8544afa371c2daaee
数据加载
第一步是加载数据:
数据加载主要用到了三个库:
- axios 基于Promise的HTTP客户端
- alasql 基于JS的开源SQL数据库
- jquery datatable JQuery的数据表格插件
数据通过我存放在GitHub中的csv格式的文件,以REST请求的方式来加载。下面的代码把Axios的Promise变成 async/wait方式。
// Ajax async request
const request = {get: url => {return new Promise((resolve, reject) => {axios.get(url).then(response => {resolve({ data: response.data });}).catch(error => {resolve({ data: error });});});}
};
封装好后,我们就可以用request.get()方法发送REST请求,获取csv文件。
let csv = await request.get(url);
这一步可能会遇到跨域请求的问题,github上的文件支持跨域。
把数据存储在一个SQL数据库中,这样做的好处是为了下一步做数据准备的时候,可以方便的利用SQL来进行查询和分析。
class SqlTable {constructor(data) {this.data = data;}async query(sql) {// following line of code does not run in full page view due to security concern.// const query_str = sql.replace(/(?<=FROM\s+)\w+/, "CSV(?)");const query_str = sql.replace("table", "CSV(?)");return await alasql.promise(query_str, [this.data]);}
}
SqlTable是一个对数据表的封装,把csv数据存在SQL数据库表中,提供一个query()方法。这里要做的是把SQL查询个从 "SELECT * FROM table" 变成 "SELECT * FROM CSV(?)" 表示查询参数是CSV数据。因为codepen的安全性限制,运行前向查找的replace语句(这里的regex表示把前面是“FROM ”词的替换为CSV(?)的)在full page view下是不能执行的,所以我用了一个更简单的假定,用户的表名就是table,这样做有很多问题,大家如果在codepen之外的环境,可以用注释掉的代码。
然后把"SELECT * FROM table"的查询结果(JSON Array)用datatable来展示。
function sanitizeData(jsonArray) {let newKey;jsonArray.forEach(function(item) {for (key in item) {newKey = key.replace(/\s/g, "").replace(/\./g, "");if (key != newKey) {item[newKey] = item[key];delete item[key];}}});return jsonArray;
}function displayData(tableId, data) {// tricky to clone arraylet display_data = JSON.parse(JSON.stringify(data));display_data = sanitizeData(display_data);let columns = [];for (let item in display_data[0]) {columns.push({ data: item, title: item });}$("#" + tableId).DataTable({data: display_data,columns: columns,destroy: true});
}
这一步有两点要注意:
- 数据中,如果列的名字中有包含点,空格等字符,例如Iris数据集中的Sepal.Length,datatable是无法正常显示的,这里要调用sanitizeData()方法把列名,也就是JsonArray中Json对象的属性名中的点和空格去掉。
- sanitizeData()方法会改变输入对象,所以在传入之前做了一个深度拷贝,这里利用JSON的stringfy和parse方法可以对JSON兼容的对象有效的拷贝。
这里要注意,Iris数据集中在datatable中的列名都不显示点,但实际数据并没有改变。
数据准备
数据加载完毕,我们来到第二步的数据准备阶段。数据准备是数据科学项目最花时间的一步,通常需要对数据进行大量的清洗,变形,抽取等工作,使得数据变得可用。
在这一步我们做了两件事:
一是显示数据的一个摘要,让我们初步了解数据的概貌,为进一步的数据变形和处理做好准备。
这个是Iris数据集的摘要:
function isString(o) {return typeof o == "string" || (typeof o == "object" && o.constructor === String);
}function summaryData(data) {let summary = {};summary.count = data.length;summary.fields = [];for (let p in data[0]) {let field = {};field.name = p;if ( isString(data[0][p]) ) {field.type = "string";} else {field.type = "number";}summary.fields.push(field);}for (let f of summary.fields) {if ( f.type == "number" ) {f.max = d3.max(data, x => x[f.name]);f.min = d3.min(data, x => x[f.name]);f.mean = d3.mean(data, x => x[f.name]);f.median = d3.median(data, x => x[f.name]);f.deviation = d3.deviation(data, x => x[f.name]);} else {f.values = Array.from(new Set(data.map(x => x[f.name])));}}return summary;
}
这里我们利用数据的类型判断出每一个字段是数值型还是字符型。对于字符型的字段,我们利用JS6的Set来获得所有的Unique数据。对于数值型,我们利用d3的max,min,mean,median,deviation方法计算出对应的最大值,最小值,平均数,中位数和偏差。
另一个就是利用SQL查询来对数据进行进一步的加工。
上图的例子中我们利用限制条件得到一个Iris数据的子集。
另外G2还提供了Dataset的功能:
- 源数据的解析,将csv, dsv,geojson 转成标准的JSON,查看Connector
- 加工数据,包括 filter,map,fold(补数据) 等操作,查看 Transform
- 统计函数,汇总统计、百分比、封箱 等统计函数,查看 Transform
- 特殊数据处理,包括 地理数据、矩形树图、桑基图、文字云 的数据处理,查看 Transform
数据处理是一个比较大的话题,我们的目标是利用尽可能少的代码完成一个数据分析的工具,所以这一步仅仅是利用alasql提供的SQL查询来处理数据。
数据展示
数据处理好后就是我们的核心内容,数据展示了。
这一步主要是利用select2提供的选择控件构建图形语法来驱动数据展示。如上图所示,对应的G2代码图形语法为:
g2chart.facet('rect', {fields: [ 'Admit', 'Dept' ],eachView(view) {view.interval().position('Gender*Freq').color('Gender').label('Freq');}
});
图形语法主要包含以下几个主要的元素:
几何标记 Geometry
几何标记定义了使用什么样的几何图形来表征数据。G2现在支持如下这些几何标记:
geom 类型 | 描述 |
---|---|
point
|
点,用于绘制各种点图。 |
path
|
路径,无序的点连接而成的一条线,常用于路径图的绘制。 |
line
|
线,点按照 x 轴连接成一条线,构成线图。 |
area
|
填充线图跟坐标系之间构成区域图,也可以指定上下范围。 |
interval
|
使用矩形或者弧形,用面积来表示大小关系的图形,一般构成柱状图、饼图等图表。 |
polygon
|
多边形,可以用于构建色块图、地图等图表类型。 |
edge
|
两个点之间的链接,用于构建树图和关系图中的边、流程图中的连接线。 |
schema
|
自定义图形,用于构建箱型图(或者称箱须图)、蜡烛图(或者称 K 线图、股票图)等图表。 |
heatmap
|
用于热力图的绘制。 |
这里要注意,intervalstack是官方支持的,但是文档没有提到,在阅读G2的API文档的时候,我也发现文档讲的不是很清楚,有很多地方没有讲清楚如何使用API。这也是开源软件值得改进的地方。
图形属性 Attributes
图形属性对应视觉编码中的不同元素,大家可以参考我的另一博客 数据可视化中的视觉属性 。
图形属性主要有以下几种。
- position:位置,二维坐标系内映射至 x 轴、y 轴;
- color:颜色,包含了色调、饱和度和亮度;
- size:大小,不同的几何标记对大小的定义有差异;
- shape:形状,几何标记的形状决定了某个具体图表类型的表现形式,例如点图,可以使用圆点、三角形、图片表示;线图可以有折线、曲线、点线等表现形式;
- opacity:透明度,图形的透明度,这个属性从某种意义上来说可以使用颜色代替,需要使用 'rgba' 的形式,所以在 G2 中我们独立出来。
在构建语法的时候,我们把图形属性绑定一个或者多个数据字段。
坐标系 Coordinates
坐标系是将两种位置标度结合在一起组成的 2 维定位系统,描述了数据是如何映射到图形所在的平面。
G2提供了以下几种坐标系:
coordType | 说明 |
---|---|
rect
|
直角坐标系,目前仅支持二维,由 x, y 两个互相垂直的坐标轴构成。 |
polar
|
极坐标系,由角度和半径 2 个维度构成。 |
theta
|
一种特殊的极坐标系,半径长度固定,仅仅将数据映射到角度,常用于饼图的绘制。 |
helix
|
螺旋坐标系,基于阿基米德螺旋线。 |
分面 Facet
分面,将一份数据按照某个维度分隔成若干子集,然后创建一个图表的矩阵,将每一个数据子集绘制到图形矩阵的窗格中。分面其实提供了两个功能:
- 按照指定的维度划分数据集;
- 对图表进行排版。
G2支持以下的分面类型:
分面类型 | 说明 |
---|---|
rect | 默认类型,指定 2 个维度作为行列,形成图表的矩阵。 |
list | 指定一个维度,可以指定一行有几列,超出自动换行。 |
circle | 指定一个维度,沿着圆分布。 |
tree | 指定多个维度,每个维度作为树的一级,展开多层图表。 |
mirror | 指定一个维度,形成镜像图表。 |
matrix | 指定一个维度,形成矩阵分面。 |
注意,在我的代码中,为了简化使用,只支持list和rect,当绑定一个字段的时候用list,绑定两个字段的时候用rect。
除了上面提到的元素,当然还有许多其它的元素我们没有包含和支持,例如:坐标轴,图例,提示等等。
关于图形的语法的更多内容,请参考这里。
生成图形语法的核心代码如下:
function getFacet(faced, grammarScript) {let facedType = "list";let facedScript = ""grammarScript = grammarScript.replace(chartScriptName,"view");if ( faced.length == 2 ) {facedType = "rect";}let facedFields = faced.join("', '")facedScript = facedScript + `${ chartScriptName }.facet('${ facedType }', {\n`;facedScript = facedScript + ` fields: [ '${ facedFields }' ],\n`;facedScript = facedScript + ` eachView(view) {\n`;facedScript = facedScript + ` ${ grammarScript };\n`;facedScript = facedScript + ` }\n`;facedScript = facedScript + `});\n`;return facedScript
}function getGrammar() {let grammar = {}, grammarScript = chartScriptName + ".";grammar.geom = $('#geomSelect').val(); grammar.coord = $('#coordSelect').val(); grammar.faced = $('#facetSelect').val(); geom_attributes.map(function(attr){grammar[attr] = $('#' + attr + "attr").val();});grammarScript = grammarScript + grammar.geom + "()";geom_attributes.map(function(attr){if (grammar[attr].length > 0) {grammarScript = grammarScript + "." + attr + "('" + grammar[attr].join("*") + "')"; } });if (grammar.coord) {grammarScript = grammarScript + ";\n " + chartScriptName + "." + "coord('" + grammar.coord + "');";} else {rammarScript = grammarScript + ";";}if ( grammar.faced ) {if ( grammar.faced.length == 1 || grammar.faced.length == 2 ) {grammarScript = getFacet(grammar.faced, grammarScript);} }console.log(grammarScript)return grammarScript;
}
这里有几点要注意:
- 使用JS的模版字符串可以有效的构造代码片段
- 使用eval执行构造好的语法驱动的代码来响应select的change事件,以获得良好的交互性。在生产环境,要注意该方法的安全性隐患,因为纯前端,eval能带来的威胁比较小,生产中,可以把这个执行放在安全的沙箱中运行
- 你需要理解图形语法,并不是任意的组合都能驱动出有效的图形。
这里对于select2的多选,有一个小的提示,在缺省情况下,多选的顺序是固定的顺序,并不依赖选择的顺序,然而许多图形语法和字段的顺序有关,所以我们使用如下的方法来相应select的选择事件。
function updateSelect2Order(evt) {let element = evt.params.data.element;let $element = $(element);$element.detach();$(this).append($element);$(this).trigger("change");
}
这样做就是每次选中后,把当前选中的项目移到数据最后的位置。
一些例子
好了,下面我们就来看一些例子,了解一下如何使用图形语法来分析和探索数据。
Iris数据集散点图
图形语法:
g2chart.point().position('Sepal.Length*Petal.Length').color('Species').size('Sepal.Width')
Car数据集折线图
图形语法:
g2chart.line().position('id*speed');
切换到极坐标:
图形语法:
g2chart.line().position('id*speed');
g2chart.coord('polar');
Berkeley数据柱状图
数据处理:
SELECT SUM(Freq) as f , Gender FROM table GROUP BY Gender
图形语法:
g2chart.interval().position('Gender*f').color('Gender').label('f');
Berkeley数据堆叠柱状图
数据处理:
SELECT SUM(Freq) as f , Gender , Admit FROM table GROUP BY Gender, Admit
图形语法:
g2chart.intervalStack().position('Gender*f').color('Admit')
Berkeley数据饼图
数据处理:
SELECT SUM(Freq) as f , Gender FROM table GROUP BY Gender
图形语法:
g2chart.intervalStack().position('f').color('Gender').label('f');
g2chart.coord('theta')
Berkeley数据分面的应用
图形语法:
g2chart.facet('rect', {fields: [ 'Dept', 'Admit' ],eachView(view) {view.coord('theta');view.intervalStack().position('Freq').color('Gender');}
});
更多的分析图形留给大家去尝试
总结
本文分享了一个利用纯前端技术构建一个类似Tableau的BI应用的例子,整个代码统计:
- JS 370 行 JS6
- HTML 69 + 9 + 5 = 83 行
- CSS 21 行
总计474 行,用这么少的代码就能完成一个看上去还不错的BI工具,还算不错吧。当然这里主要是由于开源社区提供了这么多好的前端库以供应用,我要做的仅仅是让它们有效的工作在一起。这个只能算是个原型,从功能和质量上来说都不成熟,但是能在浏览器中不借助任何的服务器来实现BI的数据分析功能,应该会有很多人想要在自己的应用中嵌一个吧?
结合我之前分享的TensorflowJS的文章,下面一步可能是加入预测功能,为数据分析加入智能,前端应用的前景,不可限量!
参考
- axios 基于Promise的HTTP客户端
- alasql 基于JS的开源SQL数据库
- jquery datatable JQuery的数据表格插件
- select2 JQuery的选择控件插件
- 相关博客 使用开源软件快速搭建数据分析平台
- 相关博客 数据可视化中的视觉属性
转载于:https://my.oschina.net/taogang/blog/1811573
用500行纯前端代码在浏览器中构建一个Tableau相关推荐
- 五大主流浏览器的内核,前端在IE浏览器中常见的兼容问题
一 标题五大主流浏览器及其内核 1.Trident 代表作:IE 元老级内核之一,由微软开发,并于1997年10月首次在ie 4.0中使用,凭借其windows垄断优势, Trident市场占有率一直 ...
- c++软件开发面试旋极面试题_经典软件开发面试题:浏览器中输入一个网址后发生了什么?...
经典软件开发面试题:浏览器中输入一个网址后发生了什么? 大家好, 这一期呢,我们来谈一个经典的面试题.这种题目是在浏览器中输入一个网址以后, 会显示一个网页,这期间到底发生了什么? 答案要求说的越 ...
- 在浏览器中输入一个域名之后都发生了什么
当你在浏览器中打入www.baidu.com后,轻轻一敲回车百度输入框就展现在你面前,我们看似很简单很简单的一个操作,背后却有着超级复杂的过程. 其实网络传输跟我们平常说话有许多相似的地方,大脑组织语 ...
- 蓝桥杯真题 跳跃 C++、Java实现 动态规划小蓝在一个 n 行 m 列的方格图中玩一个游戏。 开始时,小蓝站在方格图的左上角,即第 1 行第 1 列。
文章目录 题目描述 输入描述 输出描述 输入输出样例 示例 1 运行限制 代码(c++) 代码(Java) 思路 题目描述 小蓝在一个 n 行 m 列的方格图中玩一个游戏. 开始时,小蓝站在方格图的左 ...
- 代码的世界中,一个逻辑套着另外一个逻辑,如何让每一种逻辑在代码中都有迹可循?...
代码的世界中,一个逻辑套着另外一个逻辑,如何让每一种逻辑在代码中都有迹可循? 这正式框架的意义所在!
- 一键设置mac显示选项_如何通过关闭浏览器中的一个选项卡将Mac上的电池寿命延长一倍...
一键设置mac显示选项 by Primož Cigler 通过PrimožCigler 如何通过关闭浏览器中的一个选项卡将Mac上的电池寿命延长一倍 (How I doubled the batter ...
- 算法:小蓝在一个n行 m列的方格图中玩一个游戏
1.题目描述 小蓝在一个 n 行 m 列的方格图中玩一个游戏. 开始时,小蓝站在方格图的左上角,即第 1 行第 1 列. 小蓝可以在方格图上走动,走动时,如果当前在第 r 行第 c 列,他不能走到行号 ...
- c 语言500行小游戏代码,500行代码使用python写个微信小游戏飞机大战游戏.pdf
500行行代代码码使使用用python写写个个微微信信小小游游戏戏飞飞机机大大战战游游戏戏 这篇文章主要介绍了500行代码使用python写个微信小游戏飞机大战游戏,本文通过实例代码给大家介绍的非常详 ...
- c 浏览器语言,让C代码在浏览器中运行
WebAssembly作为一种新兴的Web技术,相关的资料和社区还不够丰富,但其为web开发提供了一种崭新的思路和工作方式,未来是很有可能大放光彩的. 使用WebAssembly,我们可以在浏览器中运 ...
最新文章
- Ubuntu中基于QT的系统网线连接状态的实时监视
- java uml 为什么_Java开发为什么需要UML (转)
- Java实战应用50篇(二)-SSM框架中的设计模式:动态代理
- 数字图像处理:第十九章 立体视觉
- python管道通信_Python进程通信之匿名管道实例讲解
- 分层结构,协议,接口,服务
- 常见的通配符_8、数据库常见操作
- VMware ESXi 环境备份与还原处理案例
- Fiddler及浏览器开发者工具进行弱网测试
- oracle_分区表的新增、修改、删除、合并。普通表转分区表方法
- python黑帽子(黑客与渗透测试编程之道)
- Oracle MySQL sql 列转行 union all 实现
- Ubuntu下codeblocks汉化
- 百度地图设置卫星地图显示图文教程
- H264三种码率控制方法(CBR, VBR, CVBR)
- 无线网服务器连接不上什么原因,无线路由器连接不上是什么原因
- 机器学习 卷积神经网络 Convolutional Neural Network(CNN)
- Virtualbox虚拟机安装win10系统卡顿
- matlab射线平均速度时距曲线,时距曲线实验
- Windows7双系统卸载Ubuntu
热门文章
- Windows + Eclipse + Gtk 环境(总结)
- php查询socket数据包头,php 查询数组值php中关于socket的系列函数总结
- python 下标越界_Python中异常处理
- 怎么获取web开发怎么获取手机的唯一标识_PYTHON实现北京住宅小区数据抓取-(Web服务API-地点检索服务)
- vue each_Vue.js从零开始——模块化项目(2)
- ubuntu docker慢_基于docker搭建MulVAL攻击图
- 爬虫模拟登陆手机验证码_Python+scrapy爬虫之模拟登陆
- 广东轻工计算机多媒体,广东轻工职业技术学院2015年自主招生计算机多媒体技术专业考核大纲...
- java 内存溢出 内存泄露_JVM——内存泄漏与内存溢出
- tcp当主动发出syn_一文读懂TCP四次挥手工作原理及面试常见问题汇总