整合营销服务商

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

免费咨询热线:

Python入坑系列-Pyside6桌面编程之Lay

Python入坑系列-Pyside6桌面编程之Layout设置精美布局效果

过本文章,你可以掌握以下内容:

  1. Pyside6 Layout介绍
  2. Pyside6 Layout布局解释及示例
  3. 自定义Layout,实现部件自动换行

1、Pyside6 Layout介绍

QtWidgets.QLayout是Qt中用于管理窗口或对话框中小部件(控件)布局的基类。它是一个抽象基类,定义了所有布局类共有的接口和行为。QLayout及其子类的主要作用是自动管理小部件的位置和大小,以便于创建整洁和灵活的图形用户界面(GUI),对于复杂界面布局是很重要的,主要作用包括:

  • 自动管理小部件位置:QLayout自动计算小部件的位置,使得小部件根据布局规则排列,无需手动指定每个小部件的具体位置。
  • 自动调整大小:当窗口大小变化时,QLayout能够自动调整其管理的小部件的大小和位置,确保布局的一致性和响应性。
  • 简化界面设计:通过使用布局,开发者可以更加专注于界面的结构设计,而不是具体的位置和大小调整,从而简化了界面设计过程。
  • 支持嵌套:布局可以嵌套使用,即一个布局可以包含其他布局,这允许创建复杂的界面结构

Layout继承关系图

2、Pyside6 Layout布局解释及示例

以下是继承自QtWidgets.QLayout的布局,每种布局有对应的行为。

布局

行为

对应html

QHBoxLayout

线性水平布局

类似于display: flex; flex-direction: row;的Flexbox布局

QVBoxLayout

线性垂直布局

类似于display: flex; flex-direction: column;的Flexbox布局

QGridLayout

在可转位网格 XxY 中

类似于html的table行和列

QStackedLayout

堆叠 (z) 于彼此前面

类似于css的z-index,并控制隐藏显示效果

QHBoxLayout

QHBoxLayout是Qt中的一个布局管理器类,用于按水平方向排列小部件。它继承自QLayout,提供了一种简便的方式来自动管理窗口或对话框中小部件的位置和大小。使用QHBoxLayout,可以将小部件从左到右依次排列,而不需要手动指定每个小部件的具体位置。

QHBoxLayout继承关系图

简单的示例如下:

import sys
from PySide6.QtWidgets import *
class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()
        self.setWindowTitle("QHBoxLayout之水平布局")
        self.setMinimumSize(400,300)
        layout=QHBoxLayout()
        layout.addWidget(QPushButton("1"))
        layout.addWidget(QPushButton("2"))
        layout.addWidget(QPushButton("3"))
        widget=QWidget()
        widget.setLayout(layout)
        self.setCentralWidget(widget)

if __name__=='__main__':
    app=QApplication(sys.argv)
    window=MainWindow()
    window.show()
    app.exec()

QHBoxLayout示例效果图

QVBoxLayout

QVBoxLayout是Qt中的一个布局管理器类,用于垂直方向排列小部件。它继承自QLayout,提供了一种简便的方式来自动管理窗口或对话框中小部件的位置和大小。使用QVBoxLayout,可以将小部件从上到下依次排列,而不需要手动指定每个小部件的具体位置。

QVBoxLayout继承关系图

简单示例如下:

import sys
from PySide6.QtWidgets import *
class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()
        self.setWindowTitle("QVBoxLayout之垂直布局")
        self.setMinimumSize(400, 300)
        layout=QVBoxLayout()
        layout.addWidget(QPushButton("1"))
        layout.addWidget(QPushButton("2"))
        layout.addWidget(QPushButton("3"))
        widget=QWidget()
        widget.setLayout(layout)
        self.setCentralWidget(widget)

if __name__=='__main__':
    app=QApplication(sys.argv)
    window=MainWindow()
    window.show()
    app.exec()

效果图如下:

QVBoxLayout示例效果图

QGridLayout

