用500行纯前端代码在浏览器中构建一个Tableau

用500行纯前端代码在浏览器中构建一个Tableau

2018/05/16 · 基础技术 ·
BI,
Tableau,
数据可视化

原文出处: naughty   

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

图片 1

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

图片 2

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

  • 演示参见 
  • 代码参见 https://gist.github.com/gangtao/e053cf9722b64ef8544afa371c2daaee 

 

现在整理下发出来,希望大家共同学习吧

数据加载

第一步是加载数据:

图片 3

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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);

1
let csv = await request.get(url);

这一步可能会遇到跨域请求的问题,github上的文件支持跨域。

把数据存储在一个SQL数据库中,这样做的好处是为了下一步做数据准备的时候,可以方便的利用SQL来进行查询和分析。

JavaScript

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(/(?<=FROMs+)w+/, “CSV(?)”); const query_str =
sql.replace(“table”, “CSV(?)”); return await alasql.promise(query_str,
[this.data]); } }

1
2
3
4
5
6
7
8
9
10
11
12
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(/(?<=FROMs+)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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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中的列名都不显示点,但实际数据并没有改变。

 

先看效果图:
图片 4
用了jquery.image-maps.js这个插件 下载地址
//www.jb51.net/jiaoben/57930.html
原理是:
通过拖动计算出当前热区可移动模块的left top right bottom
对应area的 coords 属性集成上面的位置,就可以实现热区了。
对应的模块代码是:

数据准备

数据加载完毕,我们来到第二步的数据准备阶段。数据准备是数据科学项目最花时间的一步,通常需要对数据进行大量的清洗,变形,抽取等工作,使得数据变得可用。

在这一步我们做了两件事:

一是显示数据的一个摘要,让我们初步了解数据的概貌,为进一步的数据变形和处理做好准备。

这个是Iris数据集的摘要:

图片 5

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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查询来对数据进行进一步的加工。

图片 6

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

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

  • 源数据的解析,将csv, dsv,geojson
    转成标准的JSON,查看Connector
  • 加工数据,包括 filter,map,fold(补数据)
    等操作,查看 Transform
  • 统计函数,汇总统计、百分比、封箱
    等统计函数,查看 Transform
  • 特殊数据处理,包括 地理数据、矩形树图、桑基图、文字云
    的数据处理,查看 Transform

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

 

复制代码 代码如下:

数据展示

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

图片 7

这一步主要是利用select2提供的选择控件构建图形语法来驱动数据展示。如上图所示,对应的G2代码图形语法为:

g2chart.facet(‘rect’, { fields: [ ‘Admit’, ‘Dept’ ], eachView(view) {
view.interval().position(‘Gender*Freq’).color(‘Gender’).label(‘Freq’);
} });

1
2
3
4
5
6
g2chart.facet(‘rect’, {
  fields: [ ‘Admit’, ‘Dept’ ],
  eachView(view) {
    view.interval().position(‘Gender*Freq’).color(‘Gender’).label(‘Freq’);
  }
});

图形语法主要包含以下几个主要的元素:

 

<!–模块展示 begin–>
<div class=”modeShow”>
<div id=”debug”></div>
<div class=”imgMap mapBox”>
<img src=”../images/hot_images_map.png” name=”test” border=”0″
usemap=”#Map1″ width=”980″ height=”450″ ref=’imageMaps’ />
<map name=”Map1″>
<area shape=”rect” coords=”300,80,500,150″ href=”mall.10010.com”
/>
</map>
</div>
</div>
<!–模块展示 end—>

几何标记 Geometry

几何标记定义了使用什么样的几何图形来表征数据。G2现在支持如下这些几何标记:

geom 类型 描述
point 点,用于绘制各种点图。
path 路径,无序的点连接而成的一条线,常用于路径图的绘制。
line 线,点按照 x 轴连接成一条线,构成线图。
area 填充线图跟坐标系之间构成区域图,也可以指定上下范围。
interval 使用矩形或者弧形,用面积来表示大小关系的图形,一般构成柱状图、饼图等图表。
polygon 多边形,可以用于构建色块图、地图等图表类型。
edge 两个点之间的链接,用于构建树图和关系图中的边、流程图中的连接线。
schema 自定义图形,用于构建箱型图(或者称箱须图)、蜡烛图(或者称 K 线图、股票图)等图表。
heatmap 用于热力图的绘制。

这里要注意,intervalstack是官方支持的,但是文档没有提到,在阅读G2的API文档的时候,我也发现文档讲的不是很清楚,有很多地方没有讲清楚如何使用API。这也是开源软件值得改进的地方。

 

js代码:

图形属性 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 螺旋坐标系,基于阿基米德螺旋线。

 

(function($) {
jQuery.fn.imageMaps = function(setting) {
var $container = this;
if ($container.length == 0) return false;
$container.each(function(){
var container = $(this);
var $images = container.find(‘img[ref=imageMaps]’);
$images.wrap(‘<div class=”image-maps-conrainer
image-maps-conrainerEdit”
style=”position:relative;”></div>’).css(‘border’,’1px solid
#ccc’);
$images.each(function(){
var _img_conrainer = $(this).parent();
_img_conrainer.append(‘<div class=”button-conrainer”><a
href=”javascript:void(0)” class=”addHot”>添加热点</a><a
href=”javascript:void(0)” class=”addImg”>上传图片</a><a
class=”delSub delMode”
href=”javascript:void(0)”>删除×</a></div>’).append(‘<div
class=”link-conrainer”><ul></ul><div
class=”clr”></div></div><div
class=”clr”></div><span
class=”numFloor”>模块-1</span>’).append($.browser.msie ?
$(‘<div class=”position-conrainer”
style=”position:absolute”></div>’).css({
background:’#fff’,
opacity:0
}) : ‘<div class=”position-conrainer”
style=”position:absolute”></div>’);
var _img_offset = $(this).offset();
var _img_conrainer_offset = _img_conrainer.offset();
_img_conrainer.find(‘.position-conrainer’).css({
top: _img_offset.top – _img_conrainer_offset.top,
left: _img_offset.left – _img_conrainer_offset.left,
width:$(this).width(),
height:$(this).height(),
border:’1px solid transparent’
});
var map_name = $(this).attr(‘usemap’).replace(‘#’,”);
if(map_name !=”){
var index = 1;
var _link_conrainer = _img_conrainer.find(‘.link-conrainer ul’);
var _position_conrainer =
_img_conrainer.find(‘.position-conrainer’);
var image_param = $(this).attr(‘name’) == ” ? ” : ‘[‘+
$(this).attr(‘name’) + ‘]’;
container.find(‘map[name=’+map_name+’]’).find(‘area[shape=rect]’).each(function(){
var coords = $(this).attr(‘coords’);
$(this).attr(‘ref’,”1″);
_link_conrainer.append(‘<li ref=”‘+index+'”
class=”map-link”><span
class=”link-number-text”>热点’+index+'</span>: <input
type=”text” size=”60″ name=”link’+index+'” class=”linkHref”
value=”‘+$(this).attr(‘href’)+'” /><input type=”hidden”
class=”rect-value” name=”rect’+index+'” value=”‘+coords+'”
/></li>’);
coords = coords.split(‘,’);
_position_conrainer.append(‘<div ref=”‘+index+'”
class=”map-position”
style=”left:’+coords[0]+’px;top:’+coords[1]+’px;width:’+(coords[2]-coords[0])+’px;height:’+(coords[3]-coords[1])+’px;”><div
class=”map-position-bg”></div><span
class=”link-number-text”>热点’+index+'</span><span
class=”delete”>X</span><span
class=”resize”></span></div>’);
index++;
});
}
});
});
$container.find(‘.button-conrainer .addHot’).live(“click”,function(){
var _link_conrainer = $(this).parent().parent().find(‘.link-conrainer
ul’);
var _position_conrainer =
$(this).parent().parent().find(‘.position-conrainer’);
var index = _link_conrainer.find(‘.map-link’).length +1;
var _coordsMap = $(this).parent().parent().next(‘map’);
var image =
$(this).parent().parent().find(‘img[ref=imageMaps]’).attr(‘name’);
image = (image == ” ? ” : ‘[‘+ image + ‘]’);
_link_conrainer.append(‘<li ref=”‘+index+'”
class=”map-link”><span
class=”link-number-text”>热点’+index+'</span>: <input
type=”text” size=”60″ name=”link’+index+'” class=”linkHref” value=””
/><input type=”hidden” class=”rect-value” name=”rect’+index+'”
value=”300,80,500,150″ /></li>’);
_position_conrainer.append(‘<div ref=”‘+index+'”
class=”map-position”
style=”left:300px;top:80px;width:200px;height:70px;”><div
class=”map-position-bg”></div><span
class=”link-number-text”>热点’+index+'</span><span
class=”delete”>X</span><span
class=”resize”></span></div>’);
var coords = _link_conrainer.find(‘input[name=rect’+ index
+’]’).val();
_coordsMap.append(‘<area ref=”‘+index+'” href=”” coords=”‘+ coords
+'” shape=”rect”>’);
$(“input[name=’link”+index+”‘]”).val(“请输入本热点对应的链接地址”);
bind_map_event();
define_css();
//添加map热区
});
//修改链接地址
$(“.linkHref”).live(“blur”,function(){
var valueHref = $(this).val();
var thisRef = $(this).parent().attr(‘ref’);
var appArea = $(this).parents(“.link-conrainer”).parent().next(‘map’);
$(this).val(valueHref);
appArea.find(‘area[ref=’+thisRef+’]’).attr(“href”,valueHref);
});
//绑定map事件
function bind_map_event(){
$(‘.position-conrainer .map-position
.map-position-bg’).each(function(){
var map_position_bg = $(this);
var conrainer = $(this).parent().parent();
map_position_bg.unbind(‘mousedown’).mousedown(function(event){
map_position_bg.data(‘mousedown’, true);
map_position_bg.data(‘pageX’, event.pageX);
map_position_bg.data(‘pageY’, event.pageY);
map_position_bg.css(‘cursor’,’move’);
return false;
}).unbind(‘mouseup’).mouseup(function(event){
map_position_bg.data(‘mousedown’, false);
map_position_bg.css(‘cursor’,’default’);
return false;
});
conrainer.mousemove(function(event){
if (!map_position_bg.data(‘mousedown’)) return false;
var dx = event.pageX – map_position_bg.data(‘pageX’);
var dy = event.pageY – map_position_bg.data(‘pageY’);
if ((dx == 0) && (dy == 0)){
return false;
}
var map_position = map_position_bg.parent();
var p = map_position.position();
var left = p.left+dx;
if(left <0) left = 0;
var top = p.top+dy;
if (top < 0) top = 0;
var bottom = top + map_position.height();
if(bottom > conrainer.height()){
top = top-(bottom-conrainer.height());
}
var right = left + map_position.width();
if(right > conrainer.width()){
left = left-(right-conrainer.width());
}
map_position.css({
left:left,
top:top
});
map_position_bg.data(‘pageX’, event.pageX);
map_position_bg.data(‘pageY’, event.pageY);
bottom = top + map_position.height();
right = left + map_position.width();
var newArea = new Array(left,top,right,bottom).join(‘,’);
var mapApp = conrainer.parent().next(‘map’);
mapApp.find(‘area[ref=’+map_position.attr(‘ref’)+’]’).attr(“coords”,newArea);
$(‘.link-conrainer li[ref=’+map_position.attr(‘ref’)+’]
.rect-value’).val(newArea);
return false;
}).mouseup(function(event){
map_position_bg.data(‘mousedown’, false);
map_position_bg.css(‘cursor’,’default’);
return false;
});
});
$(‘.position-conrainer .map-position .resize’).each(function(){
var map_position_resize = $(this);
var conrainer = $(this).parent().parent();
map_position_resize.unbind(‘mousedown’).mousedown(function(event){
map_position_resize.data(‘mousedown’, true);
map_position_resize.data(‘pageX’, event.pageX);
map_position_resize.data(‘pageY’, event.pageY);
return false;
}).unbind(‘mouseup’).mouseup(function(event){
map_position_resize.data(‘mousedown’, false);
return false;
});
//点击取消拖动
conrainer.unbind(‘click’).click(function(event){
map_position_resize.data(‘mousedown’, false);
return false;
});
conrainer.mousemove(function(event){
if (!map_position_resize.data(‘mousedown’)) return false;
var dx = event.pageX – map_position_resize.data(‘pageX’);
var dy = event.pageY – map_position_resize.data(‘pageY’);
if ((dx == 0) && (dy == 0)){
return false;
}
var map_position = map_position_resize.parent();
var p = map_position.position();
var left = p.left;
var top = p.top;
var height = map_position.height()+dy;
if((top+height) > conrainer.height()){
height = height-((top+height)-conrainer.height());
}
if (height <20) height = 20;
var width = map_position.width()+dx;
if((left+width) > conrainer.width()){
width = width-((left+width)-conrainer.width());
}
if(width <50) width = 50;
map_position.css({
width:width,
height:height
});
map_position_resize.data(‘pageX’, event.pageX);
map_position_resize.data(‘pageY’, event.pageY);
bottom = top + map_position.height();
right = left + map_position.width();
var newArea = new Array(left,top,right,bottom).join(‘,’);
var mapApp = conrainer.parent().next(‘map’);
mapApp.find(‘area[ref=’+map_position.attr(‘ref’)+’]’).attr(“coords”,newArea);
$(‘.link-conrainer li[ref=’+map_position.attr(‘ref’)+’]
.rect-value’).val(newArea);
return false;
}).mouseup(function(event){
map_position_resize.data(‘mousedown’, false);
return false;
});
});
$(‘.position-conrainer .map-position
.delete’).unbind(‘click’).click(function(){
var ref = $(this).parent().attr(‘ref’);
var _link_conrainer =
$(this).parent().parent().parent().find(‘.link-conrainer ul’);
var _coordsMap = $(this).parent().parent().parent().next(‘map’);
var _position_conrainer =
$(this).parent().parent().parent().find(‘.position-conrainer’);
_link_conrainer.find(‘.map-link[ref=’+ref+’]’).remove();
_position_conrainer.find(‘.map-position[ref=’+ref+’]’).remove();
_coordsMap.find(‘area[ref=’+ref+’]’).remove();
var index = 1;
_link_conrainer.find(‘.map-link’).each(function(){
$(this).attr(‘ref’,index).find(‘.link-number-text’).html(‘热点’+index);
index ++;
});
index = 1;
_position_conrainer.find(‘.map-position’).each(function(){
$(this).attr(‘ref’,index).find(‘.link-number-text’).html(‘热点’+index);
index ++;
});
index = 1;
_coordsMap.find(‘area’).each(function(){
$(this).attr(‘ref’,index);
index ++;
});
});
}
bind_map_event();
function define_css(){
//样式定义
$container.find(‘.map-position .resize’).css({
display:’block’,
position:’absolute’,
right:0,
bottom:0,
width:5,
height:5,
cursor:’nw-resize’,
background:’#000′
});
}
define_css();
};
})(jQuery);

分面 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; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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”); }

1
2
3
4
5
6
7
function updateSelect2Order(evt) {
  let element = evt.params.data.element;
  let $element = $(element);
  $element.detach();
  $(this).append($element);
  $(this).trigger("change");
}

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

 

页面引用:$(‘.imgMap’).imageMaps();

一些例子

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

 

相关文章

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*
*
Website