文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

如何用前端代码在浏览器中构建一个Tableau

2023-06-19 10:17

关注

如何用前端代码在浏览器中构建一个Tableau,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。

在Gartner最新的对商务智能软件的专业分析报告中,Tableau持续领跑。Microsoft因为PowerBI表现出色也处于领导者象限。而昔日的领导者像SAP,SAS,IBM,MicroStrategy等逐渐被拉开了差距。

如何用前端代码在浏览器中构建一个Tableau

Tableau因为其灵活,出色的数据表现已经成为BI领域里无可争议的领头羊。而其数据驱动的可视化和核心思想是来自于Leland Wilkinson的The Grammar Of Graphics ,同样受到该思想影响的还有R的图形库ggplot。

如何用前端代码在浏览器中构建一个Tableau

在数据可视化开源领域里,大家对百度开发的echarts可谓耳熟能详,echarts经过多年的发展,其功能确实非常强大,可用出色来形容。但是蚂蚁金服开源的基于The Grammar Of Graphics的语法驱动的可视化库G2,让人眼前一亮。那我们就看看如何利用G2和500行左右的纯前端代码来实现一个的类似Tableau的数据分析功能。

数据加载

第一步是加载数据:

如何用前端代码在浏览器中构建一个Tableau

数据加载主要用到了三个库:

数据通过我存放在GitHub中的csv格式的文件,以REST请求的方式来加载。下面的代码把Axios的Promise变成 async/wait方式。

// Ajax async requestconst 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 array  let 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数据集的摘要:

如何用前端代码在浏览器中构建一个Tableau

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查询来对数据进行进一步的加工。

如何用前端代码在浏览器中构建一个Tableau

上图的例子中我们利用限制条件得到一个Iris数据的子集。

另外G2还提供了Dataset的功能:

  • 源数据的解析,将csv, dsv,geojson 转成标准的JSON,查看Connector

  • 加工数据,包括 filter,map,fold(补数据) 等操作,查看 Transform

  • 统计函数,汇总统计、百分比、封箱 等统计函数,查看 Transform

  • 特殊数据处理,包括 地理数据、矩形树图、桑基图、文字云 的数据处理,查看 Transform

数据处理是一个比较大的话题,我们的目标是利用尽可能少的代码完成一个数据分析的工具,所以这一步仅仅是利用alasql提供的SQL查询来处理数据。

数据展示

数据处理好后就是我们的核心内容,数据展示了。

如何用前端代码在浏览器中构建一个Tableau

这一步主要是利用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;}

这里有几点要注意:

这里对于select2的多选,有一个小的提示,在缺省情况下,多选的顺序是固定的顺序,并不依赖选择的顺序,然而许多图形语法和字段的顺序有关,所以我们使用如下的方法来相应select的选择事件。

function updateSelect2Order(evt) {  let element = evt.params.data.element;  let $element = $(element);  $element.detach();  $(this).append($element);  $(this).trigger("change");}

这样做就是每次选中后,把当前选中的项目移到数据最后的位置。

一些例子

好了,下面我们就来看一些例子,了解一下如何使用图形语法来分析和探索数据。

Iris数据集散点图

如何用前端代码在浏览器中构建一个Tableau

图形语法:

g2chart.point().position('Sepal.Length*Petal.Length').color('Species').size('Sepal.Width')
Car数据集折线图

如何用前端代码在浏览器中构建一个Tableau

图形语法:

g2chart.line().position('id*speed');

切换到极坐标:

如何用前端代码在浏览器中构建一个Tableau

图形语法:

g2chart.line().position('id*speed'); g2chart.coord('polar');
Berkeley数据柱状图

如何用前端代码在浏览器中构建一个Tableau

数据处理:

SELECT SUM(Freq) as f , Gender FROM table GROUP BY Gender

图形语法:

g2chart.interval().position('Gender*f').color('Gender').label('f');
Berkeley数据堆叠柱状图

如何用前端代码在浏览器中构建一个Tableau

数据处理:

SELECT SUM(Freq) as f , Gender , Admit FROM table GROUP BY Gender, Admit

图形语法:

g2chart.intervalStack().position('Gender*f').color('Admit')
Berkeley数据饼图

如何用前端代码在浏览器中构建一个Tableau

数据处理:

SELECT SUM(Freq) as f , Gender FROM table GROUP BY Gender

图形语法:

g2chart.intervalStack().position('f').color('Gender').label('f');g2chart.coord('theta')
Berkeley数据分面的应用

如何用前端代码在浏览器中构建一个Tableau

图形语法:

g2chart.facet('rect', {  fields: [ 'Dept', 'Admit' ],  eachView(view) {    view.coord('theta');    view.intervalStack().position('Freq').color('Gender');  }});

更多的分析图形留给大家去尝试

本文分享了一个利用纯前端技术构建一个类似Tableau的BI应用的例子,整个代码统计:

总计474 行,用这么少的代码就能完成一个看上去还不错的BI工具,还算不错吧。当然这里主要是由于开源社区提供了这么多好的前端库以供应用,我要做的仅仅是让它们有效的工作在一起。这个只能算是个原型,从功能和质量上来说都不成熟,但是能在浏览器中不借助任何的服务器来实现BI的数据分析功能,应该会有很多人想要在自己的应用中嵌一个吧?

看完上述内容,你们掌握如何用前端代码在浏览器中构建一个Tableau的方法了吗?如果还想学到更多技能或想了解更多相关内容,欢迎关注编程网行业资讯频道,感谢各位的阅读!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     807人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     351人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     314人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     433人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     221人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