QGridLayout是Qt中一个非常强大的布局管理器,它提供一种网格式布局,这种布局由行和列组成(类似table),每个小部件占据网格中的一个或多个单元格。QGridLayout提供了灵活的方式来创建复杂的用户界面,使得小部件的布局可以精确控制,同时也能自动适应窗口大小的变化,主要特性有:

  • 行列管理:可以指定小部件应该放在网格的哪一行哪一列,甚至可以跨越多行多列。
  • 自动调整大小:当窗口大小改变时,QGridLayout会自动调整小部件的大小和位置,保持布局的整洁和一致性。
  • 最小宽度和拉伸因子:每列(或行)可以有一个最小宽度和一个拉伸因子,这决定了在可用空间中它们将如何分配额外的空间。
  • 间距和边距:可以设置小部件之间的间距(spacing())和布局边缘的边距(内容边距),以控制布局的外观。

QGridLayout继承关系图

示例代码如下:

import sys
from PySide6.QtWidgets import *
class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()
        self.setWindowTitle("QGridLayout之网格布局")
        self.setMinimumSize(400, 300)
        layout=QGridLayout()
        # 为窗口部件设置样式表,添加边框
        self.setStyleSheet("QWidget { border: 2px solid black; }")
        layout.addWidget(QLabel('第0行第0列'), 0, 0)
        layout.addWidget(QLabel('第0行第1列'), 0, 1)
        layout.addWidget(QLabel('第1行第0列'), 1, 0)
        layout.addWidget(QLabel('第1行第1列'), 1, 1)
        widget=QWidget()
        widget.setLayout(layout)
        self.setCentralWidget(widget)

if __name__=='__main__':
    app=QApplication(sys.argv)
    window=MainWindow()
    window.show()
    app.exec()

效果图如下:

QGridLayout示例效果图

QStackedLayout

QStackedLayout是Qt中的一个布局管理器,它可以在相同的空间内堆叠多个小部件,但一次只显示一个小部件。这种布局非常适合用于实现向导、选项卡和其他需要在多个页面之间切换的界面。

QStackedLayout继承关系图

主要特性:

  • 堆叠小部件:在同一个布局空间内堆叠多个小部件。
  • 单一可见性:一次只有一个小部件可见。
  • 动态切换:可以编程方式动态切换当前可见的小部件

常用方法技巧:indexOf()函数返回小部件在该列表中的索引。可以使用addWidget()函数添加小部件到列表末尾,或者使用insertWidget()函数在给定索引处插入。removeWidget()函数从布局中移除给定索引的小部件。可以使用count()函数获取布局中包含的小部件数量。widget()函数返回给定索引位置的小部件。当前显示在屏幕上的小部件的索引由currentIndex()给出,并且可以使用setCurrentIndex()进行更改。以类似的方式,可以使用currentWidget()函数检索当前显示的小部件,并使用setCurrentWidget()函数进行更改。每当布局中的当前小部件发生变化或从布局中移除小部件时,分别会发出currentChanged()和widgetRemoved()信号。

示例代码如下:

import sys
from PySide6.QtGui import QPalette, QColor
from PySide6.QtWidgets import *
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("QStackedLayout之堆叠布局")
        self.setMinimumSize(400, 300)
        pagelayout=QVBoxLayout()
        button_layout=QHBoxLayout()
        self.stacklayout=QStackedLayout()
        pagelayout.addLayout(button_layout)
        pagelayout.addLayout(self.stacklayout)
        btn=QPushButton("red")
        btn.pressed.connect(self.activate_tab_1)
        button_layout.addWidget(btn)
        self.stacklayout.addWidget(Color("red"))
        btn=QPushButton("green")
        btn.pressed.connect(self.activate_tab_2)
        button_layout.addWidget(btn)
        self.stacklayout.addWidget(Color("green"))
        btn=QPushButton("yellow")
        btn.pressed.connect(self.activate_tab_3)
        button_layout.addWidget(btn)
        self.stacklayout.addWidget(Color("yellow"))
        widget=QWidget()
        widget.setLayout(pagelayout)
        self.setCentralWidget(widget)

    def activate_tab_1(self):
        self.stacklayout.setCurrentIndex(0)

    def activate_tab_2(self):
        self.stacklayout.setCurrentIndex(1)

    def activate_tab_3(self):
        self.stacklayout.setCurrentIndex(2)

