千家信息网

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

发表于:2025-12-01 作者:千家信息网编辑
千家信息网最后更新 2025年12月01日,如何用前端代码在浏览器中构建一个Tableau,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。在Gartner最新的对商务智能软件的专业分
千家信息网最后更新 2025年12月01日如何用前端代码在浏览器中构建一个Tableau

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

在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 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数据集的摘要:

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的数据分析功能,应该会有很多人想要在自己的应用中嵌一个吧?

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

数据 图形 语法 代码 方法 维度 图表 坐标 处理 坐标系 属性 数据处理 类型 分析 支持 前端 几何 功能 字段 查询 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 jdbc进行连接数据库查询方法 网络安全如何规划 校园网络安全读后感200字 考虑网络安全因素 杭州华为网络安全工程师 怎么拉取服务器上udp的流 图书借阅管理系统数据库设计总结 青少年网络安全法则 河北计算机网络技术大赛 网络安全防护资质 考研的数据库需要考什么 数据库新建连接测试失败 贝宝中国服务器网 杭州乐洋网络技术有限公司 怎样判断数据库主键 数据库恢复技术大致可以分为三种 云服务器与实体服务器优劣 舟山提供网络技术咨询热线 备品备件管理数据库设计 常熟创新软件开发诚信合作 数据库检索报告 电子版的网络安全宣讲稿 网络安全报告会心得体会 局域网设备怎么接入远程服务器 网络安全语言学什么web 甘谷网络安全指导 搜索引擎用到的数据库技术 无线网络技术与应用论文 山东特亿宝互联网科技 数据库批量附加工具
0