D3+django谱可视化

最近学习谱聚类的时候,看到用拉普拉斯矩阵的倒数第二个的特征值对应的特征向量可以直接翻译网络的社团结构。写过代码验证结论,但当我想看其他的特征向量时,需要调整代码来看,而且当我想改变为其他的矩阵时也很麻烦。于是,我想到可以用前端可视化的方法将python得到的特征值和特征向量的结果展示出来,而且前端展示也很灵活,美观。我想到可以用D3这个js的库,配合django框架。后端django框架负责接收参数,计算结果,返回数据。前端用boootstrap,bootstrap-slider,d3负责数据可视化。

整个界面如下:

Django 后端准备

由于之前学过一些django,所以这一部分我直接用以前的创建的项目,只是新建一个app就可以了。具体流程如下:

  • python manage.py startapp spectral # 创建APP

  • 设置在mysite(项目名) 和 spectral (App名) 中的 urls:

    [mysite.urls] path('spectral/', include('spectral.urls'))

    [spectral.urls] url(r"^$", views.index, name='index')

  • 最重要的,在mysite.setting.py中的INSTALLED_APPS里添加’spectral’。

  • 在spectral里添加templates\spectral\spectral.html, 就可以在这里写前端页面,需要的js,css文件放入项目的static对应的文件夹下,后端的代码就在spectral里的views.py和models.py里写就可以。

前端准备

事实上,前端所需的框架都是边搞边准备的。这里总结下主要使用的是:jQuery, bootstrap-slider, bootstrap, d3。bootstrap-slider负责接收用户想要的一些参数,比如SBM的块大小,块数,内部连边概率,外部连边概率等。 bootstrap负责页面的整齐美化。d3负责数据的可视化。

流程

1. 获取网络参数,传给后端

我们从前端接收用户输入的参数,传送给后端,后端计算后返回网络的数据给前端。我们使用简单的SBM模型构建网络,而且是对称的结构,所需要的参数只有四个:一个块的大小blockSize,块的个数blockNumber,块内连边的概率cin,块间连边的概率cout。我使用滑动的组件来选择这四个参数,然后找到了bootstrap-slider这个js库来实现。其网址为seiyria/bootstrap-slider: A slider control for Bootstrap 3 & 4. (github.com)。其github上有样例实现的链接,根据上面的样例就可以实现我所需要的大部分功能。

HTML

首先我们需要四个Slider来获取参数,先写Html代码(包含一些bootstrap的布局的class,四个slider组件分布在一行内):

<div class="row">
        <div class="col-sm-3">
            <div class="card card-body bg-info text-white">
                <div class="m-auto">
                    <h4>Block Size: <span id="ex1Value"></span></h4>
                    <input id="ex1" data-provide='slider' data-slider-id='ex1Slider' type="text" data-slider-min="10" data-slider-max="100" data-slider-step="1" data-slider-value="20"/>
                </div>
            </div>
        </div>
        <div class="col-sm-3">
            <div class="card card-body bg-info text-white">
                <div class="m-auto">
                    <h4>Block Number: <span id="ex2Value"></span></h4>
                    <input id="ex2" data-provide='slider' data-slider-id='ex2Slider' type="text" data-slider-min="1" data-slider-max="5" data-slider-step="1" data-slider-value="3"/>
                </div>
            </div>
        </div>
        <div class="col-sm-3">
            <div class="card card-body bg-info text-white">
                <div class="m-auto">
                    <h4>Cin: <span id="ex3Value"></span></h4>
                    <input id="ex3" data-provide='slider' data-slider-id='ex3Slider' type="text" data-slider-min="0" data-slider-max="1" data-slider-step="0.01" data-slider-value="0.8"/>
                </div>
            </div>
        </div>
        <div class="col-sm-3">
            <div class="card card-body bg-info text-white">
                <div class="m-auto">
                    <h4>Cout: <span id="ex4Value"></span></h4>
                    <input id="ex4" data-provide='slider' data-slider-id='ex4Slider' type="text" data-slider-min="0" data-slider-max="1" data-slider-step="0.01" data-slider-value="0.2"/>
                </div>
            </div>
        </div>
    </div>

CSS

然后调整下CSS布局:

body {
    margin: 5%;
}
#ex1Slider .slider-selection {
    background: #BABABA;
}
#ex2Slider .slider-selection {
    background: #BABABA;
}
#ex3Slider .slider-selection {
    background: #BABABA;
}
#ex4Slider .slider-selection {
    background: #BABABA;
}
.row div {
    padding-bottom: 10px;
}

JAVASCRIPT