class Color(QWidget):
    def __init__(self, color):
        super(Color, self).__init__()
        self.setAutoFillBackground(True)
        palette=self.palette()
        palette.setColor(QPalette.Window, QColor(color))
        self.setPalette(palette)

if __name__=='__main__':
    app=QApplication(sys.argv)
    window=MainWindow()
    window.show()
    app.exec()

效果图如下:

QStackedLayout示例效果图


混合布局

使用QHBoxLayout、QVBoxLayout、QGridLayout、QStackedLayout这几种布局组合使用,来控制界面的整体风格视角,制作精美的布局效果

示例代码如下:

import sys
from PySide6.QtWidgets import *

class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()
        self.setWindowTitle("混合布局")
        self.setMinimumSize(400, 300)
        self.setStyleSheet("QLabel { border: 1px solid blue; }")
        layout1=QHBoxLayout()
        layout2=QVBoxLayout()
        layout3=QVBoxLayout()
        layout1.setContentsMargins(0,0,0,0)
        layout1.setSpacing(20)
        layout2.addWidget(QLabel('hbox1-QVBoxLayout1'))
        layout2.addWidget(QLabel('hbox1-QVBoxLayout2'))
        layout2.addWidget(QLabel('hbox1-QVBoxLayout3'))
        layout1.addLayout(layout2)
        layout1.addWidget(QLabel('hbox2'))
        layout3.addWidget(QLabel('hbox2-QVBoxLayout1'))
        layout3.addWidget(QLabel('hbox2-QVBoxLayout2'))
        layout1.addLayout(layout3)
        widget=QWidget()
        widget.setLayout(layout1)
        self.setCentralWidget(widget)

if __name__=='__main__':
    app=QApplication(sys.argv)
    window=MainWindow()
    window.show()
    app.exec()

效果图如下:


混合布局效果图

注意:QLayout边框及样式不能通过这种方式来设置样式效果,需要指定Widget的样式

#无效果
self.setStyleSheet("QHBoxLayout { border: 1px solid black; }")
#有效果
self.setStyleSheet("QLabel { border: 1px solid blue; }")

自定义Layout,实现部件自动换行

由于QHBoxLayout、QVBoxLayout、QGridLayout、QStackedLayout布局放置的控件,不增加任何处理,生成的控件会固定住窗口大小,可以通过下面来动态排列控件。

示例代码如下:

import sys
from PySide6.QtCore import QRect, QSize, QPoint, Qt
from PySide6.QtWidgets import *

class FlowLayout(QLayout):
    def __init__(self, parent=None, margin=0, spacing=-1):
        super(FlowLayout, self).__init__(parent)
        if parent is not None:
            self.setContentsMargins(margin, margin, margin, margin)
        self.setSpacing(spacing)
        self.items=[]

    def addItem(self, item):
        self.items.append(item)

    def count(self):
        return len(self.items)

    def itemAt(self, index):
        if index >=0 and index < len(self.items):
            return self.items[index]
        return None

    def takeAt(self, index):
        if index >=0 and index < len(self.items):
            return self.items.pop(index)
        return None

    def expandingDirections(self):
        return 0

    def hasHeightForWidth(self):
        return True

    def heightForWidth(self, width):
        height=self.doLayout(QRect(0, 0, width, 0), True)
        return height

    def setGeometry(self, rect):
        super(FlowLayout, self).setGeometry(rect)
        self.doLayout(rect, False)

    def sizeHint(self):
        return QSize(self.doLayout(QRect(0, 0, 10000, 0), True), 10000)

    def doLayout(self, rect, testOnly):
        x=rect.x()
        y=rect.y()
        lineHeight=0
        for item in self.items:
            wid=item.widget()
            spaceX=self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton,Qt.Horizontal)
            spaceY=self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton,Qt.Vertical)
            nextX=x + item.sizeHint().width() + spaceX
            if nextX - spaceX > rect.right() and lineHeight > 0:
                x=rect.x()
                y=y + lineHeight + spaceY
                nextX=x + item.sizeHint().width() + spaceX
                lineHeight=0
            if not testOnly:
                item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))
            x=nextX
            lineHeight=max(lineHeight, item.sizeHint().height())

        return y + lineHeight - rect.y()
