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});
}

这一步有两点要注意:

  1. 数据中,如果列的名字中有包含点,空格等字符,例如Iris数据集中的Sepal.Length,datatable是无法正常显示的,这里要调用sanitizeData()方法把列名,也就是JsonArray中Json对象的属性名中的点和空格去掉。
  2. 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

图形属性对应视觉编码中的不同元素,大家可以参考我的另一博客 数据可视化中的视觉属性 。

图形属性主要有以下几种。

  1. position:位置,二维坐标系内映射至 x 轴、y 轴;
  2. color:颜色,包含了色调、饱和度和亮度;
  3. size:大小,不同的几何标记对大小的定义有差异;
  4. shape:形状,几何标记的形状决定了某个具体图表类型的表现形式,例如点图,可以使用圆点、三角形、图片表示;线图可以有折线、曲线、点线等表现形式;
  5. opacity:透明度,图形的透明度,这个属性从某种意义上来说可以使用颜色代替,需要使用 'rgba' 的形式,所以在 G2 中我们独立出来。

在构建语法的时候,我们把图形属性绑定一个或者多个数据字段。

坐标系 Coordinates

坐标系是将两种位置标度结合在一起组成的 2 维定位系统,描述了数据是如何映射到图形所在的平面。

G2提供了以下几种坐标系:

coordType 说明
rect 直角坐标系,目前仅支持二维,由 x, y 两个互相垂直的坐标轴构成。
polar 极坐标系,由角度和半径 2 个维度构成。
theta 一种特殊的极坐标系,半径长度固定,仅仅将数据映射到角度,常用于饼图的绘制。
helix 螺旋坐标系,基于阿基米德螺旋线。

分面 Facet

分面,将一份数据按照某个维度分隔成若干子集,然后创建一个图表的矩阵,将每一个数据子集绘制到图形矩阵的窗格中。分面其实提供了两个功能:

  1. 按照指定的维度划分数据集;
  2. 对图表进行排版。

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相关推荐

  1. 五大主流浏览器的内核,前端在IE浏览器中常见的兼容问题

    一 标题五大主流浏览器及其内核 1.Trident 代表作:IE 元老级内核之一,由微软开发,并于1997年10月首次在ie 4.0中使用,凭借其windows垄断优势, Trident市场占有率一直 ...

  2. c++软件开发面试旋极面试题_经典软件开发面试题:浏览器中输入一个网址后发生了什么?...

    经典软件开发面试题:浏览器中输入一个网址后发生了什么? ​ 大家好, 这一期呢,我们来谈一个经典的面试题.这种题目是在浏览器中输入一个网址以后, 会显示一个网页,这期间到底发生了什么? 答案要求说的越 ...

  3. 在浏览器中输入一个域名之后都发生了什么

    当你在浏览器中打入www.baidu.com后,轻轻一敲回车百度输入框就展现在你面前,我们看似很简单很简单的一个操作,背后却有着超级复杂的过程. 其实网络传输跟我们平常说话有许多相似的地方,大脑组织语 ...

  4. 蓝桥杯真题 跳跃 C++、Java实现 动态规划小蓝在一个 n 行 m 列的方格图中玩一个游戏。 开始时,小蓝站在方格图的左上角,即第 1 行第 1 列。

    文章目录 题目描述 输入描述 输出描述 输入输出样例 示例 1 运行限制 代码(c++) 代码(Java) 思路 题目描述 小蓝在一个 n 行 m 列的方格图中玩一个游戏. 开始时,小蓝站在方格图的左 ...

  5. 代码的世界中,一个逻辑套着另外一个逻辑,如何让每一种逻辑在代码中都有迹可循?...

    代码的世界中,一个逻辑套着另外一个逻辑,如何让每一种逻辑在代码中都有迹可循? 这正式框架的意义所在!

  6. 一键设置mac显示选项_如何通过关闭浏览器中的一个选项卡将Mac上的电池寿命延长一倍...

    一键设置mac显示选项 by Primož Cigler 通过PrimožCigler 如何通过关闭浏览器中的一个选项卡将Mac上的电池寿命延长一倍 (How I doubled the batter ...

  7. 算法:小蓝在一个n行 m列的方格图中玩一个游戏

    1.题目描述 小蓝在一个 n 行 m 列的方格图中玩一个游戏. 开始时,小蓝站在方格图的左上角,即第 1 行第 1 列. 小蓝可以在方格图上走动,走动时,如果当前在第 r 行第 c 列,他不能走到行号 ...

  8. c 语言500行小游戏代码,500行代码使用python写个微信小游戏飞机大战游戏.pdf

    500行行代代码码使使用用python写写个个微微信信小小游游戏戏飞飞机机大大战战游游戏戏 这篇文章主要介绍了500行代码使用python写个微信小游戏飞机大战游戏,本文通过实例代码给大家介绍的非常详 ...

  9. c 浏览器语言,让C代码在浏览器中运行

    WebAssembly作为一种新兴的Web技术,相关的资料和社区还不够丰富,但其为web开发提供了一种崭新的思路和工作方式,未来是很有可能大放光彩的. 使用WebAssembly,我们可以在浏览器中运 ...

最新文章

  1. Ubuntu中基于QT的系统网线连接状态的实时监视
  2. java uml 为什么_Java开发为什么需要UML (转)
  3. Java实战应用50篇(二)-SSM框架中的设计模式:动态代理
  4. 数字图像处理:第十九章 立体视觉
  5. python管道通信_Python进程通信之匿名管道实例讲解
  6. 分层结构,协议,接口,服务
  7. 常见的通配符_8、数据库常见操作
  8. VMware ESXi 环境备份与还原处理案例
  9. Fiddler及浏览器开发者工具进行弱网测试
  10. oracle_分区表的新增、修改、删除、合并。普通表转分区表方法
  11. python黑帽子(黑客与渗透测试编程之道)
  12. Oracle MySQL sql 列转行 union all 实现
  13. Ubuntu下codeblocks汉化
  14. 百度地图设置卫星地图显示图文教程
  15. H264三种码率控制方法(CBR, VBR, CVBR)
  16. 无线网服务器连接不上什么原因,无线路由器连接不上是什么原因
  17. 机器学习 卷积神经网络 Convolutional Neural Network(CNN)
  18. Virtualbox虚拟机安装win10系统卡顿
  19. matlab射线平均速度时距曲线,时距曲线实验
  20. Windows7双系统卸载Ubuntu

热门文章

  1. Windows + Eclipse + Gtk 环境(总结)
  2. php查询socket数据包头,php 查询数组值php中关于socket的系列函数总结
  3. python 下标越界_Python中异常处理
  4. 怎么获取web开发怎么获取手机的唯一标识_PYTHON实现北京住宅小区数据抓取-(Web服务API-地点检索服务)
  5. vue each_Vue.js从零开始——模块化项目(2)
  6. ubuntu docker慢_基于docker搭建MulVAL攻击图
  7. 爬虫模拟登陆手机验证码_Python+scrapy爬虫之模拟登陆
  8. 广东轻工计算机多媒体,广东轻工职业技术学院2015年自主招生计算机多媒体技术专业考核大纲...
  9. java 内存溢出 内存泄露_JVM——内存泄漏与内存溢出
  10. tcp当主动发出syn_一文读懂TCP四次挥手工作原理及面试常见问题汇总