Lisp-Stat翻譯 —— 第十章 一些動態繪圖實例

第十章 一些動態繪圖實例

關於統計學領域動態繪圖方法的有效使用的研究纔剛剛開始(注:本文寫於1991年),經過支持對標準方法變化的研究和對新方法開發的研究,Lisp-Stat繪圖系統被設計成支持統計學的動態繪圖研究。本章展現若干實例,都是用來講明Lisp-Stat繪圖系統的用途的,所選的例子即會介紹現有文獻裏提出的新的統計學思想,也會展現使用Lisp-Stat來實現這些思想的一寫有用的編程技術。有的例子很短也很直接,而有的很寬泛。 編程

10.1 一些動畫效果

19世紀60年代晚期,Fowlkes開發了一個系統,用來測試機率繪圖方面冪轉換方面的研究,這是統計學裏使用動態繪圖方法的最先的例子之一。這幅圖繪製在一個CRT顯示器上,進行轉換操做的參數由顯示器上的標度盤來控制。這種依靠一個或多個連續的參數(這些參數可使用機械或圖形的方式進行控制)的動畫形式,在不少狀況下都頗有用。這在Lisp-Stat裏很容易實現。第一個例子,即Flowlkes的冪轉換圖型,將在第2.7.3節裏給出,該節會從新檢測這個冪轉換的實例,而且會再展現2個例子。 canvas

10.1.1 再看冪轉換

第2.7.3節給出了一個冪轉換的可動態變化的圖形實例,該圖形是針對第2.2.1節給出的降雨量數據。首先對降雨量數據進行排序,而後繪製排序後的數據與對應的正常分數的數據圖形,該圖形生成完成。在冪指數改變以後,經過向圖形發送一個:add-points消息來添加新數據,接着再發送:clear消息來實現圖形重畫。 windows

    該方法有兩個缺陷,首先,經過對數據進行排序,降水量數據原來的索引和圖形裏的數據的一致性不存在了,這意味着冪轉換圖形與降水量數據的另外一個圖形之間的鏈接再也不會產生正確的結果;另外一個問題是點數據的特徵,好比說顏色、符號和狀態,在冪變化的時候不會保存。 安全

    經過使用上一章介紹的一些消息,咱們能夠克服這些問題。爲了不對數據進行排序,咱們可使用等級來構造這個正常的分數: app

> (let ((ranks (rank precipitation)))
    (setf nq (normal-quant (/ (+ ranks 1) 31))))
向每個等級值上加1是須要的,由於rank函數返回的是基於0的等級分級。就像2.7.3節裏同樣,咱們能夠這樣定義一個bc函數:
> (defun bc (x p)
    (let* ((bcx (if (< (abs p) .0001) (log x) (/ (^ x p) p)))
           (min (min bcx))
           (max (max bcx)))
      (/ (- bcx min) (- max min))))
BC
那麼,對於1的冪的初始化圖形能夠這樣設置:
> (setf w (plot-points nq (bc precipitation 1)))
由於使用的冪是1,函數bc只不太重新縮放一下數據。

    爲了改變圖形中正在使用的冪,咱們能夠定義一個函數change-power,咱們可使用:point-coordinate消息來改變圖形裏點的y座標。點數據的全部其它特性均不變。由於:point-coordinate消息須要全部點的下標的列表,設置一個包含這些下標的變量是有用的: dom

> (setf indices (iseq 30))
這避免了每次這個冪值改變時都不得不構建該列表,使用這個變量,change-power函數這樣定義:
> (defun change-power (p)
    (send w :point-coordinate 1 indices (bc precipitation p))
    (send w :redraw-content))
CHANGE-POWER
咱們只須要調用一次:point-coordinate消息,由於該消息對應的方法是矢量化的。須要:redraw-content消息的緣由是:point-coordinate方法不能重畫圖形。

    咱們能夠從解釋器裏調用change-function函數,或者把它當作一個滑塊的動做函數: ide

> (setf slider
        (interval-slider-dialog '(-1 2) :action #'change-power))
#<Object: 133b544, prototype = INTERVAL-SLIDER-DIALOG-PROTO>
在滑塊空間裏,其默認的初始化值是區間的小端點側,下邊的表達式將滑塊的值設置爲構造的圖形使用的冪值。由於slider變量是爲控制圖形對象w專門設計的,當圖形關閉時,該滑塊要從屏幕中移除,經過將滑塊定義爲一個使用:add-subordinate消息的圖形的附屬品,咱們能夠確保滑塊移除成功:
> (send w :add-subordinate slider)
    對於較大的數據集,或者速度慢的電腦,相似這樣的動畫效果可能移動的很是慢。若是動畫只依賴一個參數,一般可能會計算它可能須要的全部預計算的值。例如,咱們可使用下邊的表達式創建一個冪列表,而後將轉換後的數據計算爲列表的列表:
> (setf powers (rseq -1 2 31))
(-1.0 -0.9 -0.8 -0.7 -0.6 -0.5 -0.3999999999999999 -0.29999999999999993 -0.19999999999999996 -0.09999999999999998 0.0 0.10000000000000009 0.20000000000000018 0.30000000000000004 0.40000000000000013 0.5 0.6000000000000001 0.7000000000000002 0.8 0.9000000000000001 1.0 1.1 1.2000000000000002 1.3000000000000003 1.4000000000000004 1.5 1.6 1.7000000000000002 1.8000000000000003 1.9000000000000004 2.0)
> (setf data (mapcar #'(lambda (p) (bc precipitation p)) poser))
而後,一個序列滑塊可用來在該數據列表上滾動:
> (flet ((change (x)
                 (send w :point-coordinate 1 indices x)
                 (send w :redraw-content)))
    (sequence-slider-dialog data
                            :display powers
                            :action #'change))
在該滑塊裏,動做函數的參數是針對當前冪值的轉化後的座標值列表,對於滑塊來講,冪值列表被用作顯示序列。

    在動畫效果中,預計算一般會引發極大的速度提高,可是這也須要額外的編程努力。因爲爲了容納預計算的結果須要大量的空間,若是動畫參數超過一個的話,預計算一般是行不通的。 函數

練習 10.1 工具

10.1.2 繪圖插值

      動態製圖的一個主要目標就是在數據超過三維時提供可視化的方法。檢測四維數據的一個方法就是將變量並變成數據對(x1,y1)和(x2,y2),而後在數據對的二維散點圖之間,使用動畫連續地移動。這種技術叫圖形插值。 佈局

      有的方法能夠用於在兩個圖形中進行插值,很天然的選擇就是圖插值。

xp = (1-p)x1 + px2
yp = (1-p)y1 + py2

那就是:當p由0向1連續變化時,設置和顯示yp相對於xp的圖形。不幸的是,這個方法有個問題,當用於不想關數據的時候,插值顯示的點雲在p從0移動到0.5時是收縮的,而當p從0.5移動到1時是擴張的。

      爲了理解問題的本源,假設咱們使用插值來觀看構成4維球形正態分佈的樣本,該組件是獨立標準正態隨機變量,因此插值圖形裏的最初的和最後的兩個圖形表示的是兩個獨立標準正態變量的圖形。可是對於p=0.5的情況,變量xp和yp是兩個獨立標準正態變量的平均值,所以他們的標準方差是1/sqrt(2)。

      爲了不這個問題,Buja et al建議使用三角插值。公式以下:

xp = cos(p*pi/2)x1 + sin(p*pi/2)x2
yp = cos(p*pi/2)y1 + sin(p*pi/2)y2
表示當p從0到1連續變化時,yp對xp的圖形,若是全部變量都歸一化到想吐的方差下,這種方法能夠保持組件的方差。這與9.1.3節裏使用的旋轉是等價的。

      三角插值能夠多種方式實現,一個方法就是使用一個二維圖形,而後在圖形裏使用:point-coordinate消息來改變數據。

      爲了開始驗證,咱們能夠定義一個能夠標準化數據的函數:

> (defun standardize (x)
     (let ((x-bar (mean x))
           (s (standard-deviation x)))
        (/ (- x x-bar) s)))
使用第5.6.2節的stack loss數據,咱們能夠構造4個標準化的變量:
(setf std-air (standardize air))
(setf std-temp (standardize temp))
(setf std-conc (standardize conc))
(setf std-loss (standardize loss))
plot-points函數能夠用來設置前兩個變量的圖形:
(setf w (plot-points std-air std-temp))

咱們須要設置這兩個變量的範圍,以確保它們足夠大可以顯示全部數據的旋轉效果。對於標準化的數據,[-3,3]這一區間對這兩個變量就足夠大了。由於:range消息對應的方法是矢量化的,這兩個變量的範圍均可以使用下式來設置:

> (send w :range '(0 1) -3 3)
      在咱們使用下式將點數據的索引儲存到  變量indices以後:
> (setf indices (iseq (length std-air)))
下式定義的interpolate函數帶一個點參數,其範圍在[0,1],計算對應的角度,而後將圖形裏的變量設置到角度值得的差值上去。
> (defun interpolate (p)
    (let* ((alpha (* (/ pi 2) p))
           (s (sin alpha))
           (c (cos alpha))
           (x (+ (* c std-air) (* s std-temp)))
           (y (+ (* c std-conc) (* s std-loss))))
       (send w :point-coordinate 0 indices x)
       (send w :point-coordinate 1 indices y)
       (send w :redraw-content)))
插值後的結構可使用這樣的循環來運行:
> (dolist (p (rseq 0 1 30)) (interpolate p))
或者使用下式構造的滑塊來運行:
> (interval-slider-dialog '(0 1) :action #'interpolate)
就像冪轉換那個例子同樣,該滑塊應該以繪圖窗體下級的控件的角色進行註冊,以確保當窗體關閉的時候會隨之關閉。

    構造一個三角圖形插值的第二個方法就是使用一個四維散點圖,而後利用graph-proto原型提供的變換系統來完成。plot-points函數可再一次用來設置該圖形,該函數能夠接受一個四個變量的列表,同時也接受由:scale-type關鍵字指定的尺度類型:

> (setf w (plot-points (list std-air std-conc std-loss std-temp)
                       :scale-type 'fixed))
這裏使用fixed這一尺度類型是合適的,由於咱們要使用標準化標量。

    插值函數能夠這樣定義:

> (defun interpolate (p)
    (let* ((alpha (* (/ pi 2) p))
           (s (sin alpha))
           (c (cos alpha))
           (m (make-array '(4 4) :initial-element 0)))
      (setf (aref m 0 0) c)
      (setf (aref m 0 2) s)
      (setf (aref m 1 1) c)
      (setf (aref m 1 3) s)
      (send w :transformation m)))
該定義裏的變換矩陣不是正定變換,由於它將第3個和第4個轉換後的座標設置爲0,咱們可使用正定矩陣,可是這裏並不須要。

    interpolate函數能夠像之前同樣,從一個循環或一個滑塊對話框開始運行。圖10.1展現該圖形插值的4個視圖。

圖10.1 stack loss數據的4個插值視圖

    冪轉換動畫和圖形插值動畫之間有不少類似的地方,可是仍有一個主要的不一樣之處。在冪轉換的例子裏,其目標是找到一個合理的冪值,並探討其附近的值。使用這個動畫,咱們一般能夠在一個較寬範圍內開始,而後縮小這個範圍。相反地,在圖形插值裏一般幾乎想要從開始的座標對到終止的座標對,完整地運行該動畫。當數據進行旋轉操做時,中間的圖形確實有像數據的旋轉操做同樣的表達能力,可是在插值圖形裏它們不是最受關注的。中間圖形的目的是當觀察者從一個散點圖向另外一個散點圖進行移動時,容許他們跟蹤單獨的點或點羣。所以,插值圖形的目標與鏈接兩個散點圖的目標是類似的。結果,就像在9.1.7節的例子裏構造的按鈕疊置層同樣,對於運行圖形插值來講,一個按鈕控件與滾動條相比,多是一個更好的繪圖控件。

練習 10.2
略。

10.1.3 選擇平滑參數

到目前爲止,咱們討論的連個例子使用了動畫技術展現點數據的變化。動畫也可用於線數據的展現。例如,對於降雨量數據的核密度估計,咱們可使用動畫技術提供一個圖形化的方法來選擇其平滑參數。降雨量數據的範圍能夠這樣給出:

> (min precipitation)
0.32
> (max precipitation)
4.75
kernel-dens函數接收窗體寬度參數,它可由:width關鍵字提供。對於降雨量數據的範圍,初始大小爲1的窗體多是合理的,下式將使用默認的雙平方核和寬度爲1的窗體來設置一個圖形:
> (setf w (plot-lines (kernel-dens precipitation :width 1)))
    由於咱們可能既想改變核的寬度,又想改變核的類型,全部咱們能夠安裝幾個槽,用來保留圖形裏的當前核規則。
> (send w :add-slot 'kernel-width 1)

> (send w :add-slot 'kernel-type 'b)

圖10.2 使用雙平方核和2個不一樣的平滑參數值的降雨量數據的核密度估計

假設該密度估計的圖形可使用:set-lines消息來重置,這些槽的獲取方法能夠這樣定義:

> (defmteh w :kernel-width (&optional width)
    (when width
          (setf (slot-value 'kernel-width) width)
          (send self :set-lines))
    (slot-value 'kernel-width))

> (defmteh w :kernel-type (&optional type)
    (when width
          (setf (slot-value 'kernel-type) type)
          (send self :set-lines))
    (slot-value 'kernel-type))

:set-lines方法可使用:clear-lines和:add-lines消息來定義:

> (defmeth w :set-lines ()
    (let ((width (send self :kernel-width))
          (type (send self :kernel-type)))
      (send self :clear-lines :draw nil)
      (send self :add-lines
            (kernel-dens precipitation
                         :width width :type type))))
爲了不linestart裏有任何的顏色、寬度或者線型信息的丟失,咱們可使用:linestart-coordinate消息來代替。可是與維護點數據的屬性相比,維護linestart屬性一般沒有那麼重要。

    再一次,滑塊能夠用來改變該核窗體,對於這個數據,咱們能夠設置一個區間爲[0.25 1.5]合理的範圍內,用來討論:

> (interval-slider-dialog '(.25 1.5)
                            :action
                            #'(lambda (s)
                                (send w :kernel-width s)))
該滑塊值應該設置爲圖形的當前核值,同時該滑塊應該註冊爲繪圖窗體的子窗體。默認地,滑塊的顯示區域將顯示當前窗體的寬度。一個替代無就是,咱們可讓它顯示判據的一個值,該判據是用於選擇針對文獻裏可用的平滑參數。圖示10.2展現了針對兩個不一樣的窗體寬度的密度估計。

    爲了簡化核類型的變化,咱們能夠定義一個:choose-kernel方法,它能夠彈出一個對話框來選擇核:

> (defmeth w :choose-kernel ()
    (let* ((types '("Bisquare" "Gaussian" "Triangle" "Uniform"))
           (i (choose-item-dialog "Kernel Type" types)))
      (if i (send w :kernel-type (select '(b g t u) i)))))
該消息能夠從鍵盤發送出去,或者你也能夠構造一個菜單項:
> (setf kernel-item
    (send menu-item-proto :new "Kernel Type"
          :action #'(lambda () (send w :choose-kernel))))
而後將該菜單項安裝到圖形菜單裏:
> (send (send w :menu) :append-items kernel-item)
練習 10.3 

練習 10.4

10.2 使用新的鼠標模式

圖形響應用戶動做是依靠它的鼠標模式。經過向圖形裏添加鼠標模式,咱們能夠獲取不少效果。本節將戰術3個實例,用來講明可能性範圍。

10.2.1 簡單線性迴歸裏的敏感性

第一個例子是一個可交互的圖形,用來展現最小二乘迴歸線的槓桿影響。一個散點圖展現帶最小二乘迴歸線的(x,y)點對集合。有一個新的鼠標模式,在該模式中經過點擊附近的點並拖拽的方法,達到在圖形裏移動點的目的。該回歸線在當鼠標按鍵釋放時進行重畫。該例子的首要做用是做爲一個指導性的工具,可是,基於該思想的變種做爲探索性的工具也是頗有用的。

    爲了構造這個實例,咱們可使用下式給定的模擬數據做爲開始:

(setf x (append (iseq 1 18) (list 30 40)))
(setf y (+ x (* 2 (normal-rand 20))))

值y遵循這樣的規律:帶單位斜率的正態線性迴歸,點中的兩個x值給予他們一個大的槓桿調節。下邊的表達式將爲這些數據構造一個散點圖:

(setf w (plot-points x y))
    一個新的鼠標模式——point-moving這樣定義:
(send w :add-mouse-mode 'point-moving
      :title "Point Moving"
      :cursor 'finger
      :click :do-point-moving)
該消息對應的方法:do-point-moving可使用:drag-point消息來定義:
(defmeth w :do-point-moving (x y a b)
  (let ((p (send self :drag-point x y :draw nil)))
    (if p (send self :set-regression-line))))
    消息:set-regression-line負責調整迴歸線以適應圖形中的點數據。它對應的方法這樣定義:


(defmeth w :set-regression-line ()
  (let ((coefs (send self :calculate-coefficients)))
    (send self :clear-lines :draw nil)
    (send self :abline (select coefs 0) (select coefs 1))))
該定義假設:calculate-coefficients消息能夠用來肯定當前數據的迴歸係數。對於最小二乘擬合方法,該消息對應的方法能夠這樣定義:
(defmeth w :calculate-coefficients ()
  (let* ((i (iseq 0 (- (send self :num-points) 1)))
         (x (send self :point-coordinate 0 i))
         (y (send self :point-coordinate 1 i))
         (m (regression-model x y :print nil)))
    (send m :coef-estimates)))
從繪圖處理裏分離擬合處理的好處是,只須要重定義:calculate-coefficients方法就能引入一個新的擬合方法。

    爲了完成這個實例,咱們能夠加入一條初始迴歸線,而後將圖形置於point-moving模式中:

(send w :set-regression-line)

(send w :mouse-mode 'point-moving)

圖10.3展現了移除點數據中的一個先後的圖形:

圖10.3:移除最右側的數據點以前與以後的數據與迴歸線

    該實例能夠經過幾種方法來改進。例如,咱們能夠重定義:calculate-coefficients方法,目的是僅使用圖形裏的可見數據點:

(defmeth w :calculate-coefficients ()
  (let* ((i (send self :points-showing))
         (x (send self :point-coordinate 0 i))
         (y (send self :point-coordinate 1 i))
         (m (regression-model x y :print nil)))
    (send m :coef-estimates)))
由於散點圖裏用來維護點數據狀態的系統 ,一般會設置:need-adjusting標識,時機是 當一個點數據的狀態改變爲或者改變自非可見的狀況,並使用下式覆蓋:adjust-screen方法的時候,該覆蓋方法圖形裏的或其任意鏈接圖形裏的點變爲不可見時確保迴歸線能夠從新計算。

練習 10.5
略。

聯繫 10.6
略。

10.2.2 手動旋轉

一個圖形上的旋轉控制容許圖形繞屏幕的x、y及垂直於屏幕的軸旋轉。其它的控制策略也可使用,其中的一種策略基於:想象數據是包含於一個「球」內的,這個「球」能夠經過鼠標進行抓取和移動,當釋放鼠標按鍵的時候,旋轉操做能夠中止或者繼續:若是使用帶有擴展標示符(shift,alt等鍵)的「抓取」操做則繼續。該控制策略能夠經過定義一個新的鼠標模式來實現。由於這個鼠標模式對圖形旋轉操做是有用的,咱們能夠在spin-proto原型裏安裝它。

    這個新的鼠標模式能夠這樣定義:

> (send spin-proto :add-mouse-mode 'hand-rotate
        :title "Hand Rotate"
        :cursor 'hand
        :click :do-hand-rotate)
HAND-ROTATE
對於該模式來講,手型圖標看起來是一個天然的選擇。

    爲了開發針對這個新模式的點擊方法,咱們須要一個這樣的方法:它能夠將一個旋轉圖形窗體裏的點擊事件轉換爲一個球上的數據點:

> (defmeth spin-proto :canvas-to-sphere (x y rad)
    (let* ((p (send self :canvas-to-scaled x y))
           (x (first p))
           (y (second p))
           (norm-2 (+ (* x x) (* y y)))
           (rad-2 (^ rad 2))
           (z (sqrt (max (- rad-2 norm-2) 0))))
      (if (< norm-2 rad-2)
          (list x y z)
          (let ((r (sqrt (/ norm-2 rad2))))
            (list (/ x r) (/ y r) (/ z r))))))
:CANVAS-TO-SPHERE
傳遞給該方法的參數是點擊處的畫布座標和尺度座標系裏的球體半徑。球體將與屏幕表面相交,該屏幕在一個指定半徑的圓的內部,在該圓內部的一次點擊將轉換爲上述點擊對應的球體上的數據點。在該圓形外部的點擊將被投影到圓的外部。

    使用:canvas-to-sphere消息,:do-hand-rotate方法這樣定義:

(defmeth spin-proto :do-hand-rotate (x y m1 m2)
  (let* ((m (send self :num-variables))
         (range (send self :scaled-range 0))
         (rad (/ (apply #'- range) 2))
         (oldp (send self :canvas-to-sphere x y rad))
         (p oldp)
         (trans (identity-matrix m)))
    (flet ((spin-sphere (x y)
             (setf lodp p)
             (setf p (send self :canvas-to-sphere x y rad))
             (setf (select trans vars vars)
                   (make-rotation oldp p))
             (when m1
                   (send self :rotation-type trans)
                   (send self :idle-on t))
             (send self :apply-transformation trans)))
    (send self :idle-on nil)
    (send self :while-button-down #'spin-sphere))))

局部函數spin-sphere用來做爲:while-button-down消息的動做函數。他使用了三個變量,這三個變量都是在局部函數環境裏定義的。變量p和oldp表明當前和前一個點擊事件的位置,並將它們轉換到球體上;變量trans表明一個變換矩陣,該矩陣標識除了內容變量所在的行與列的剩下的元素組成的矩陣。該定義是須要的,由於旋轉圖形能夠用在超過三個變量的時候。spin-sphere函數更新oldp和p的值,而後使用新值做爲make-rotation的參數,它還會返回一個旋轉矩陣,該矩陣將在一個平面裏旋轉圖形,這個平面是由這兩個點定義的,保證固定的正交補。在使用該旋轉操做以前,spin-sphere會檢查擴展標識符(便是否按下了alt或shift等鍵),若是標識符的值爲非nil,打開空置功能,旋轉類型被設置爲當前步進變換。旋轉圖形對應的:do-idle在每次發送:apply-transformation方法的時候使用這個矩陣。

10.2.3 圖形函數輸入

有這樣一些狀況,在這些狀況裏過程(至關於函數)須要一個正值函數做爲輸入。一個例子就是關於一個實值量的先驗密度函數的啓發。在一些狀況下,該函數被指定爲一個過程或者一個變量是充分需求,可是在其它的狀況下容許將函數被圖形化地指定將更加方便。一個用來圖形化地指定一個函數應該是這樣的:

  • 容許設置和獲取當前的函數
  • 強制爲過程輸入一個真正的函數,也就是防止一個單獨的x值對應多個y值
強制函數爲連續性函數多是有用的。

    爲了構造一個圖形,用來在單位區間來指定正值函數,咱們能夠經過構造一個圖形了開始,這裏的圖形包含一個50個互相聯通的點的序列,x值在單位區間裏的是等間隔的,y值等於0:

(setf p (plot-lines (rseq 0 1 50) (repeat 0 50)))
若是咱們主要對函數的形狀感興趣,咱們可使用下式移除y座標軸:
(send p :y-axis nil)
圖形裏起始的點表示一個值爲0的函數。爲了容許該函數使用鼠標來改變,咱們能夠指定一個新的模式:
(send p :add-mouse-mode 'drawing
      :title "Drawing"
      :cursor 'finger
      :click :mouse-drawing)
如下表達式將圖形放置到新的模型裏。

    點擊消息:mouse-drawing對應的方法將鼠標點擊處的x座標做爲參數,使用該值來區分與linestart最近的x座標,而後將linestart的y值改變成點擊處的y值。當拖動鼠標的時候,x值被鼠標穿過的linestarts數據須要讓他們的y值也獲得調整。:mouse-drawing方法的一個簡單的實現能夠是這樣的:

(defmeth p :mouse-drawing (x y m1 m2)
  (flet ((adjust (x y)
          (let* ((n (send self :num-lines))
                 (reals (send self :canvas-to-real x y))
                 (i (x-index (first reals) n))
                 (y (second reals)))
            (send self :linestart-coordinate 1 i y)
            (send self :redraw-content))))
    (adjust x y)
    (send self :while-button-down #'adjust)))
函數x-index這樣定義:
(defun x-index (x n)
  (max 0 (min (- n 1) (floor (* n x)))))
    若是鼠標被點擊和緩慢地拖拽,這個定義就會生效。可是若是鼠標快速地移動,它可能會忽略一些linestarts數據點,致使一個帶峯值的函數。爲了不這個問題,咱們可使用一個略微精心製做的:mouse-drawing方法,該方法將在點數據之間進行線性差值,這些點數據會被傳遞到鼠標按下時觸發的動做函數裏。
(defun interpolate (x a b ya yb)
  (let* ((range (if-else (/= a b) (- b a) 1))
         (p (pmax 0 (pmin 1 (abs (/ (- x a) range))))))
    (+ (* p yb) (* (- 1 p) ya))))
這個函數將處於點(a,ya)和(b,yb)之間的x參數對應的y值進行線性差值。這裏的a可能比b大,全部的參數多是複合數據。若是a與b相等,if-else表達式須要避免被0除。使用interpolate函數,咱們能夠這樣定義:mouse-drawing方法:
(defmeth p :mouse-drawing (x y m1 m2)
  (let* ((n (send self :num-lines))
         (reals (send self :canvs-to-real x y))
         (old-i (x-index (first reals) n))
         (old-y (second reals)))
    (flet ((adjust (x y)
              (let* ((reals (send self :canvas-to-real x y))
                     (new-i (x-index (first reals) n))
                     (new-y (second reals))
                     (i (iseq old-i new-i))
                     (yvals (interpolate
                             i old-i new-i old-y new-y)))
                (send self :linestart-coordinate 1 i yvals)
                (send self :redraw-content)
                (setf old-i new-i)
                (setf old-y new-y))))
  (adjust x y)
  (send self :while-button-down #'adjust))))
局部函數adjust在其上下文環境裏使用old-i和old-y變量,目的是容納最近一次的adjust函數調用相對應的值。前一個和當前鼠標位置之間的linestart數據是線性差值的。

    圖形p的linestart數據包括它的函數的值,該函數處於x值的網格。:lines消息對應的方法這樣來定義:

(defmeth p :lines ()
  (let ((i (iseq (send self :num-lines))))
    (list (send self :linestart-coordinate 0 i)
          (send self :linestart-coordinate 1 i))))
它能夠用來獲取當前函數的值,這些值以x值列表和y值列表的列表形式獲取。

練習 10.7
略。

練習 10.8
略。

練習 10.9
略。

10.3 圖形控制疊置

繪圖動做能夠從菜單、對話框處獲得控制,也能夠在處於圖形自身的疊置層內部進行控制。本節描述兩種簡單的疊置層控制原型,而後展現如何針對旋轉圖形爲這些原型添加額外的功能。

10.3.1 按鈕控制

按鈕是由表明按鈕自身的小正方形和繪製在其右側的標籤字符串組成的。當鼠標在正方形內部點擊的時候,就會按壓到該按鈕,正方形將高亮,而後將持續調用一個動做函數直到按鈕釋放。

按鈕原型

按鈕原型這樣定義:

(defproto button-overlay-proto
          '(location title)
          nil
          graph-overlay-proto)
原型中兩個槽的讀取函數定義以下:
> (defmeth button-overlay-proto :location (&optional new)
    (if new (setf (slot-value 'location) new))
    (slot-value 'location))

> (defmeth button-overlay-proto :title (&optional new)
    (if new (setf (slot-value 'location) new))
    (slot-value 'title))
這兩個讀取方法都沒有任何錯誤檢查,另外若是修改了槽的值也不會視圖重畫控件。

    咱們可使用這兩個讀取函數爲原型的location槽和title槽賦合理的初始化值:

> (send button-overlay-proto :locatiion '(0 0))

> (send button-overlay-proto :title "Button")
位置也能夠經過:resize方法來改變。

    爲有助於定位按鈕,咱們須要可以肯定包圍按鈕的矩形的大小,假設location槽包含該矩形區域的左上角座標,該矩形的尺寸取決於包含按鈕疊置層的圖形的文本字體的大小。按鈕自己是一個正方形,他的邊在數值上與字符頂部到基線的距離相等。按鈕和標籤字符串之間會放置一個空白,其大小是字符頂部到基線的距離的一半,與這個空白相同尺寸的邊距將會被放置到字符串與按鈕之間。使用這樣的佈局,返回該矩形的寬度與高度的列表的:size方法這樣定義:

(defmeth button-overlay-proto :size ()
    (let* ((graph (send self :graph))
           (title (send self :title))
           (text-width (send graph :text-width title))
           (side (send graph :text-ascent))
           (gap (floor (/ side 2)))
           (descent (send graph :text-descent))
           (height (+ side descent (* 2 gap))))
      (list (+ side (* 3 gap) text-width) height)))
    基於剛剛描述的佈局,一個返回按鈕正方形的矩形座標的列表的方法能夠這樣給定:
(defmeth button-overlay-proto :button-box ()
    (let* ((graph (send self :graph))
           (loc (send self :location))
           (side (send graph :text-ascent))
           (gap (floor (/ side 2))))
      (list (+ gap (first loc)) (+ gap (second loc)) side side)))
下邊這個方法是用來計算標籤字符串繪製位置的:
(defmeth button-overlay-proto :title-start ()
    (let* ((graph (send self :graph))
           (loc (send self :location))
           (title (send self :title))
           (side (send graph :text-ascent))
           (gap (floor (/ side 2))))
      (list (+ (* 2 gap) side (first loc))
            (+ gap side (second loc)))))
    繪製該按鈕的方法這樣給定:
(defmeth button-overlay-proto :draw-button (&optional paint)
    (let ((box (send self :button-box))
          (graph (send self :graph)))
      (apply #'send graph :erase-rect box)
      (if paint
          (apply #'send graph :paint-rect box)
          (apply #'send graph :frame-rect box))))
這個方法帶一個可選參數,若是該參數是非nil的,按鈕將以高亮狀態繪製;不然,按鈕只是簡單地構造出來。這裏的apply函數是須要的,由於box變量包含一個參數列表,矩形繪製函數須要該參數列表。

    繪製標籤字符串的方法能夠這樣定義:

(defmeth button-overlay-proto :draw-title ()
    (let ((graph (send self :graph))
          (title (send self :title))
          (title-xy (send self :title-start)))
      (apply #'send graph :draw-string title title-xy)))
使用這兩個方法,:redraw方法這樣定義:
(defmeth button-overlay-proto :redraw ()
    (send self :draw-title)
    (send self :draw-button))
    爲了肯定疊置層是否應該響應點擊事件,咱們須要肯定點擊的位子是否位於按鈕正方形的範圍內。這個工做能夠用下邊定義的方法來定義:
(defmeth button-overlay-proto :point-in-button (x y)
    (let* ((box (send self :button-box))
           (left (first box))
           (top (second box))
           (side (third box)))
      (and (< left x (+ left side)) (< top y (+ top side)))))
咱們假定按鈕疊置層有一個:do-action方法,使用該方法能夠實現按鈕的動做。對於點擊操做,該動做可能須要利用修飾符(shift或alt鍵)。咱們可能想要這個動做在鼠標按下的初始狀態和隨後的調用中表現不一樣的行爲,爲了容許這種可能性,咱們可使用下邊這一協定,即發送只帶一個參數的:do-action消息。在一次點擊以後的第一次調用的時候,參數是一個帶有兩個修飾符的列表;在後續的調用中,這個參數爲nil。:do-click方法能夠這樣定義:
(defmeth button-overlay-proto :do-click (x y m1 m2)
    (let ((graph (send self :graph)))
      (when (send self :point-in-button x y)
            (send self :draw-button t)
            (send self :do-action (list m1 m2))
            (send graph :while-button-down
                  #'(lambda (x y) (send self :do-action nil)) nil)
            (send self :draw-button nil)
            t)))
由於發送了第二個參數爲nil的:while-button-down消息,當按鈕按下的時候它的動做函數將持續調用。

    爲了完成按鈕原型的定義,咱們能夠給出一個不作任何事情的:do-action方法:

(defmeth button-overlay-proto :do-action (x) nil)
一個應用:滾動一個可旋轉圖形

在計算機上旋轉一個圖形帶來的問題是:當旋轉中止的時候,由動做建立的深度暗示將會消失。換句話說,當圖形旋轉的時候,很難將注意力集中到一個特定的視圖上。解決這個問題的一個方法就是容許圖形來回翻轉,在垂直於屏幕的座標軸附近的每一個方向上經過一個很小的數量旋轉圖形。翻轉動做提出了深度錯覺,可是數據視圖基本上保持不變。一個用來翻轉可旋轉圖形的方法能夠這樣來定義:

(defmeth spin-proto :rock-plot (&optional (a . 15))
    (let* ((angle (send self :angle))
           (k (round (/ a angle))))
      (dotimes (i k) (send self :rotate-2 0 2 angle))
      (dotimes (i (* 2 k)) (send self :rotate-2 0 2 (- angle)))
      (dotimes (i k) (send self :rotate-2 0 2 angle))))
該方法經過一個方向上的指定角度進行旋轉,在相反方向上以該角度的兩倍進行旋轉,而後旋轉回原始位置。私用的默認角度是0.15弧度。

    按鈕提供了一個便捷的方式,將:rock-plot消息發送到一個旋轉圖形裏。使用按鈕原型,咱們能夠爲翻轉一個旋轉圖形定義一個新的原型:

(defproto spin-rock-control-proto () () button-overlay-proto)
這個按鈕對應的標題能夠這樣設置:
(send spin-rock-control-proto :title "Rock Plot")
對於翻轉按鈕的動做方法將向圖形發送:rock-plot消息。
(defmeth spin-rock-control-proto :do-action (first)
  (send (send self :graph) :rock-plot))
    按鈕的一個很天然的位置就是沿着圖形的底端,同時標準的旋轉控件。:resize方法將會把按鈕的位置設置到圖形的右側較低的角落裏:
(defmeth spin-rock-control-proto :resize ()
  (let* ((graph (send self :graph))
         (size (send self :size))
         (width (send graph :canvas-width))
         (height (send graph :canvas-height)))
    (send self :location (- (list width (+ 3 height)) size))))
這裏的3像素高度的調整是須要的,目的是與Macintosh操做系統的標準控件的按鈕對其。

    舉個例子,使用第2.5.1節裏的磨損數據,如下表達式將構造一個旋轉圖形,並在圖形裏安裝一個翻轉按鈕:

(let ((w (spin-plot
           (list hardness tensile-strength abrasion-loss)))
      (b (send spin-rock-control-proto :new)))
  (send w :add-overlay b)
  (send b :resize)
  (send b :redraw))

10.3.2 雙按鈕控件

雙按鈕控件原型

旋轉圖形上的標準控件是雙按鈕控件,一個按鈕用來產生正角度,另外一個用來產生負角度。雙按鈕控件對應的原型能夠從頭構造,就像點擊按鈕同樣。可是在點擊按鈕和雙按鈕之間有至關大的類似處,因此咱們能夠經過定義這個繼承自點擊按鈕的新的原型來省些事情

(defproto twobutton-control-proto () () button-overlay-proto)

這裏繼承的使用與線性模型裏的非線性迴歸模型的定義是類似的。

    雙按鈕控件的佈局與單按鈕控件佈局類似,只不過增長了一個額外的按鈕而已,那麼:size方法也可使用繼承來的方法,而且能夠爲兩個按鈕之間增長一塊空白空間:

(defmeth twobutton-control-proto :size ()
    (let* ((graph (send self :graph))
           (size (call-next-method))
           (side (send graph :text-ascent))
           (gap (floor (/ side 2))))
      (list (+ gap side (first size)) (second size))))
:title-start方法也可使用繼承來的方法,可是從頭來定義它也很容易:
(defmeth twobutton-control-proto :title-start ()
    (let* ((graph (send self :graph))
           (loc (send self :location))
           (title (send self :title))
           (side (send graph :text-ascent))
           (gap (floor (/ side 2))))
      (list (+ (* 3 gap) (* 2 side) (first loc))
            (+ gap side (second loc)))))
    因爲如今有兩個按鈕,咱們須要一種方式區分它們。符號 - 和 +將分別用於左邊和右邊的按鈕。這裏的:button-box方法帶一個參數,即button符號:
(defmeth twobutton-control-proto :button-box (which)
    (let* ((graph (send self :graph))
           (loc (send self :locatiion))
           (side (send graph :text-ascent))
           (gap (floor (/ side 2)))
           (left (case which
                   (+ (+ gap (first loc)))
                   (- (+ (* 2 gap) side (first loc))))))
      (list left (+ gap (second loc)) side side)))
:draw-button方法這樣定義:
(defmeth twobutton-control-proto :draw-button (which &optional paint)
    (let ((box (send self :button-box which))
          (graph (send self :graph)))
      (cond (paint (apply #'send graph :paint-rect box))
        (t (apply #'send graph :erase-rect box)
           (apply #'send graph :frame-rect box)))))
它也須要一個button符號參數,而且帶一個可選的參數來指定該按鈕是否高亮。:redraw方法如今能夠這樣給出:
(defmeth twobutton-control-proto :redraw ()
    (send self :draw-title)
    (send self :draw-button '-)
    (send self :draw-button '+))
    :point-in-button方法是用來作這樣的事的:若是點擊處的座標沒有落在按鈕範圍內,返回nil;若是它們落在按鈕範圍內的話,將返回button符號:
(defmeth twobutton-control-proto :point-in-button (x y)
    (let* ((box1 (send self :button-box '-))
           (box2 (send self :button-box '+))
           (left1 (first box1))
           (top (second box1))
           (side (third box1))
           (left2 (first box2)))
      (cond
        ((and (< left1 x (+ left1 side)) (< top y (+ top side))) '-)
        ((and (< left2 x (+ left1 side)) (< top y (+ top side))) '+))))
點擊方法能夠這樣定義:
(defmeth twobutton-control-proto :do-click (x y m1 m2)
    (let ((graph (send self :graph))
          (which (send self :point-in-button x y)))
      (when which
            (send self :draw-button which t)
            (send self :do-action which (list m1 m2))
            (send graph :while-button-down
                  #'(lambda (x y)
                      (send self :do-action which nil))
                  nil)
            (send self :draw-button which nil)
            t)))
:do-action消息的發送須要兩個參數,第一個參數是一個button符號,第二個參數由第一次調用時的修飾符列表組成,在隨後的調用過程當中將返回nil,默認的:do-action方法這樣給定:
(defmeth twobutton-control-proto :do-action (which mods) nil)
應用舉例:繞座標軸旋轉

用於旋轉圖形的標準控件容許圖形繞屏幕座標旋轉,有時候繞着一個座標軸旋轉是有用的,這隻有在三維座標系,由於在更高維度的狀況下,座標軸和角度不能惟一指定一次旋轉操做。

    下邊這個方法將一個圖形的rotation-type設置成爲一個矩陣,該矩陣由經過索引參數v指定的座標軸和繞着該座標軸轉的角度組成。

(defmeth spin-proto :set-axis-rotation (v)
    (let* ((m (send self :num-variables))
           (v1 (if (= v 0) 1 0))
           (v2 (if (= v 2) 1 2))
           (trans (send self :transformation))
           (cols (column-list
                  (if trans trans (identity-matrix m))))
           (x1 (select cols v1))
           (x2 (select cols v2))
           (angle (send self :angle)))
      (send self :rotation-type (make-rotation x1 x2 angle))))
繞指定座標軸旋轉的雙按鈕原型能夠這樣定義:
(defproto spin-rotate-control-proto
    '(v) () twobutton-control-proto)
槽v表明旋轉軸的索引,:isnew方法定義以下:
(defmeth spin-rotate-control-proto :isnew (v &rest args)
    (apply #'call-next-method :v v args))
該方法帶一個索引爲參數,並使用繼承來的:isnew方法將該索引值安置到槽v裏。

    座標軸旋轉按鈕的標籤字符串能夠經過使用:variable-label消息來從圖形裏讀取:

(defmeth spin-rotate-control-proto :title ()
    (send (send self :graph) :variable-label (slot-value 'v)))
:do-action方法這樣定義:
(defmeth spin-rotate-control-proto :do-action (sign mods)
    (let ((graph (send self :graph)))
      (if mods
          (let ((v (slot-value 'v))
                (angle (abs (send graph :angle))))
            (send graph :idle-on (first mods))
            (send graph :angle
                  (if (eq sign '+) angle (- angle)))
            (send graph :set-axis-rotation v)))
      (send graph :rotate)))

該方法在點擊的時候設置爲座標軸旋轉,並在後續的調用中發送:rotate消息。角度的正負能夠根據按下的按鈕來調整,若是提供了帶點擊的擴展修飾符,那麼將開始一段空置時間。

    咱們在使用一次上文提到的磨損數據,如下表達式將設置一個旋轉圖形,並給出3個軸的旋轉控制,每一個數據軸一個:

(flet ((width (c) (first (send c :size))))
    (let* ((w (spin-plot
               (list hardness tensile-strength abrasion-loss)))
           (c0 (send spin-rotate-control-proto :new 0))
           (c1 (send spin-rotate-control-proto :new 1))
           (c2 (send spin-rotate-control-proto :new 2)))
      (send w :add-overlay c0)
      (sedn w :add-overlay c1)
      (send w :add-overlay c2)
      (let ((width (max (mapcar #'width (list c0 c1 c2))))
            (height (second (send c0 :size)))
            (margin (send w :margin)))
        (send c1 :location (list 0 height))
        (send c2 :location (list 0 (* 2 height)))
        (send w :margin width 0 0 (fourth margin)))))
新的控件將放置在圖形的左邊,:margin方法將向圖形發送:resize和:redraw消息,這些消息對應的方法將向疊置層發送對應的消息,結果圖形見10.4。

圖10.4 帶軸旋轉控件的磨損數據旋轉圖

10.4 grand tours

最近,爲了探討多維數據咱們提出的方法是grand tour,它的基本意思就是找出數據的一維、二維或者三維投影的序列的一種,在全部可能的投影中間這些序列會迅速地變得稠密。該序列能夠針對那些極其「不一樣尋常的」數據視圖進行檢測,就像展現聚類和其它結構的視圖那樣。這類檢測能夠經過兩種方式完成,一個是經過對投影進行統計計算在數值上進行完成,還有一個方法是經過顯示投影的影像來圖形化地完成。對於數值化的方法,投影序列不須要任何附加的結構;對於圖形化的方法,可能須要提供持續變化的能力,目的是讓觀察者能夠容易地進行單點和點羣跟蹤。

    在Lisp-Stat裏實現m維圖形grand tour的一種方法就是選擇一個旋轉序列,當開始該旋轉時觀察圖形。與短程grand tour方法接近的模式是這樣的:選擇m維的單位球面的兩點,使用這兩個點定義一個平面,而後構造一個旋轉序列,當保持固定平面的正交互補的同事將第一個點帶入到第二個點裏。當到達第二個點時,使用另外一個點對重複該過程。在該模式上的輕微的改變能夠用於構造增量旋轉,在切換到一個新的旋轉以前,本次旋轉將使用由時間產生的隨機數。做用到旋轉上的時間數值能夠均勻地選取[0, pi/2a]裏非負整數。

    該模式能夠經過使用一些基本原型來實現。旋轉圖形已經提供了對速度的控制,提供了使用增量旋轉的系統。換句話說,在一維投影中,直方圖能夠給出更好的點密度視圖,對於探測偏離度來講會更有用。對於基於旋轉圖形和直方圖的tours,咱們無需實現每一個tour的全部特徵,咱們能夠利用對象系統的多重繼承能力,經過定義一個mixin來維持tour所需的方法和槽規則。那麼,基於旋轉圖形的tour原型,能夠經過使用mixin和spin-proto做爲其父類的方式來構造。

    爲了實現上邊着重提到的策略,tour圖形使用make-rotation函數來構造矩陣,用來從一個點向另外一個點進行旋轉轉換。旋轉操做使用一個由當前旋轉速度肯定的角度,旋轉操做須要使用屢次來將球形上的第一個點映射到第二個點上。當到達第二個點的時候,將產生一個新的旋轉增量,旋轉操做使用的次數也會獲得計算。那麼這裏的tour mixin原型能夠這樣定義:

(defproto tour-mixin '(tour-count tour-trans))
這兩個槽表示增量旋轉矩陣和須要應用此操做的次數。mixin沒有父類,由於它是用來與其它圖形原型聯合使用的。

    tour處理過程就是持續地運行,所以它能夠經過定義:do-idle方法來實現:

(defmeth tour-mixin :do-idle () (send self :tour-step))
這裏的:tour-step是該系統的主要部分:
(defmeth tour-mixin :tour-step ()
    (when (< (slot-value 'tour-count) 0)
          (flet ((sphere-rand (m)
                              (let* ((x (normal-rand m))
                                     (nx2 (sum (^ x 2))))
                                (if (< 0 nx2)
                                    (/ x (sqrt nx2))
                                    (/ (repeat 1 m) (sqrt m))))))
            (let* ((m (send self :num-variables))
                   (angle (send self :angle))
                   (mx (+ 1 (abs (floor (/ pi (* 2 angle)))))))
              (setf (slot-value 'tour-count) (random max))
              (setf (slot-value 'tour-trans)
                    (make-rotation (sphere-rand m)
                                   (spere-rand m)
                                   angle)))))
    (send self :apply-transformation (slot-value 'tour-trans))
    (setf (slot-value 'tour-count)
          (- (slot-value 'tour-count) 1)))

該方法檢測tour-count槽,以查明其是否小於0,若是不小於0,其值將逐次遞減1,將會使用到tour-trans槽的值;若是tour-count槽的值小於0,該方法首先將計算新的變換矩陣和計數。局部函數sphere-rand用來在m維的單元球上生成正態散點,它是經過正規化m維獨立標準正態隨機變量獲得的。爲了安全起見,將檢測除數爲0的狀況。使用sphere-rand生成兩個點,經過向圖形發送:angle消息能夠獲取角度,其結果將被傳遞到make-rotation函數以構造新的增量旋轉矩陣,random函數將構造一個新的計數。

    爲了使該方法更好地工做,圖形必須有一個:angle方法,由於spin-proto圖形已經有這個方法了,它就不會包含到tour-mixin裏了。基於其它原型的Tour圖形加入它們本身的angle方法。

    爲了保證能夠計算新的變換矩陣和計數,首次要使用:tour-step方法,咱們能夠將tour-count槽的值設置成一個負數:

(send tour-mixin :slot-value ;tour-count -1)
爲了能夠打開和關閉tour功能,或者爲了確認tour功能是否打開,咱們能夠定義一個:tour-on方法。在可能的狀況下,該方法的最簡單的版本就是想:idle-on消息傳遞參數:
(defmeth tour-mixin :tour-on (&rest args)
    (apply #'send self :idle-on args))
晚些時候咱們可能想要一個更加精心設計的定義。

    爲了容許向圖形菜單上加入一個菜單項,用來打開和光比tour功能,咱們能夠定義一個tour-item-proto原型:

(defproto tour-item-proto '(graph) () menu-item-proto)
該原型的:isnew方法須要一個圖對象做爲參數:
(defmeth tour-item-proto :isnew (graph)
    (call-next-method "Touring")
    (setf (slot-value 'graph) graph))
圖對象可使用:graph消息來獲取:
(defmeth tour-item-proto :graph () (slot-value graph))
若是tour功能是打開的,該菜單項的:update方法將在它前邊放一個標記:
(defmeth tour-item-proto :update ()
    (let ((graph (send self :graph)))
      (send self :mark (send graph :tour-on))))
:do-action方法轉換tour的動做:
(defmeth tour-item-proto :do-action ()
    (let* ((graph (send self :graph))
           (is-on (send self :tour-on)))
      (send graph :tour-on (not is-on))))
最後,咱們能夠爲tour mixin從新定義:menu-template方法,用來向菜單模板的尾部添加一個tour項,這裏的菜單模板由繼承來的方法產生:
(defmeth tour-mixin :menu-template ()
    (append (call-next-method)
            (list (send tour-item-proto :new self))))
    使用tour mixin,一個基於旋轉圖形的tour圖形原型能夠這樣定義:
(defproto spin-tour-proto () () (list tour-mixin spin-proto))
該原型有兩個父類,分別是tour mixin和spin-proto,mixin放在spin-proto前邊,因此在優先級列表裏,mixin的方法出如今從旋轉圖形裏獲取的方法的前邊。使用下邊的表達式,咱們能夠賦予新原型更加合適的窗體標題和菜單標題:
(send spin-tour-proto :title "Grand Tour")

(send spin-tour-proto :menu-title "Tour")
針對一個數據集產生tour圖形的構造函數能夠這樣來定義:
> (defun tour-plot (data &rest args &key point-labels)
    (let ((graph (apply #'send spin-tour-proto :new
                        (length data) args)))
      (if point-labels
          (send graph :add-points
                data :point-labels point-labels :draw nil)
          (send graph :add-points data :draw nil))
      (send graph :adjust-to-data :draw nil)
      graph))

圖10.5 糖尿病數據grand tour的四個視圖

經過向原型發送:new消息,該函數產生了一個新的圖形,並將數據添加到圖形裏,而後,使用:adjust-to-data消息將圖形縮放到數據之上。因爲tour mixin沒有:isnew方法,它將使用從spin-proto繼承來的:isnew方法。

    對於在多維空間裏進行聚類探測,Grand tours看起來是頗有用的。做爲一我的工事例,圖10.5展現了Reaven和Miller地區的一個數據集的一個grand tour的4個視圖,圖形裏使用的數據包括150個患者數據,每位患者都進行3次連續測量,兩次葡萄糖測量和一次胰島素耐量測量,第4個變量是一個分類變量,它分3個等級,用來指示患者被分類到正常、「化學性」糖尿病或是明顯的糖尿病。圖10.5裏前兩個視圖顯示:這3個連續變量裏點雲數據呈現爲迴旋鏢的形狀;剩下的兩個視圖代表:當將第4個分類變量引入到tour裏的時候,點雲分紅獨立的3個聚類。

    使用這種tour與圖形混合的模式,咱們還能夠構造一個直方圖tour原型,該定義增長了一個角度槽:

(defproto hist-tour-proto
    '(angle) () (list tour-mixin histogram-proto))
該槽的讀取函數這樣給出:
(defmeth hist-tour-proto :angle (&optional new)
    (if new (setf (slot-value 'angle) new))
    (slot-value 'angle))
能夠這樣設置其初始值:
(send hist-tour-proto :angle .1)
    該原型的默認縮放選項應該是可變縮放:
(send hist-tour-proto :scale-type 'variable)
而後咱們爲原型賦予新的窗體標題和新的菜單標題:
(send hist-tour-proto :title "Histogram Tour")

(send hist-tour-proto :menu-title "Tour")
咱們能夠定義一個構造函數:
(defun histogram-tour (data &rest args &key point-labels)
    (let ((graph (apply #'send hist-tour-proto :new
                        (length data) :draw nil args)))
      (if point-labels
          (send graph :add-points
                data :point-labels point-labels :draw nil)
          (send graph :add-points data :draw nil))
      (send graph :adjust-to-data :draw nil)
      graph))
    

圖10.6 6維單位立方里的100個點數據的均勻分佈的直方grand tour的四個視圖

    用一個直方圖tour來研究興趣點,這種方法是理論觀測,對於較大的m空間維度,m維空間裏的多數正態數據的投影都是「看起來是」正態的。下表的表達式夠早了一個100個點的直方圖tour,這100個點均勻地分佈在六維單位立方體裏:

(histogram-tour (uniform-rand (repeat 100 6)))
圖10.6展現了該tour的4個視圖。第1個視圖表示首座標的直方圖,看起來是比較合理的均勻分佈;其它視圖根據tour裏不一樣的點而定,與其說均勻不如說是正態。

    構造一個基於散點圖矩陣的tour圖形,能夠私用類似的方法,散點圖矩陣tour提供了當前旋轉操做中全部座標對兒的同步視圖。

練習 10.10

練習 10.11
略。
練習 10.12

練習 10.13

練習 10.14

10.5 平行座標圖

在3維或更高緯度裏顯示數據的另外一個方法就是使用平行座標圖。平行座標圖是由圖上等距放置的縱座標軸平行的圖形構造而成的,每一個座標軸上每一個點的值都是分開的,而後在爲每一個點創建鏈接符號。舉個例子,圖10.7展現了煙道損耗數據的平行座標圖,這4個座標軸以線的形式扭結在一塊兒,咱們沒有畫出他們。經過對點數據符號進行選擇操做或者刷操做,咱們能夠在圖形裏選取點數據,初始狀況下,這些點符號是位於第一個座標軸上的,咱們能夠經過使用菜單裏的一個對話框將這些點符號定位到其它任意座標軸上。該佈局是基於Andreas Buja和Paul Tukey兩人使用過的一個類似圖形的。

圖10.7 煙道損耗數據的平行座標圖

    平行座標圖的原型能夠經過使用graph-proto原型來開發,新的原型須要一個額外的槽來表示當前座標軸的索引,該當前座標軸是包含點符號的:

(defproto parallel-plot-proto '(v) () graph-proto)
能夠這樣爲平行座標圖安置新標題:
(send parallel-plot-proto :title "Parallel Plot")
    與直方圖類似,平行座標圖向數據中添加了一個額外的維度,該維度用來沿着水平軸定位點符號。:isnew方法向其維度參數裏增長了1,將當前座標軸槽設置爲0,而後將初始內容變量設置爲最後一個座標軸和第一個維度:
(defmeth parallel-plot-proto :isnew (m &rest args)
    (setf (slot-value 'v) 0)
    (apply #'call-next-method (+ 1 m) args)
    (send self :content-variables m 0))
對於最後一個維度,只要放置的值是合適的,該設置就能夠確保graph-proto方法在合適的位置繪製點符號,並處理標準鼠標動做。對於平行圖形,須要一些其它方法來保證安置合適的數據,並細心地處理數據線的繪製。

    爲了得到點數據符號在當前座標的位置,安置正確的點座標值是該方法的責任。以不帶參數的方式調用,該方法將返回當前座標軸的索引;以帶參數的方式調用,該參數是指定新座標軸的索引i,它會將最後一個變量的點座標的值設置爲i,而後將內容變量設置爲最後一個變量和變量i:

(defmeth parallel-plot-proto :current-axis
    (&optional (i nil set) &key (draw t))
    (when set
          (setf (slot-value 'v) i)
          (let* ((n (send self :num-points))
                 (m (- (send self :num-variables) 1))
                 (i (max 0 (min i (- m 1)))))
            (if (< 0 n)
                (send self :point-coordinate m (iseq n) i))
            (send self :content-variables m i))
          (if draw (send self :redraw)))
    (slot-value 'v))
關鍵字參數:draw可用來避免方法重畫圖形。一個用來切換當前座標軸的對話框能夠這樣表示:
(defmeth parallel-plot-proto :choose-current-axis ()
    (let* ((choices
            (mapcar #'(lambda (x) (format nil "~d" x))
                    (iseq (- (send self :num-variables) 1))))
           (v (choose-item-dialog
               "Current Axis:"
               choices
               :initial (send self :current-axis))))
      (if v (send self :current-axis v))))
爲了顯示該對話框,能夠經過修改:menu-template方法,向標準菜單裏增長一個菜單項:
(defmeth parallel-plot-proto :menu-template ()
    (flet ((action () (send self :choose-current-axis)))
      (let ((item (send menu-item-proto :new
                        "Current Variable"
                        :action #'action)))
        (append (call-next-method) (list item)))))
    :adjust-to-data方法使用了繼承來的方法,而後調整最後那個變量的範圍,使第一個和最後一個座標軸的邊側留下0.1個單元的空白。若是縮放類型爲nil,數據變量的方位能夠擴展10%:
(defmeth parallel-plot-proto :adjust-to-data (&key (draw t))
    (call-next-method :draw nil)
    (let ((m (- (send self :num-variables) 1)))
      (if (null (send self :scale-type))
          (flet ((expand-range (i)
                               (let* ((range (send self :range i))
                                      (mid (mean range))
                                      (half (- (second range) (first range)))
                                      (low (- mid (* .55 half)))
                                      (high (+ mid (* .55 half))))
                                 (send self :range i low high :draw nil))))
            (dotimes (i m) (expand-range i))))
      (send self :scale m 1 :draw nil)
      (send self :center m 0 :draw nil)
      (send self :range m -.1 (- m .9) :draw draw)))
    在調用繼承來的方法以前,:add-points方法須要向新的點數據增長一個額外的座標,只要集成來的方法不會繪製點數據,實際值就不會出現什麼問題,而且能夠私用:current-axis消息設置成合適的值:
(defmeth parallel-plot-proto :add-points (data &key (draw t))
    (let ((n (length (first data))))
      (call-next-metod (append data (list (repeat 0 n)))
                       :draw nil))
    (send self :current-axis
          (send self :current-axis) :draw draw))
:add-lines消息能夠這樣覆寫:
(defmeth parallel-plot-proto :add-lines (&rest args)
    (error "Lines are not meaningful for this plot"))
由於在平行座標圖裏,線數據不能合理地顯示。

    :redraw-content方法能夠經過幾種合理的方式來定義。最有效的方法就是利用畫布座標,它是由graph-proto原型轉換系統定義的。向下邊這樣定義:resize方法能夠確保全部數據變量的畫布範圍都被設置到這一範圍內:即從0到內容矩形的高度:

(defmeth parallel-plot-proto :resize ()
    (call-next-method)
    (let ((height (fourth (send self :content-rect)))
          (m (- (send self :num-variables) 1)))
      (send self :canvas-range (iseq m) 0 height)))
這個繼承來的、在新方法開始處調用的方法確保了以下事實:當前的x變量使其範圍在0到內容矩形的寬度範圍以內。

    做爲點的平行表示方法的直線能夠繪製成一個多邊形,這裏的x座標能夠計算爲對內容原點的偏移量,該原點基於當前座標軸設置裏使用的慣例;y座標是這樣的點:畫布座標到內容原點的偏移量。在彩色顯示器上,這些線以點的顏色着色。那麼繪製一個或更多這類線的方法能夠這樣定義:

(defmeth parallel-plot-proto :draw-parallel-point (i)
    (let* ((points (if (numberp i) (list i) i))
           (width (third (send self :content-rect)))
           (origin (send self :content-origin))
           (x-origin (first origin))
           (y-origin (second origin))
           (m (- (send self :num-variables) 1))
           (gap (/ width (+ (- m 1) .2)))
           (xvals (+ x-origin
                     (round (* gap (+ .1 (iseq 0 (- m 1)))))))
           (indices (iseq 0 (- m 1)))
           (oldcolor (send self :draw-color)))
      (dolist (i points)
              (if (sned self :point-showing i)
                  (let* ((color (send self :point-color i))
                         (yvals (- y-origin
                                   (send self
                                         :point-canvas-coordinate
                                         indices
                                         i)))
                         (poly (transpose (list xvals yvals))))
                    (if color (send self :draw-color color))
                    (send self :frame-poly poly)
                    (if color (send self :draw-color oldcolor)))))))
該方法的參數能夠是一個單獨的索引,也能夠是一個索引列表。如今,:redraw-content方法能夠定義成這樣:
(defmeth parallel-plot-proto :redraw-content ()
    (let ((indices (iseq (send self :num-points))))
      (send self :start-buffering)
      (call-next-method)
      (send self :draw-parallel-point indices)
      (send self :buffer-to-screen)))
    最後,咱們定義一個構造函數:
(defun parallel-plot (data &rest args &key point-labels)
    (let ((graph (apply #'send parallel-plot-proto :new
                        (length data) :draw nil args)))
      (if points-labels
          (send graph :add-points
                data :point-labels point-labels :draw nil)
          (send graph :add-points data :draw nil))
      (send graph :adjust-to-data :draw nil)
      graph))
下表的表達式將建立圖10.7展現的圖形:
(parallel-plot (list air temp conc loss))
    由於平行圖形的原型是創建在graph-proto原型之上的,因此它已經得到了所有的變換系統,所以,它使用旋轉操做去定義一個grand tour的平行圖形版本是可能的。

練習10.15
略。

練習10.16
略。

練習10.17
略。

10.6 一個可代替的鏈接策略

Lisp-Stat裏使用的默認的鏈接策略是使用一個共同的索引在不一樣的圖形之間創建一個鬆散的對應關係。點的狀態是經過鏈接圖進行匹配的,可是其餘特徵,好比符號或者顏色則不能。一個可替代的方法將觀測值視爲對象,這裏的對象能夠視爲使用不一樣的圖形,此方法McDonald和Stuetzle使用過。點狀態、顏色或者符號這些屬性都是觀測值的屬性,這些屬性裏的任何一個改變,或者說某個觀測值裏的變量的值發生了改變,應該在全部的觀測圖形裏都有反應。在這個方法裏,觀測值放入圖形裏的順序是不重要的,由於觀測值做爲對象有他們本身的標識,不須要經過索引來識別。這意味着不一樣的圖形能夠展現不一樣的觀測集。

    本節將在Lisp-Stat繪圖系統裏描述一個這種鏈接策略的簡單的實現,該實現基於第6.8.2節描述的數據展現。基本的思想就是將觀測值表示爲對象,並容許使用圖形改變觀測對象的特徵,而後確保將觀測值對象的更改傳遞到全部包含這個觀測值的圖形上去。

    基本的觀測值原型定義以下:

(defproto observation-proto '(label state symbol color views))
6.8.2節裏定義的這些參數,它們的槽用來存放繪製觀測值所須要的特徵,這些槽的讀取方法這樣給出:
(defmeth observation-proto :label () (slot-value 'label))

(defmeth observation-proto :state () (slot-value 'state))

(defmeth observation-proto :symbol () (slot-value 'symbol))

(defmeth observation-proto :color () (slot-value 'color))
默認的狀態和符號值能夠這樣安置:
(send observation-proto :slot-value 'state 'normal)

(send observation-proto :slot-value 'symbol 'disk)
對於特殊的數據集的觀測量,能夠經過增長槽的方式來構造,目的是將變化的值保存到對象上,這裏的對象是從該觀測量原型繼承來的。

    這裏的觀測量裏的views槽是與特定的圖形裏的觀測量的索引一塊兒,用來來記錄哪一個槽包含觀測量的。經過向觀測量發送:add-view消息,咱們能夠向這個列表裏添加一個新的入口,要添加的消息有兩個參數:圖形對象和觀測量的索引:

(defmeth observation-proto :add-view (graph key)
    (setf (slot-value 'views)
          (cons (list graph key) (slot-value 'views))))
下邊的方法從views列表裏爲特定的圖形刪除該入口。爲了提升效率,使用了析構函數delete:
(defmeth observation-proto :delete-view (graph)
    (flet ((test (x y) (eq x (first y))))
      (let ((views (slot-value 'views)))
        (if (member graph views :test #'test)
            (setf (slot-value 'views)
                  (delete graph views :test #'test))))))
返回當前views列表的讀取方法這樣定義:
(defmeth observation-proto :views () (slot-value 'views))
爲了支持新的鏈接系統,觀測量對象裏的槽應該僅僅經過這樣的方法來改變,即經過向觀測量發送:change消息來改變,其參數爲槽的符號和新的槽值。下邊的方法向每一個視圖(包括該對象)發送:changed消息,其參數爲觀測量的索引、槽符號和槽的新值。
(defmeth observation-proto :change (slot value)
    (setf (slot-value slot) value)
    (dolist (view (send self :views))
            (send (first view) :changed (second view) slot value)))
    :changed消息是觀測量與須要支持新鏈接策略的圖形之間的通訊的協議的一部分,該消息對應的方法和其它消息一塊兒能夠組合到一個mixin裏:
(defproto observation-plot-mixin '(observations variables))

這裏的mixin有兩個槽,observations槽表示圖形裏的觀測量對象的矢量。就像6.8.2節同樣,圖形裏表示的變量經過消息選擇器關鍵字來展現,variables槽保存了這些關鍵字的列表。這兩個槽的讀取函數這樣定義:

(defmeth observation-plot-mixin :observations ()
    (slot-value 'observations))

(defmeth observation-plot-mixin :variables ()
    (slot-value 'variables))
:isnew方法須要一個變量列表,而不是一個維度的數值:
(defmeth observation-plot-mixin :isnew (vars &rest args)
    (apply #'call-next-method
           (length vars)
           :variable-labels (mapcar #'string vars)
           args)
    (setf (slot-value 'variables) vars))
由於這個mixin模式將與其它圖形原型一同使用,繼承來的:isnew方法將須要一個指定圖形維度的整型參數。

    經過使用:add-observation消息,觀測量能夠加入到圖形中,這裏的:add-observation消息須要觀測量列表做爲參數,並接收:draw關鍵字,目的是指定圖形是否應該重畫:

(defmeth observation-plot-mixin :add-observations
    (new-obs &key (draw t))
    (let* ((obs (send self :observations))
           (n (length obs))
           (m (length new-obs))
           (new-obs (coerce new-obs 'vector)))
      (setf (slot-value 'observations)
            (concatenate 'vector obs new-obs))
      (dotimes (i m)
               (send (aref new-obs i) :add-view self (+ i n)))
      (send self :needs-adjusting t)
      (if draw (send self :adjust-screen))))
    當圖形接收到:remove消息時,好比關閉窗體,它會移除自身,就像從觀測量裏移除一個視圖同樣:
(defmeth observation-plot-mixin :remove ()
    (call-next-method)
    (let ((obs (send self :observations)))
      (dotimes (i (length obs))
               (send (aref obs i) :delete-view self))))
adjust-screen方法檢測圖形是否須要調整,若是須要調整,清除當前點數據,新的點數據經過引用圖中觀測量的方式安置進來:
(defmeth observation-plot-mixin :adjust-screen ()
    (if (send self :needs-adjusting)
        (let ((vars (send self :variables))
              (obs (send self :observations)))
          (send self :clear-points :draw nil)
          (when (< 0 (length obs))
                (flet ((variable (v)
                                 (map-elements #'(lambda (x) (send x v))
                                               obs)))
                  (send self :add-points
                        (mapcar #'variable vars) :draw nil))
                (dotimes (i (length obs))
                         (let ((x (aref obs i)))
                           (send self :point-label i (send x :label))
                           (send self :point-label i (send x :state))
                           (send self :point-label i (send x :color))
                           (send self :point-symbol i (send x :symbol)))))
          (send self :needs-adjusting nil)
          (send self :redraw-content))))
:changed消息對應的方法這樣定義:
(defmeth observation-plot-mixin :changed (key what value)
    (case what
      (state (send self :point-state key value))
      (t (send self :needs-adjusting t))))
由於大多數圖形均可以使它們的點的狀態快速地調整,因此咱們使用:point-state消息來響應狀態改變。其它的改變經過在圖上作標記來調整。

    一些被標準圖形菜單使用的方法須要從新定義,以改變觀測量對象而不是改變圖形,而後確保全部將該對象做爲視圖的圖形都獲得調整。下邊的函數能夠用來調整全部圖形:

(defun synchronize-graphs ()
    (dolist (g (active-windows))
            (if (kind-of-p g observation-plot-mixin)
                (send g :adjust-screen))))
菜單使用的三個方法定義以下,:erase-selection方法用來使選中的點數據不可見:
(defmeth observation-plot-mixin :erase-selection ()
    (let ((obs (send self :observations)))
      (dolist (i (send self :selection))
              (send (aref obs i) :change 'state 'invisible)))
    (synchronize-graphs))
:show-all-points方法將任意不可見觀測量設置爲正常狀態:
(defmeth observation-plot-mixin :show-all-points ()
    (let ((obs (send self :observations)))
      (dotimes (i (length obs))
               (send (aref obs i) :change 'state 'normal)))
    (synchronize-graphs))
:focus-on-selection方法用來使全部未選中的點數據不可見:
(defmeth observation-plot-mixin :focus-on-selection ()
    (let* ((obs (send self :observations))
           (showing (send self :points-showing))
           (selection (send self :selection)))
      (dolist (i (set-difference showing selection))
              (send (aref obs i) :change 'state 'invisible)))
    (synchronize-graphs))
    因爲原來的鏈接系統再也不使用,在mixin裏定義:menu-template方法,處理掉標準菜單裏的鏈接項就是個好想法:
(defmeth observation-plot-mixin :menu-template ()
    (remove 'link (call-next-method)))
    標準鼠標方法使用的消息也須要修改,:unselect-all-points方法變成這樣:
(defmeth observation-plot-mixin :unselect-all-points ()
    (let ((obs (send self :observations)))
      (dolist (i (send self :selection))
              (send (aref obs i) :change 'state 'normal))
      (send self :adjust-screen)))
新的:adjust-points-in-rect方法能夠經過:points-in-rect消息來定義:
(defmeth observation-plot-mixin :adjust-points-in-rect (left top width height state)
    (let ((points (send self :points-in-rect
                        left top width height))
          (selection (send self :selection))
          (obs (send self :observations)))
      (case state
        (selected
         (dolist (i (set-difference points selection))
                 (send (aref obs i) :change 'state 'selected)))
        (hilited
         (let* ((points (set-difference points selection))
                (hilited (send self :points-hilited))
                (new (set-difference points hilited))
                (old (set-difference hilited points)))
           (dolist (i new)
                   (send (aref obs i) :change 'state 'hilited))
           (dolist (i old)
                   (send (aref obs i) change 'state 'normal))))))
    (synchronize-graphs))
這兩個方法都影響了觀測量裏而不是圖形的狀態,而且經過通訊協議從觀測量向它們的視圖回傳變化。

    使用觀測量圖形混合模式,咱們能夠設置一個觀測量散點圖:

(defproto obs-scatterplot-proto ()
    ()
    (list observation-plot-mixin
          scatterplot-proto))
它的簡單的構造函數這樣給出:
(defun plot-observations (obs vars)
    (let ((graph (send obs-scatterplot-proto :new vars)))
      (send graph :new-menu)
      (send graph :add-observations obs)
      (send graph :adjust-to-data)
      graph))
圖形的其它類型能夠以一樣的方式構造。

    爲了表示新的鏈接系統,咱們能夠再次回到煙道損失數據上,就像在6.8.2節說的,針對該數據的觀測量原型能夠這樣給出:

(defproto stack-obs '(air temp conc loss) () observation-proto)
這四個變量槽的讀取方法能夠這樣定義:
(defmeth stack-obs :air (slot-value 'air))
(defmeth stack-obs :temp (slot-value 'temp))
(defmeth stack-obs :conc (slot-value 'conc))
(defmeth stack-obs :loss (slot-value 'loss))
派生變量,好比對數損耗變量,能夠定義爲:
(defmeth stack-obs :log-loss () (log (send self :loss)))
下邊的表達式設置了一個觀測量對象的列表:
(flet ((make-obs (air temp conc loss index)
                   (let ((label (format nil "~d" index)))
                     (send stack-obs :new
                           :air air
                           :temp temp
                           :conc conc
                           :loss loss
                           :label label))))
    (setf stack-list (mapcar #'make-obs
                             air temp conc loss (iseq 0 20))))
下邊的兩個表達式將產生觀測量的兩個圖形,一個圖形顯示全部的觀測量,另外一個圖形只顯示其中的一個子集:
(plot-observations stack-list '(:air :log-loss))

(plot-observations (select stack-list (iseq 10 20))
                     '(:temp :conc))
由於這些數據點由他們的觀測量對象標識而不是由它們的索引標識,全部這個不會引發問題。

練習 10.18
略。

《Lisp-Stat:一種統計計算和動態製圖的面向對象環境》系列正文部分翻譯完畢。

相關文章
相關標籤/搜索