if __name__=='__main__':
    app=QApplication(sys.argv)
    mainWidget=QWidget()
    mainWidget.setMinimumSize(300, 200)
    layout=FlowLayout(mainWidget)
    for i in range(50):
        layout.addWidget(QPushButton(f'Button {i}'))
    mainWidget.setLayout(layout)
    mainWidget.show()
    sys.exit(app.exec_())

效果如下:

自定义Layout效果图

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的选择控件插件
  • 相关文章 再谈使用开源软件搭建数据分析平台
  • 相关文章 使用开源软件快速搭建数据分析平台
  • 相关文章 高维数据可视化图形语法指南

.先上最后效果图:

2.代码跟上,重点在 2):

1)

//服务列表页面动态加载服务

function ready() {

var url=base_path+"console/cfg/getBaseLayers/"+configId;

$.ajax({

url:url,

type:"get",

dataType:"json",

success:function (result) {

//生成之前先清空tr,防止AJAX异步加载重复生成

$("#lot tr").remove();

var length=result.length;

for (var i=0;i<length;i++){

var name=result[i].name; //服务名称

var alias=result[i].alias;//服务别名

var type=result[i].type;//服务类型

var year=result[i].year;//年份

var url=result[i].url;//服务地址

var visible=result[i].visible;//是否可见

var id=result[i].id;//id

serviceIdArray[i]=id;//此处将id塞给serviceIdArray,用于判断是否添加。

var str="";

if (visible==true){

//生成tr

str +='<tr id="';

str +=id;

str +='"';

str +=' class="lot_box"> <td>';

str +=i+1;

str +='</td> <td>';

str +=name;

str +='</td> <td>';

str +=alias;

str +='</td> <td>';

str +=type;

str +='</td> <td>';

str +=year;

str +='</td> <td>';

str +=url;

str +='</td> <td>';

str +='<input id="';

str +=id;

str +='"';

str +='type="checkbox" checked="true" onchange="modifyService(this.id);"/>';

str +='</td> <td> <input id="';

str +=id;

str +='"';

str +='type="button" value="编辑" onclick="editTd(this.id)"/>';

str +='</td> <td>';

str +='<button class="rosy" id="';

str +=id;

str +='"';

str +=' onclick="deleteService(this.id);">';

str +='<img src="static/img/del14.png"></button>';

str +='</td> </tr>';

}else {

//生成tr

str +='<tr id="';

str +=id;

str +='"';

str +=' class="lot_box"> <td>';

str +=i+1;

str +='</td> <td>';

str +=name;

str +='</td> <td>';

str +=alias;

str +='</td> <td>';

str +=type;

str +='</td> <td>';

str +=year;

str +='</td> <td>';

str +=url;

str +='</td> <td>';

str +='<input id="';

str +=id;

str +='"';

str +='type="checkbox" onchange="modifyService(this.id);"/>';

str +='</td> <td> <input id="';

str +=id;

str +='"';

str +='type="button" value="编辑" onclick="editTd(this.id)"/>';

str +='</td> <td>';

str +='<button class="rosy" id="';

str +=id;

str +='"';

str +=' onclick="deleteService(this.id);">';

str +='<img src="static/img/del14.png"></button>';

str +='</td> </tr>';

}

var $tr=$(str);

$("#lot").append($tr);

}

}

});

}