最后是js部分,对每一个滑动组件,我们监听其滑动的动作,及时更新组件对应的变量的值blockSizeblockNumbercincout并显示在组件相应的位置上。(组件的提示tooltip放在下面,防止阻挡上方的信息。js的所有代码要放到$(document).ready(function() {}中,这样可以在html文档加载之后在运行js文件。):

var ex1 = $('#ex1').slider({
    formatter: function(value) {
        return 'Current value: ' + value;
    }, tooltip_position:'bottom'
}).on('slide', function(slideEvt) {
    $("#ex1Value").text(slideEvt.value);
    blockSize = ex1.slider('getValue');
});
var blockSize = ex1.slider('getValue');
$("#ex1Value").text(blockSize);

var ex2 = $('#ex2').slider({
    formatter: function(value) {
        return 'Current value: ' + value;
    }, tooltip_position:'bottom'
}).on('slide', function(slideEvt) {
    $("#ex2Value").text(slideEvt.value);
    blockNumber = ex2.slider('getValue');
});
var blockNumber = ex2.slider('getValue');
$("#ex2Value").text(blockNumber);

var ex3 = $('#ex3').slider({
    formatter: function(value) {
        return 'Current value: ' + value;
    }, tooltip_position:'bottom'
}).on('slide', function(slideEvt) {
    $("#ex3Value").text(slideEvt.value);
    cin = ex3.slider('getValue');
});
var cin = ex3.slider('getValue');
$("#ex3Value").text(cin);

var ex4 = $('#ex4').slider({
    formatter: function(value) {
        return 'Current value: ' + value;
    }, tooltip_position:'bottom'
}).on('slide', function(slideEvt) {
    $("#ex4Value").text(slideEvt.value);
    cout = ex4.slider('getValue');
});
var cout = ex4.slider('getValue');
$("#ex4Value").text(cout);

然后我们需要给每一个slider加一个滑动结束的监听函数:当slider结束滑动时,向后端发送参数数据,接收返回的数据并更新可视化的内容(网络邻接矩阵,Operator(默认为邻接矩阵的)特征向量,特征值)。

function postNetData() {
    $("#ex6").attr('data-slider-max', blockSize*blockNumber);
    $("#ex6").slider({
        max : blockSize*blockNumber, 
        reversed : true
    });
    $('#sel1').val('A');
    $("#ex5").slider("disable");
    $.ajax({
        type: 'POST',
        url: `http://${home_Path}/spectral/`,
        data: {'blockSize':blockSize, 'blockNumber':blockNumber, 'cin':cin, 'cout':cout},
        // update spy figure
        success: function(data) {
            console.log(data);
            netAdj = data['adj'];
            var size = netAdj.length;
            var width = $('#net').width()
            var height = width
            d3.select('body').select("#net").select("svg").remove();
            d3.select('body').select("#net").append("svg")
                .attr("width", width)
                .attr("height", height)
                .selectAll("rect")
                .data(netAdj)
                .enter()
                .append("g")
                .selectAll("rect")
                .data(function (d,i) {return d;})
                .enter()
                .append("rect")
                .attr("x", function(d,i){
                            return i*width/size;
                })
                .attr("y", function(d, i, j){
                            return j*width/size;
                })
                .attr("height", width/size * 0.75)
                .attr("width", width/size * 0.75)
                .attr("fill", function (d,i) {
                    if (d > 0) {
                        return 'blue';
                    } else {
                        return "gray"
                    }
                })
                .attr("text", function (d,i) {return d;});
            update_svg(data);
            update_eigen(data);
        }
    });
};
// POST net parameter
ex1.on("slideStop", postNetData);
ex2.on("slideStop", postNetData);
ex3.on("slideStop", postNetData);
ex4.on("slideStop", postNetData);
var home_Path = window.location.host;
var netAdj = null;
postNetData();

postNetData函数开始有一些是关于后面的组件的信息更新,比如Operator恢复默认值(下拉菜单组件#sel1),控制展示特征向量的index的slider组件(#ex6)的最大值等。然后该函数就ajax向后端POST数据,将四个参数传给后端。在成功接收到后端返回的数据后,首先16-47行绘制邻接矩阵,这一部分开始需要用到d3这个js库,我们第3节在讲;然后update_svg和update_eigen函数是后面负责更新特征向量特征值(谱)的可视化更新的,这一部分我们第4,5节讲。我们现在需要去后端处理返回数据的问题。

2. 后端返回数据

后端主要是models.py 和 views.py 这两个文件。

views.py

views.py 接受前端传来的数据,提取各个参数,然后创建后端模型的类对象,计算邻接矩阵,特征值,特征向量,并以Json格式返回给前端。

// views.py
def index(request):
    if request.method == 'GET':
        context = dict()
        # context['hello'] = 'Hello World!'
        return render(request, 'spectral/spectral.html', context)
    elif request.method == 'POST':
        data = request.POST
        print(data)
        blockSize = int(data['blockSize'])
        blockNumber = int(data['blockNumber'])
        cin = float(data['cin'])
        cout = float(data['cout'])
        n = blockSize * blockNumber
        sizes = [blockSize] * blockNumber
        ps = [[cin if i == j else cout for j in range(blockNumber)] for i in range(blockNumber)]
        global sbm
        sbm = SBMMatrix(n, sizes=sizes, ps=ps)
        w, v = sbm.get_eigen()
        print(n, sizes, ps)
        return JsonResponse({'adj': sbm.A.tolist(), 'eigenValue': w.tolist(), 'eigenVector':v.tolist()})

值得注意的点有:

  • 返回的值一定要是list,直接调用numpy的tolist方法即可。
  • sbm是全局变量,为了使之后选择不同的Operator时,sbm这个对象保持不变(也即网络不变,只是边Operator)

models.py

models.py里的SBMMatrix是之前实现的一个类,使用networkx生成SBM模型的网络,并计算其邻接矩阵,还可以选择不同的Operator返回其从小到大排列的特征值以及对应的特征向量。目前Operator包括邻接矩阵(A), 拉普拉斯矩阵的三种形式(L,L_rw,L_sym), Bethe-Hessian矩阵(BH)。non-backtracking矩阵由于维度和原来的矩阵不一样,暂时没有使用。

// models.py
import numpy as np
import scipy as sp
import networkx as nx


class Matrix:
    def __init__(self, n):
        self.n = n
        self.A = np.zeros((n, n))

    def construct(self):
        pass


class SBMMatrix(Matrix):
    def __init__(self, n, sizes, ps, operator="A"):
        super().__init__(n)
        self.g = None
        self.sizes = sizes
        self.ps = ps
        self.operator = operator
        if len(sizes) == len(ps):
            self.construct()
        else:
            print("Parameter Wrong: please check sizes or ps!")

    def construct(self):
        if self.g is None:
            self.g = nx.stochastic_block_model(sizes=self.sizes, p=self.ps)
        self.change_operator(self.operator)

    def change_operator(self, operator='A', r=0):
        """ operator: A, L, L_rw, L_sym, NB, BH
                r special for BH
        """
        self.operator = operator
        A = nx.to_numpy_array(self.g)
        if self.operator == 'A':
            self.A = A
        elif self.operator == 'L':
            L = np.diag(np.sum(A, 0)) - A
            self.A = L
        elif self.operator == 'L_rw':
            self.A = np.linalg.inv(np.diag(np.sum(A, 0))) @ A
        elif self.operator == 'L_sym':
            D = np.sum(A, 0)
            _D = np.diag(D)
            L = _D @ A @ _D
            self.A = L
        elif self.operator == 'NB':
            edges = []
            x, y = np.where(A == 1)
            for i, x_i in enumerate(x):
                edges.append((x_i, y[i]))
            e = len(edges)
            B = np.zeros((e, e))
            for i in range(len(edges)):
                for j in range(len(edges)):
                    if edges[i][1] == edges[j][0] and edges[i][0] != edges[j][1]:
                        B[i, j] = 1
                    else:
                        B[i, j] = 0
            self.A = B
        elif self.operator == 'BH':
            D = np.sum(A, 0)
            _D = np.diag(D)
            B = np.eye(np.shape(A)[0]) * (r**2 - 1) - r * A + _D
            self.A = B
        else:
            pass
    
    def get_eigen(self):
        w, v = np.linalg.eig(self.A)
        sort_index = np.argsort(w)
        w = w[sort_index]
        v = v[:, sort_index]
        return w, np.transpose(v)

3. 可视化展示网络邻接矩阵

前端在接收到后端返回的网络数据后,第一件事就是绘制网络的邻接矩阵。首先在HTML中创建一个div容纳邻接矩阵的展示区域:

<div class="col-sm-4">
    <div class="card card-body" id="net">
    </div>
</div>

然后,在JS中,使用d3负责可视化这个邻接矩阵。我们提取这一部分的JS代码:

netAdj = data['adj'];
var size = netAdj.length;
var width = $('#net').width()
var height = width
d3.select('body').select("#net").select("svg").remove();
d3.select('body').select("#net").append("svg")
    .attr("width", width)
    .attr("height", height)
    .selectAll("rect")
    .data(netAdj)
    .enter()
    .append("g")
    .selectAll("rect")
    .data(function (d,i) {return d;})
    .enter()
    .append("rect")
    .attr("x", function(d,i){
        return i*width/size;
    })
    .attr("y", function(d, i, j){
        return j*width/size;
    })
    .attr("height", width/size * 0.75)
    .attr("width", width/size * 0.75)
    .attr("fill", function (d,i) {
        if (d > 0) {
            return 'blue';
        } else {
            return "gray"
        }
    })
    .attr("text", function (d,i) {return d;});

d3使用svg矢量图创建图像。关于D3的入门教程可以看D3.js入门教程-极客学院Wiki 。这里创建许多矩形来展示邻接矩阵。展示的宽和高由#net div的宽来决定。每一个小矩形的颜色由对应的数据决定(为1是蓝色,0为灰色)。

4. 获取Operator的特征向量

接下来,需要控制特征向量的可视化。我使用下拉菜单来控制Operator的选择,对于BetheHessian矩阵,存在一个参数r需要控制,我依然使用slider来调整r,且只有当下拉菜单选择了BetheHessian时,调整r的slider才会生效。

前端

首先完成HTML部分:

<div class="row">
    <div class="col-sm-6">
        <div class="card card-body bg-success">
            <div id="operator_div" class="row">
                <div id="operator_label" class="col-sm-2 m-auto"><h6>Operator</h6></div>
                <div class="col-sm-10 m-auto">
                    <div class="form-group">
                        <select class="form-control" id="sel1">
                            <option>A</option>
                            <option>L</option>
                            <option>L_sym</option>
                            <option>L_rw</option>
                            <option>BH</option>
                        </select>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <div class="col-sm-6">
        <div class="card card-body bg-success">
            <div class="m-auto">
                <h5>r: <span id="ex5Value"></span></h5>
                <input id="ex5" data-provide='slider' data-slider-id='ex5Slider' type="text" data-slider-min="0" data-slider-max="100" data-slider-step="0.01" data-slider-value="1"/>
            </div>
        </div>
    </div>
</div>

对这一部分的CSS微调一下:

#operator_div div {
    padding-bottom: 0px;
}
#operator_label {
    padding-right: 0px;
}

