您的位置:澳门新葡8455最新网站 > Web前端 > 基于jquery自定义图片热区效果,用500行纯前端代

基于jquery自定义图片热区效果,用500行纯前端代

发布时间:2019-12-13 04:15编辑:Web前端浏览(62)

    用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这个插件 下载地址
    原理是:
    通过拖动计算出当前热区可移动模块的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();

    一些例子

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

     

    先看效果图: 用了jquery.image-maps.js这个插件 下载地址 原理是: 通过...

    Iris数据集散点图

    图片 8

    图形语法:

    g2chart.point().position('Sepal.Length*Petal.Length').color('Species').size('Sepal.Width')

    1
    g2chart.point().position('Sepal.Length*Petal.Length').color('Species').size('Sepal.Width')

     

    Car数据集折线图

    图片 9

    图形语法:

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

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

    切换到极坐标:

    图片 10

    图形语法:

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

    1
    2
    g2chart.line().position('id*speed');
    g2chart.coord('polar');

     

    Berkeley数据柱状图

    图片 11

    数据处理:

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

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

    图形语法:

    g2chart.interval().position('Gender*f').color('Gender').label('f');

    1
    g2chart.interval().position('Gender*f').color('Gender').label('f');

     

    Berkeley数据堆叠柱状图

    图片 12

    数据处理:

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

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

    图形语法:

    g2chart.intervalStack().position('Gender*f').color('Admit')

    1
    g2chart.intervalStack().position('Gender*f').color('Admit')

     

    Berkeley数据饼图

    图片 13

    数据处理:

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

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

    图形语法:

    g2chart.intervalStack().position('f').color('Gender').label('f'); g2chart.coord('theta')

    1
    2
    g2chart.intervalStack().position('f').color('Gender').label('f');
    g2chart.coord('theta')

     

    Berkeley数据分面的应用

    图片 14

    图形语法:

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

    1
    2
    3
    4
    5
    6
    7
    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的选择控件插件
    • 相关博客 使用开源软件快速搭建数据分析平台 
    • 相关博客 数据可视化中的视觉属性

      2 赞 1 收藏 评论

    图片 15

    本文由澳门新葡8455最新网站发布于Web前端,转载请注明出处:基于jquery自定义图片热区效果,用500行纯前端代

    关键词: