整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

JavaFX中TableView的CSS相关设置

、特殊的table设置

  • TableView的单元之间去掉行横线
.table-view .table-row-cell {  
    -fx-background-insets: 0;  
}  
  • TableView的单元之间去掉没有数据的竖线
table-row-cell:empty .table-cell {  
    -fx-border-width: 0px;  
}  
  • TableView的单元之间去掉竖线
table-row-cell .table-cell {  
    -fx-border-width: 0px;  
}  
  • TableView的TableColumn的列头设置
.table-view .column-header{
    -fx-border-color:white lightgray white white;  
}
  • table的空闲的列头设置
.table-view .filler{
     -fx-background-color: white;    
}
  • table的列首背景设置,其中包括column-header,filler,MenuButton
.table-view .column-header-background{   
   -fx-background-color: white;    
}
  • table的垂直滚动条设置
.table-view > .virtual-flow > .scroll-bar:vertical{
    -fx-background-insets: 0, 0 0 0 1;
    -fx-padding: -1 -1 -1 0;
}
  • table的水平滚动条设置
.table-view > .virtual-flow > .scroll-bar:horizontal{
    -fx-background-insets: 0, 1 0 0 0;
    -fx-padding: 0 -1 -1 -1;
}
  • table的边角设置
.table-view > .virtual-flow > .corner {
    -fx-background-color: derive(-fx-base,-1%); /*-fx-base 是modena 预先定义的颜色*/
}
  • 选择一行
.table-view:focused > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled:selected{
    -fx-background: -fx-selection-bar;
    -fx-table-cell-border-color: derive(-fx-selection-bar, 20%);
}
  • 当控件没有聚焦时选择
.table-row-cell:filled > .table-cell:selected{
    -fx-background: -fx-selection-bar-non-focused;
    -fx-table-cell-border-color: derive(-fx-selection-bar-non-focused, 20%);
}
  • 聚焦的单元 (键盘导航)
.table-view:focused:cell-selection > .virtual-flow > .clipped-container > .sheet > .table-row-cell > .table-cell:focused{
    -fx-background-color: -fx-background, -fx-cell-focus-inner-border, -fx-background;
    -fx-background-insets: 0, 1, 2;
}
.table-view{
    /* Constants used throughout the tableview. */
    -fx-table-header-border-color: -fx-box-border;
    -fx-table-cell-border-color: derive(-fx-color,5%);
}

二、TableView tableRow编辑

  • tableRow
/* Each row in the table is a table-row-cell. Inside a table-row-cell is any number of table-cell. */
.table-row-cell {
    -fx-background: -fx-control-inner-background;
    -fx-background-color: -fx-table-cell-border-color, -fx-background;
    -fx-background-insets: 0, 0 0 1 0;
    -fx-padding: 0;
    -fx-text-fill: -fx-text-background-color;
}
  • tableRow单元单数tableCell
.table-row-cell:odd {
    -fx-background: -fx-control-inner-background-alt;
}

三、TableView 单元tableCell编辑

  • tableCell
.table-cell {
    -fx-padding: 0.166667em; /* 2px, plus border adds 1px */
    -fx-background-color: null;
    -fx-border-color: transparent -fx-table-cell-border-color transparent transparent;
    -fx-cell-size: 2.0em; /* 24 */
    -fx-text-fill: -fx-text-background-color;
}
  • tableCell选择单元
.table-view > .virtual-flow > .clipped-container > .sheet > .table-row-cell .table-cell:selected {
    -fx-background-color: -fx-table-cell-border-color, -fx-background;
    -fx-background-insets: 0, 0 0 1 0;
}
  • tableCell最右可视单元
/* When in constrained resize mode, the right-most visible cell should not have
   a right-border, as it is not possible to get this cleanly out of view without
   introducing horizontal scrollbars (see RT-14886). */
.table-view:constrained-resize > .virtual-flow > .clipped-container > .sheet > .table-row-cell > .table-cell:last-visible {
    -fx-border-color: transparent;
}

四、TableView列头编辑

  • TableView列大小重新调整线
/* The column-resize-line is shown when the user is attempting to resize a column. */
.table-view .column-resize-line {
    -fx-background: -fx-accent;
    -fx-background-color: -fx-background;
    -fx-padding: 0.0em 0.0416667em 0.0em 0.0416667em; /* 0 0.571429 0 0.571429 */
}
  • TableView列头背景
/* This is the area behind the column headers. An ideal place to specify background
   and border colors for the whole area (not individual column-header's). */
.table-view .column-header-background{
    -fx-background-color: -fx-inner-border, -fx-body-color;
    -fx-background-insets: 0, 1;
}
  • TableView列头行设置背景
/* The column header row is made up of a number of column-header, one for each
   TableColumn, and a 'filler' area that extends from the right-most column
   to the edge of the tableview, or up to the 'column control' button. */
.table-view .column-header,
.table-view .filler,
.table-view > .column-header-background > .show-hide-columns-button,
.table-view:constrained-resize .filler{
    -fx-background-color: -fx-box-border, -fx-inner-border, -fx-body-color;
    -fx-background-insets: 0, 0 1 1 0, 1 2 2 1;
    -fx-font-weight: bold;
    -fx-size: 2em;
    -fx-text-fill: -fx-selection-bar-text;
    -fx-padding: 0.166667em;
}
  • TableView空闲列
.table-view .filler,
.table-view:constrained-resize .filler{
    -fx-background-insets: 0, 0 0 1 0, 1 1 2 1;
}
  • TableView列头展示隐藏列按钮
.table-view > .column-header-background > .show-hide-columns-button {
    -fx-background-insets: 0, 0 0 1 1, 1 1 2 2;
}
  • TableView列头排序顺序点容器
.table-view .column-header .sort-order-dots-container{
    -fx-padding: 2 0 2 0;
}
  • TableView列头排序顺序
.table-view .column-header .sort-order{
    -fx-font-size: 0.916667em; /* 11pt - 1 less than the default font */
}
  • TableView列头排序顺序点
.table-view .column-header .sort-order-dot {
    -fx-background-color: -fx-mark-color;
    -fx-padding: 0.115em;
    -fx-background-radius: 0.115em;
}
  • TableView列头文本标签
.table-view .column-header .label{
    -fx-alignment: center;
}
/* Plus Symbol */
.table-view .show-hide-column-image,
 {
    -fx-background-color: -fx-mark-color;
    -fx-padding: 0.25em; /* 3px */
    -fx-shape: "M398.902,298.045c0.667,0,1.333,0,2,0c0,0.667,0,1.333,0,2c0.667,0,1.333,0,2,0c0,0.667,0,1.333,0,2c-0.667,0-1.333,0-2,0c0,0.666,0,1.332,0,1.999c-0.667,0-1.333,0-2,0c0-0.667,0-1.333,0-1.999c-0.666,0-1.333,0-1.999,0c0-0.667,0-1.334,0-2c0.666,0,1.333,0,1.999,0C398.902,299.378,398.902,298.711,398.902,298.045z"; 
}
  • TableView列拖拽头留下的空闲区
/* When a column is being 'dragged' to be placed in a different position, there
   is a region that follows along the column header area to indicate where the
   column will be dropped. This region can be styled using the .column-drag-header
   name. */
.table-view .column-drag-header {
    -fx-background: -fx-accent;
    -fx-background-color: -fx-selection-bar;
    -fx-border-color: transparent;
    -fx-opacity: 0.6;
}
  • TableView当前正在移动且半透明覆盖的列
/* Semi-transparent overlay to indicate the column that is currently being moved */
.table-view .column-overlay{
    -fx-background-color: darkgray;
    -fx-opacity: 0.3;
}
  • TableView列头排序箭头
/* Header Sort Arrows */
.table-view /*> column-header-background > nested-column-header >*/ .arrow{
    -fx-background-color: -fx-mark-color;
    -fx-padding: 0.25em 0.3125em 0.25em 0.3125em; /* 3 3.75 3 3.75 */
    -fx-shape: "M 0 0 h 7 l -3.5 4 z";
}
  • TableView没有行列

TableView简介

QTableView是模型-视图(Model-View)框架类之一,是Qt模型-视图框架的组成部分,它实现了一个表格视图。在一个应用需要和一批数据进行交互,需要以表格形式输出这些信息的时候,QTableView是最合适的选择。

QTableView实现了QAbstractItemView类定义的接口,因此它能够显示从QAbstractItemModel类派生的模型提供的数据。

我们可以通过使用鼠标单击某个单元格或者使用箭头来导航表格视图中的单元格。QTableView拥有一个水平表头和垂直标表头。表格视图中显示的条目与其他视图中的条目一样,使用标准委托类来渲染和编辑。

QTableView常用方法:

  • setModel(): 设置视图的Model类;
  • horizontalHeader(): 获得水平表头;
  • verticalHeader(): 获得垂直表头;
  • rowHeight(): 获得每一行的高度;
  • columnWidth(): 获得列的宽度;
  • hideRow(): 隐藏指定行;
  • showRow(): 显示指定行;
  • hideColumn(): 隐藏指定列;
  • showColumn(): 显示指定列;
  • selectRow(): 选择指定行;
  • selectColumn(): 选择指定列。

QTableView类继承关系:

测试QTableView