对于JS部分,我们需要的功能是,当下拉菜单选择了一个Operator或者调整r的slider结束滑动时,需要向后端POST Operator和r参数,后端根据参数,调用sbm的change_operator方法,并返回相应的谱。

function change_operator() {
    var val = $("#sel1 option:selected").text();
    console.log(val);
    console.log(r)
    if (val == 'BH') {
        $("#ex5").slider("enable");
    } else {
        $("#ex5").slider("disable");
    }
    $.ajax({
        type: 'POST',
        url: `http://${home_Path}/spectral/change_operator/`,
        data: {'operator': val, 'r': r},
        // update spy figure
        success: function(data) {
            console.log(data);
            update_svg(data);
            update_eigen(data);
        }
    });
};
// change operator
var ex5 = $('#ex5').slider({
    formatter: function(value) {
        return 'Current value: ' + value;
    }
}).on('slide', function(slideEvt) {
    $("#ex5Value").text(slideEvt.value);
    r = ex5.slider('getValue');
});
var r = ex5.slider('getValue');
$("#ex5Value").text(r);
ex5.on("slideStop", change_operator);
$("#ex5").slider("disable");
$("#sel1").change(change_operator);

update_svg 和 update_eigen 即为更新谱可视化的函数。33,35行绑定slider停止滑动和select选择事件到change_operator函数上。

后端

这里后端的逻辑很简单,接收数据,提取数据,传入sbm的change_operator方法,get_eigen获得谱数据,以Json形式返回谱数据。

// views.py
def change_operator(request):
    if request.method == 'POST':
        data = request.POST
        print(data)
        operator = str(data['operator'])
        if 'r' in data.keys():
            r = float(data['r'])
        else:
            r = 0
        global sbm
        if sbm is not None:
            sbm.change_operator(operator, r=r)
            w, v = sbm.get_eigen()
        else:
            w, v = [], []
        return JsonResponse({'eigenValue': w.tolist(), 'eigenVector':v.tolist()})

5. 展示特征向量和对应的特征值

在获得了谱数据后,如何展示是一个问题。这里我只是想方便的查看Operator的各个特征向量和其对应的特征值。这样我们需要一个slider负责选择我们想要看的是第几个特征。我们采取左边特征向量,中间slider,右边特征值的布局:

<div class="row">
    <div class="col-sm-5">
        <div class="card card-body" id="eigenvector_content">
        </div>
    </div>
    <div class="col-sm-2">
        <div class="card card-body bg-success">
            <div class="m-auto">
            	<h6>Eigen: <span id="ex6Value"></span></h6>
        	</div>
        	<div class="m-auto">
            	<input id="ex6" type="text" data-slider-id='ex6Slider' data-slider-min="1" data-slider-max="60" data-slider-step="1" data-slider-value="1" data-slider-orientation="vertical"/>
        	</div>
    	</div>
    </div>
    <div class="col-sm-5">
    	<div class="card card-body" id="eigenvalue_content">
    	</div>
    </div>
</div>

JS部分,首先实现较为简单的slider的部分,中间的slider需要是垂直倒置的:最下面是1,最上面是网络节点数(blockSize * blockNum),这一点在第一节JAVASCRIPT部分的postNetData第3行有所展示。在slider滑动的过程中,第11行及时更新谱的展示,实现流畅的可视化。

$("#ex6").slider({
    reversed : true
});
var ex6 = $('#ex6').slider({
    formatter: function(value) {
        return 'Current value: ' + value;
    }, tooltip_position:'bottom'
}).on('slide', function(slideEvt) {
    $("#ex6Value").text(slideEvt.value);
    eigen = ex6.slider('getValue');
    update_eigen({'eigenValue':eigenValue, 'eigenVector':eigenVector});
});
var eigen = ex6.slider('getValue');
$("#ex6Value").text(eigen);
var eigenValue = null;
var eigenVector = null;
var eigVecSvg = null;
var eigValSvg = null;

可视化特征向量

为了防止在改变展示的谱后多添加svg,遂将svg添加的部分单独抽出一个函数。只有在改变网络时调用该函数删除原有svg,新建一个svg。而在只是改变谱数据时,不调用这个函数,保证只有一个svg在展示区域。改变谱数据只是改变svg里展示的内容而已。对于特征值的可视化同样如此。

function update_svg(data) {
    d3.select("body").select("#eigenvector_content").select("svg").remove();
    var width = $('#eigenvector_content').width();    // 可视区域宽度
    var height = width;   // 可视区域高度
    eigVecSvg = d3.select("body").select("#eigenvector_content")
        .append("svg")
        .attr("width", width).attr("height", height);

    d3.select("body").select("#eigenvalue_content").select("svg").remove();
    width = $('#eigenvalue_content').width();    // 可视区域宽度
    height = width;   // 可视区域高度
    eigValSvg = d3.select("body").select("#eigenvalue_content")
        .append("svg")
        .attr("width", width).attr("height", height/5);
};

我使用散点图(关于D3散点图的代码参考D3.js-散点图 - 云+社区 - 腾讯云 (tencent.com)的代码。)可视化特征向量,纵轴是特征向量值,横轴是对应的节点id(一个特征向量长度为网络的节点数)。首先,我们计算出绘制的点作为dataset, 并且计算出可视区域的宽高以确定x轴y轴的长度:

 // 构造绘制点的dataset
 y = eigenVector[eigen-1];
 x = Array.from(Array().keys());
 var eigVecDataset = []
 for (let i = 0; i < blockSize * blockNumber; i++) {
 	eigVecDataset.push([i, y[i]]);
 }
 // var dataset = [[0.5, 0.5],[0.7, 0.8],[0.4, 0.9],
 //     [0.11, 0.32],[0.88, 0.25],[0.75, 0.12],
 //     [0.5, 0.1],[0.2, 0.3],[0.4, 0.1]];

var eigVecWidth = $('#eigenvector_content').width();    // 可视区域宽度
var eigVecHeight = eigVecWidth;   // 可视区域高度

var eigVecxAxisWidth = eigVecWidth;   // x轴宽度
var eigVecyAxisWidth = eigVecWidth - 30;   // y轴宽度

然后我们需要处理x轴和y轴的比例尺,x轴比例尺的定义域范围比较好确定,从0~节点数即可。但y轴比例尺定义域最大最小应该是所有特征向量的最大元素值和 最小元素值。所以我们需要提前获取这两个值,然后定义比例尺:

// 获取最小和最大的特征向量的值
var minEigVec = 0;
var maxEigVec = 0;
for (let i = 0; i < blockSize * blockNumber; i++) {
    if (i == 0) {
        minEigVec = d3.min(eigenVector[i]);
        maxEigVec = d3.max(eigenVector[i]);
    } else {
        if (d3.min(eigenVector[i]) < minEigVec) {
            minEigVec = d3.min(eigenVector[i]);
        }
        if (d3.max(eigenVector[i]) > maxEigVec) {
            maxEigVec = d3.max(eigenVector[i])
        }
    }
}
/*定义比例尺*/
var eigVecxScale = d3.scale.linear()
.domain([0, blockSize * blockNumber + 10])
.range([0, eigVecxAxisWidth]);

var eigVecyScale = d3.scale.linear()
.domain([minEigVec, maxEigVec])
.range([0, eigVecyAxisWidth]);

然后是需要两个函数,一个根据dataset负责绘制点,一个负责绘制坐标轴,分别为drawCircle和。首先是drawCircle部分。该函数体现了d3在根据数据添加元素时的三种状态:update,enter和exit。update指已经存在的元素,这部分根据数据进行变化;enter 指还未创建的元素,这部分根据数据创建;exit指多余的元素,这部分一般会被删除。该函数参数较多:

  • svg :绘制点所在的svg。由于特征值和特征向量均需要绘制散点,所以需要添加此参数。
  • dataset:绘制点的坐标数据。
  • xScale, yScale:横纵轴的比例尺。
  • height:绘制界面的高度。
  • ss:渐变色,因我们需要不同的块的节点颜色不同,以作区别,需要传入渐变色数组控制颜色的表达。
  • r:散点的半径。
  • obviousShow:该函数主要为了特征值的明显表示,当控制eigen的Slider滑动时,相应的特征值应该改变颜色(由ss控制)且变大一些,以表示当前为该特征值。

update的部分给一个transition的延迟渐变效果,然散点可以上下移动(体现在横轴和transition之前一样,纵轴坐标发生改变)。drawCircle函数如下:

/* 绘制圆点 */
function drawCircle(svg, dataset, xScale, yScale, height, ss, r, obviousShow){
    var circleUpdate = svg.selectAll("circle").data(dataset);
    // update处理
    circleUpdate.transition().duration(500)
        .attr("cx", function(d){
        return padding.left + xScale(d[0]);
    })
        .attr("cy", function(d, i){
        return height - padding.bottom - yScale(d[1]);
    })
        .attr("r", function(d, i){
        if (obviousShow != null && i == obviousShow) {
            return r + 3;
        } else {
            return r;
        }
    })
        .attr('fill', function(d, i) {
        return ss[i];
    });

    // enter处理
    circleUpdate.enter().append("circle")
        .attr("cx", function(d){
        return padding.left + xScale(d[0]);
    })
        .attr("cy", function(d, i){
        return height - padding.bottom;
    })
        .attr("r", function(d, i){
        if (obviousShow != null && i == obviousShow) {
            return r + 3;
        } else {
            return r;
        }
    })
        .transition().duration(500)
        .attr("cx", function(d, i){
        return padding.left + xScale(d[0]);
    })
        .attr("cy", function(d, i){
        return height - padding.bottom - yScale(d[1]);
    })
        .attr('fill', function(d, i) {
        return ss[i];
    });
    // exit处理
    circleUpdate.exit()
        .transition().duration(500)
        .attr("fill", "white")
        .remove();

}

drawScale负责绘制坐标轴,根据传入的参数绘制。这里我加入一个yStepNum参数控制y轴tick的个数,防止过密的标记带来视觉上的不适。y轴由于本来是从上至下的,所以我们在绘制之前需要将y轴的比例尺的定义域范围反转,以和我们左下角为原点的习惯相符。

/* 添加坐标轴 */
function drawScale(svg, xScale, yScale, yAxisWidth, height, yStepNum){
    var xAxis = d3.svg.axis().scale(xScale).orient("bottom");
    yScale.range([yAxisWidth, 0]);  // 重新设置y轴比例尺的值域,与原来的相反
    var yAxis = d3.svg.axis().scale(yScale).orient("left");
    if (yStepNum != null) {
        yAxis.ticks(yStepNum);
    }
    svg.append("g").attr("class", "axis")
        .attr("transform", "translate("+ padding.left +","+ (height - padding.bottom) +")")
        .call(xAxis);

    svg.append("g").attr("class", "axis")
        .attr("transform", "translate("+ padding.left +","+ (height - padding.bottom - yAxisWidth) +")")
        .call(yAxis);

    // 绘制完比例尺,还原比例尺y轴值域
    yScale.range([0, yAxisWidth]);
}

完成这些后,还需要根据SBM的参数,计算设定对应块的颜色。由于我们只设定最多只有5个块,所以只需要最多5种颜色即可:

/* 颜色 */
var ss = ["#CC0000", '#0000FF', '#00FF00', '#FF9900', '#CC00CC']
var eigVecColors = [];
for (i = 0; i < blockNumber * blockSize; i++) {
    var accumulate = 0;
    for (j = 0; j < blockNumber; j++) {
        if (accumulate <= eigVecDataset[i][0] && eigVecDataset[i][0] < accumulate + blockSize) {
            eigVecColors[i] = ss[j];
            break;
        } else {
            accumulate += blockSize;
        }
    }
}