在测试代码中,我们使用QStandardItemModel作为QTableView的模型类, 对视图的属性做了一些限制(只能选中一行,只允许行选中模式,不可编辑等等)。当我们选中某一行是,在状态条上添加信息显示,在菜单栏实现了添加行和删除行的功能。完整代码如下:

import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt,QItemSelection, QItemSelectionModel, QModelIndex
from PyQt5.QtGui import QStandardItemModel, QStandardItem
from PyQt5.QtWidgets import (QApplication, QMainWindow, QTableView,
                             QAbstractItemView, QHeaderView, QMenu, QMenuBar, QAction)
 
class DemoTableView(QMainWindow):
    def __init__(self, parent=None):
        super(DemoTableView, self).__init__(parent)   
        
         # 设置窗口标题
        self.setWindowTitle('实战PyQt5: QTableView 演示')      
        # 设置窗口大小
        self.resize(520, 360)
      
        self.initUi()
        
    def initUi(self):
        
        #行数和列数
        self.rows = 8
        self.cols = 4
        
        #设置水平表头信息
        model = QStandardItemModel(self.rows, self.cols, self)
        hTitle=[]
        for col in range(self.cols):
            hTitle.append('第{}列'.format(col+1))
        model.setHorizontalHeaderLabels(hTitle)
        #设置垂直表头信息
        vTitle=[]
        for row in range(self.rows):
            vTitle.append('第{}行'.format(row+1))
        model.setVerticalHeaderLabels(vTitle)
        
        #设置Item里的内容
        for row in range (self.rows):
            for column in range (self.cols):
                item = QStandardItem('(row %s, column %s)'%(row, column))
                model.setItem(row, column, item)
        
        tableView = QTableView(self)
        tableView.setModel(model)
        #设置只能选中一行
        tableView.setSelectionMode(QAbstractItemView.SingleSelection)
        #不可编辑
        tableView.setEditTriggers(QTableView.NoEditTriggers)
        #设置只有行选中
        tableView.setSelectionBehavior(QAbstractItemView.SelectRows)
        #所有列自动拉伸,充满界面
        tableView.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
        
        #显示选中行的信息
        tableView.selectionModel().currentChanged.connect(self.onCurrentChanged)
                
        self.model = model
        self.tableView = tableView
        self.setCentralWidget(tableView)
    
        #添加菜单项
        menuBar = self.menuBar()
        fileMenu = menuBar.addMenu('文件')
        editMenu = menuBar.addMenu('编辑')
        
        #退出应用
        appExit = QAction('退出', self)
        appExit.triggered.connect(self.close)
        fileMenu.addAction(appExit)
        
        #添加一行
        editAppend = QAction('添加', self)
        editAppend.triggered.connect(self.appendRow)
        #删除一行
        editRemove = QAction('删除', self)
        editRemove.triggered.connect(self.removeRow)
        editMenu.addAction(editAppend)
        editMenu.addAction(editRemove)
        
    def appendRow(self):
        row = self.model.rowCount()
        items = list()
        for col in range (self.cols):
            items.append(QStandardItem('(row %s, column %s)'%(row, col)))
        #添加一行
        self.model.appendRow(items)
        #更新头信息
        self.model.setVerticalHeaderItem(row, QStandardItem('第{}行'.format(row+1)))
    
    def removeRow(self):
        #获取选中的行
        sel = self.tableView.selectionModel().selectedRows()
        print(sel)
        if sel:
            #下面删除时,选中多行中的最后一行,会被删掉;不选中,则默认第一行删掉
            index=self.tableView.currentIndex()
            print(index.row())
            self.model.removeRow(index.row())
            
    def onCurrentChanged(self,current, previous):
        #初始化时,previous.row() = -1,不显示信息
        if int(previous.row() < 0):
             return
        self.statusBar().showMessage('选中第{}行'.format(current.row()+1))
    
if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = DemoTableView()
    window.show()
    sys.exit(app.exec()) 

运行结果如下图:

测试QTableView

本文知识点

  • QTableView的行和列的属性设置;
  • QTableView的选择属性的设置;
  • QTableView水平和垂直表头信息设置;
  • 在QTableView中添加或者删除一行。

喜欢本文内容就关注, 收藏,点赞,评论和转发。

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

结合我之前分享的TensorflowJS的文章,下面一步可能是加入预测功能,为数据分析加入智能,前端应用的前景,不可限量!

参考

  • axios 基于Promise的HTTP客户端
  • alasql 基于JS的开源SQL数据库
  • jquery datatable JQuery的数据表格插件
  • select2 JQuery的选择控件插件
  • 相关文章 再谈使用开源软件搭建数据分析平台
  • 相关文章 使用开源软件快速搭建数据分析平台
  • 相关文章 高维数据可视化图形语法指南