然后就可以调用drawCircle和drawScale函数绘制特征向量的散点图了。

// 初始化特征向量绘制
drawCircle(eigVecSvg, eigVecDataset, eigVecxScale, eigVecyScale, eigVecHeight, eigVecColors, 2, null);
drawScale(eigVecSvg, eigVecxScale, eigVecyScale, eigVecyAxisWidth, eigVecHeight, null);

可视化特征值

有了前面绘制特征向量的准备,绘制特征值就很轻松了。值得注意的是,特征值都绘制在x轴上,所以y轴很小,且横轴的比例尺的范围发生变化,是特征值的最大最小值。且特征值只需要选中的特征值颜色改变即可。关于绘制特征值之前的准备代码如下:

var eigValDataset = []
for (let i = 0; i < blockSize * blockNumber; i++) {
	eigValDataset.push([eigenValue[i], 0]);
}
var eigValWidth = $('#eigenvalue_content').width();    // 可视区域宽度
var eigValHeight = eigValWidth / 5;   // 可视区域高度
var eigValxAxisWidth = eigValWidth - 20;   // x轴宽度
var eigValyAxisWidth = eigValWidth / 5;   // y轴宽度
/*定义比例尺*/
out = 2 * ((d3.max(eigenValue) - d3.min(eigenValue))/(blockSize * blockNumber));
var eigValxScale = d3.scale.linear()
    .domain([d3.min(eigenValue) - out, d3.max(eigenValue) + out])
    .range([0, eigValxAxisWidth]);

var eigValyScale = d3.scale.linear()
    .domain([0, 1])
    .range([0, eigValyAxisWidth]);
var eigValColors = [];
eigValColors[eigen-1] = '#CC0000';

然后调用drawCircle和drawScale绘制散点图即可:

drawCircle(eigValSvg, eigValDataset, eigValxScale, eigValyScale, eigValHeight, eigValColors, 3, eigen-1);
drawScale(eigValSvg, eigValxScale, eigValyScale, eigValyAxisWidth, eigValHeight, 2);

所有代码如下:

function update_eigen(data) {
    eigenValue = data['eigenValue'];
    eigenVector = data["eigenVector"];

    // 获取最小和最大的特征向量的值
    var minEigVec = 0;
    var maxEigVec = 0;
    for (let i = 0; i < blockSize * blockNumber; i++) {
        if (i == 0) {
            minEigVec = d3.min(eigenVector[i]);
            maxEigVec = d3.max(eigenVector[i]);
        } else {
            if (d3.min(eigenVector[i]) < minEigVec) {
                minEigVec = d3.min(eigenVector[i]);
            }
            if (d3.max(eigenVector[i]) > maxEigVec) {
                maxEigVec = d3.max(eigenVector[i])
            }
        }
    }

    // 构造绘制点的dataset
    y = eigenVector[eigen-1];
    x = Array.from(Array().keys());
    var eigVecDataset = []
    for (let i = 0; i < blockSize * blockNumber; i++) {
        eigVecDataset.push([i, y[i]]);
    }
    // var dataset = [[0.5, 0.5],[0.7, 0.8],[0.4, 0.9],
    //     [0.11, 0.32],[0.88, 0.25],[0.75, 0.12],
    //     [0.5, 0.1],[0.2, 0.3],[0.4, 0.1]];

    var eigVecWidth = $('#eigenvector_content').width();    // 可视区域宽度
    var eigVecHeight = eigVecWidth;   // 可视区域高度

    var eigVecxAxisWidth = eigVecWidth;   // x轴宽度
    var eigVecyAxisWidth = eigVecWidth - 30;   // y轴宽度

    var padding = {top: 30, right: 20, bottom:20, left:30};

    /*定义比例尺*/
    var eigVecxScale = d3.scale.linear()
    .domain([0, blockSize * blockNumber + 10])
    .range([0, eigVecxAxisWidth]);

    var eigVecyScale = d3.scale.linear()
    .domain([minEigVec, maxEigVec])
    .range([0, eigVecyAxisWidth]);


    /* 绘制圆点 */
    function drawCircle(svg, dataset, xScale, yScale, height, ss, r, obviousShow){
        var circleUpdate = svg.selectAll("circle").data(dataset);
        // update处理
        circleUpdate.transition().duration(500)
            .attr("cx", function(d){
            return padding.left + xScale(d[0]);
        })
            .attr("cy", function(d, i){
            return height - padding.bottom - yScale(d[1]);
        })
            .attr("r", function(d, i){
            if (obviousShow != null && i == obviousShow) {
                return r + 3;
            } else {
                return r;
            }
        })
            .attr('fill', function(d, i) {
            return ss[i];
        });

        // enter处理
        circleUpdate.enter().append("circle")
            .attr("cx", function(d){
            return padding.left + xScale(d[0]);
        })
            .attr("cy", function(d, i){
            return height - padding.bottom;
        })
            .attr("r", function(d, i){
            if (obviousShow != null && i == obviousShow) {
                return r + 3;
            } else {
                return r;
            }
        })
            .transition().duration(500)
            .attr("cx", function(d, i){
            return padding.left + xScale(d[0]);
        })
            .attr("cy", function(d, i){
            return height - padding.bottom - yScale(d[1]);
        })
            .attr('fill', function(d, i) {
            return ss[i];
        });
        // exit处理
        circleUpdate.exit()
            .transition().duration(500)
            .attr("fill", "white")
            .remove();

    }

    /* 添加坐标轴 */
    function drawScale(svg, xScale, yScale, yAxisWidth, height, yStepNum){
        var xAxis = d3.svg.axis().scale(xScale).orient("bottom");
        yScale.range([yAxisWidth, 0]);  // 重新设置y轴比例尺的值域,与原来的相反
        var yAxis = d3.svg.axis().scale(yScale).orient("left");
        if (yStepNum != null) {
            yAxis.ticks(yStepNum);
        }
        svg.append("g").attr("class", "axis")
            .attr("transform", "translate("+ padding.left +","+ (height - padding.bottom) +")")
            .call(xAxis);

        svg.append("g").attr("class", "axis")
            .attr("transform", "translate("+ padding.left +","+ (height - padding.bottom - yAxisWidth) +")")
            .call(yAxis);

        // 绘制完比例尺,还原比例尺y轴值域
        yScale.range([0, yAxisWidth]);
    }

    /* 颜色 */
    var ss = ["#CC0000", '#0000FF', '#00FF00', '#FF9900', '#CC00CC']
    var eigVecColors = [];
    for (i = 0; i < blockNumber * blockSize; i++) {
        var accumulate = 0;
        for (j = 0; j < blockNumber; j++) {
            if (accumulate <= eigVecDataset[i][0] && eigVecDataset[i][0] < accumulate + blockSize) {
                eigVecColors[i] = ss[j];
                break;
            } else {
                accumulate += blockSize;
            }
        }
    }

    // 初始化特征向量绘制
    drawCircle(eigVecSvg, eigVecDataset, eigVecxScale, eigVecyScale, eigVecHeight, eigVecColors, 2, null);
    drawScale(eigVecSvg, eigVecxScale, eigVecyScale, eigVecyAxisWidth, eigVecHeight, null);
    // 初始化特征值绘制
    var eigValDataset = []
    for (let i = 0; i < blockSize * blockNumber; i++) {
        eigValDataset.push([eigenValue[i], 0]);
    }
    var eigValWidth = $('#eigenvalue_content').width();    // 可视区域宽度
    var eigValHeight = eigValWidth / 5;   // 可视区域高度
    var eigValxAxisWidth = eigValWidth - 20;   // x轴宽度
    var eigValyAxisWidth = eigValWidth / 5;   // y轴宽度
    /*定义比例尺*/
    out = 2 * ((d3.max(eigenValue) - d3.min(eigenValue))/(blockSize * blockNumber));
    var eigValxScale = d3.scale.linear()
    .domain([d3.min(eigenValue) - out, d3.max(eigenValue) + out])
    .range([0, eigValxAxisWidth]);

    var eigValyScale = d3.scale.linear()
    .domain([0, 1])
    .range([0, eigValyAxisWidth]);
    var eigValColors = [];
    eigValColors[eigen-1] = '#CC0000';
    drawCircle(eigValSvg, eigValDataset, eigValxScale, eigValyScale, eigValHeight, eigValColors, 3, eigen-1);
    drawScale(eigValSvg, eigValxScale, eigValyScale, eigValyAxisWidth, eigValHeight, 2);

};

总结

这次自己的心血来潮搞的小作业。主要是来学习,积攒经验。学一点前端D3的东西,积累一些可视化的经验,丰富可视化的方法。之后想前端可视化和python结合起来就依然可以用这套工具来实现了。但本次依然由一些问题没有解决,比如特征值可视化时,突出显示的特征值没有排列到其他散点之前,这是由于SVG图没有zindex的概念,其元素的绘制顺序由标签的先后顺序决定。关于D3的一些基本知识也还没有掌握很清楚,但其实感觉现用现学才行,不然学了不用也记不住,白学。

召唤看板娘