[ << ] [ >> ]           [Top] [Contents] [Index] [ ? ]

C. ラベルと軸が付いたグラフ

座標軸を描くと、グラフを理解する手助けになる。目盛もつけたい。以前の章で は (グラフを描く準備, 参照) グラフの本体を描 くコードを書いたのであった。ここではその本体にそって縦軸と横軸を表示し、 ラベルをつけるコードを書くことにしよう。

ラベルのついたグラフのサンプル  
C.1 print-graph の変数リスト  print-graph の中の let
C.2 関数 print-Y-axis  縦軸のラベルの表示
C.3 関数 print-X-axis  横軸のラベルの表示
C.4 グラフ全体の表示  

ラベルのついたグラフのサンプル

ここでの挿入は、バッファのポイントの下方を右方向に向って埋めていくので、 新しいグラフ表示関数では、まず Y 軸ないしは縦軸を表示し、ついで本体を、 そして最後に X 軸ないしは横軸を表示するという順番になる。この流れから、 関数の中身のレイアウトは次のような感じになる。

  1. コードの下準備。

  2. Y 軸の表示。

  3. グラフ本体の表示。

  4. X 軸の表示。

以下に出来上がったグラフの例がどうあるべきかを載せておく。

 
    10 -          
                  *
                  *  *
                  *  **
                  *  ***
     5 -      *   *******
            * *** *******
            *************
          ***************
     1 - ****************
         |   |    |    |
         1   5   10   15

このグラフでは、縦軸と横軸には数によるラベルがついている。しかしながら、 グラフの種類によっては横軸が時間で、月によるラベルをつけた方が良い場合も 多いだろう。次のような感じだ。

 
     5 -      * 
            * ** *
            *******
          ********** **
     1 - **************
         |    ^      |
         Jan  June   Jan

実際には、ちょっと考えれば横軸や縦軸について様々な形式を思いつく。複雑な 関数にすることも可能だ。しかし複雑さは混乱を招く。それよりも、最初は単純 な形式を選んで、後でそれを修正ないしは置き換えるのが良いだろう。

以上のことを考慮すると、次のようなアウトラインで print-graph 関数 を書いていくのがいいだろう。

 
(defun print-graph (numbers-list)
  "説明文字列..."
  (let ((height  ...
        ...))
    (print-Y-axis height ... )
    (graph-body-print numbers-list)
    (print-X-axis ... )))

この print-graph 関数の雛型を元にして、各々の部分を順に書いていく ことにしよう。


C.1 print-graph の変数リスト

print-graph 関数を書く際に、まずやらなければならないことは、 let 式の変数リストを書くことである。(取り敢えず、インタラクティブ 式や説明文字列のことは置いておく。)

変数リストでは幾つかの値を設定しなければならない。縦軸のラベルの一番上は 少なくともグラフの高さでなくてはいけないので、まずこれについての情報が必 要になる。print-graph-body 関数でもこの情報が必要だったことを思い 出そう。同じ計算を別の場所で二度行う理由はどこにもないので、以前定義した print-graph-body を書き換えてこの計算を一度だけにするべきである。

同様に、X 軸のラベルを表示する関数と print-graph-body 関数のどち らにおいても記号の幅の値を知る必要がある。これも以前の章の print-graph-body の定義を書き換えて、最初に計算をすませてしまうべ きであろう。

横軸のラベルの長さは少なくともグラフの長さの分はなければならない。しかし、 この情報は横軸を表示する関数だけに必要なものである。従って、ここでやらな くとも良い。

以上のことから、print-graphlet 式の変数リストは次のよ うな感じになる。

 
(let ((height (apply 'max numbers-list)) ; 最初のバージョン
      (symbol-width (length graph-blank)))

が、後で見るように、これではちょっとまずい。


C.2 関数 print-Y-axis

print-Y-axis 関数の仕事は、縦軸として次のようなラベルを表示するこ とである。

 
    10 -
        
        
        
        
     5 -
        
        
        
     1 -

この関数には、グラフの高さを渡してやる必要がある。それを元に適切な目盛と 数を構成し、挿入するわけである。

図で見ると、Y 軸のラベルがどうなるべきかは一目瞭然である。しかし、これを 言葉で表しその仕事をする関数を書くとなると、少々ややこしくなる。単に5行 ごとに数や目盛をつければよいというのは間違いである。`1'`5' の間には三行しかない (2、3、4行)。しかし、`5'`10' の間には 四行ある (6、7、8、9行)。まずは一行目にベースとなる行として数1と目盛をつ け、その他に最下行から5行ごとに目盛と数をつけていく方がよいだろう。

次の問題は、ラベルの高さをどうするかである。例えば、グラフの縦の列の中で 最も高いものの高さが7だったとしよう。この場合、最も上のラベルは `5 -' にして、グラフを上に突き出させるべきだろうか。それとも7以上の 最大の5の倍数として `10 -' の所までラベルを付けるべきだろうか。

これは後者の方がよいだろう。大抵のグラフは長方形のなかに描かれており、側 面は刻み幅の整数倍になっている。今の場合なら、5、10、15というふうな5の倍 数である。しかし、縦軸の仕様としてこのようなものを採用するとなると、以前 の関数の変数リストでの、単純に高さを計算するS式ではまずいことに気付く。 これは (apply 'max numbers-list) というものだったが、これでは単に 正確なグラフの縦の列の高さの最大値が出るだけで、5の倍数になるような調整 はしてはくれない。もっと複雑な関数が必要なわけである。

いつものことだが、複雑な問題も幾つかの小さな問題に分割して考えれば単純 な問題になることが多い。

最初に、いつグラフの高さが5の整数倍になるかを考えてみよう。これは5、10、 15等の5の倍数の時である。この場合は、この値を即、Y 軸の高さとしてよい。

ある数が5の倍数になるかどうかを見るには、この数を5で割ってみて、余りがど うなるかを見るのが早い。もし余りが無ければ、その数は5の倍数である。7の場 合は余りが2になるので、これは5の倍数ではない。ちょっと言い方を変えて小学 校ふうに説明するなら、5は7の中に一回だけ含まれて、残りは2になる。一方、5 は10の中には2回含まれ、残りはない。従って、10は5の倍数ということになる。

C.2.1 寄り道: 剰余の計算  
C.2.2 Y 軸の要素の構成  
C.2.3 Y 軸全体の構成  
C.2.4 print-Y-axis 最終版  縦軸の表示、最終版


C.2.1 寄り道: 剰余の計算

Lisp では余りを計算する関数は % である。この関数は最初の引数を二 番目の引数で割った時の余りを返す。あいにく、% という Emacs Lisp 関数は apropos では見つけられない。つまり、M-x apropos RET remainder RET とすると、何も見つけられない。% と いう関数の存在を知るためにはこの文書などの本を見るか、Emacs Lisp のソー スを眺めるしか方法がない。% 関数は rotate-yank-pointer の 中で使われており、これは次の所で説明されている。 (rotate-yank-pointer の本体, を参照。)

% 関数を試してみるには、次の二つのS式を評価してみると良い。

 
(% 7 5)

(% 10 5)

最初のS式は2を返すし、二番目のS式は0を返す。

返された値が0かそうでないかのテストには zerop 関数が使える。この 関数は引数として数値を取り、それが零なら t を返す。

 
(zerop (% 7 5))
     => nil

(zerop (% 10 5))
     => t

というわけで、次のようなS式を書けば、高さが5で割切れる場合には t が返される。

 
(zerop (% height 5))

(height の値は勿論 (apply 'max numbers-list). で分る。)

一方、もし height が5の倍数でない場合、値が5の倍数になるように再 設定する必要がある。これは既にお馴染みの関数を使えば単純な算数にすぎない。 まずは height を5で割ってこの中に何回5が含まれるかを調べる。例え ば12なら2回含まれる。この値に1加えて5倍すれば、棒グラフの最大の高さより も大きい最初の5の倍数が得られる。5は12の中には2回含まれるので、これに1を 足して5倍すると15が求まる。これが12を越える最初の5の倍数であ。この作業を 行うS式は次の通りである。

 
(* (1+ (/ height 5)) 5)

例えばもし次のS式を評価したなら、結果は15になる。

 
(* (1+ (/ 12 5)) 5)

これまでの議論では「5」を Y 軸についての目盛幅としてきたわけだが、ここは 他の値を使っても構わない。一般性のために、5を他の値も設定出来るように変 数で置き換えよう。私としては、この変数は Y-axis-label-spacing と 名付けるのが良いと思う。この変数と if 式を使うと、次のS式が出来 る。

 
(if (zerop (% height Y-axis-label-spacing))
    height
  ;; else
  (* (1+ (/ height Y-axis-label-spacing))
     Y-axis-label-spacing))

このS式は、もし heightY-axis-label-spacing の値の倍数 なら height の値そのものを返し、そうでない場合は、それより大きい 最小の Y-axis-label-spacing の倍数の値を計算して返す。

それでは、このS式を print-graph 関数の中の let 式の中に (Y-axis-label-spacing の設定の後で) 埋め込んでみよう。

 
(defvar Y-axis-label-spacing 5
  "Number of lines from one Y axis label to next.")

...
(let* ((height (apply 'max numbers-list))
       (height-of-top-line
        (if (zerop (% height Y-axis-label-spacing))
            height
          ;; else
          (* (1+ (/ height Y-axis-label-spacing))
             Y-axis-label-spacing)))
       (symbol-width (length graph-blank))))
...

(let* 関数を使っていることに注意。始めに変数 height の初期値が (apply 'max numbers-list) 式で計算され、その値を使って他の変数の 値を計算しているためである。let* についての詳細は、次を参照。 let*.)


C.2.2 Y 軸の要素の構成

縦軸を表示する際には、`5 -' とか `10 -' 等を5行ごと に挿入したい。更に、数は下の部分を揃えたい。つまり、小さな桁の数は、前に 空白を置くことになる。もし二桁の数が出てきた時には、一桁の数の先頭には一 つの空白を置くことになるわけである。

数の長さを求めるには、length 関数が使われる。しかし、 length 関数は文字列に対してしかうまく動作せず、数値は扱えない。そ こでまず、数を数値から文字列に変換する必要がある。これは、 int-to-string 関数を使って行うことが出来る。例えば、次のような感 じである。

 
(length (int-to-string 35))
     => 2

(length (int-to-string 100))
     => 3

更に、各ラベルにおいて、各々の数には ` - ' のような文字列を付け 加えたい。これを、Y-axis-tic マーカと呼ぶことにしよう。この変数は、 defvar を使って定義する。

 
(defvar Y-axis-tic " - "
   "String that follows number in a Y axis label.")

Y ラベルの長さは、Y axis tick mark とグラフの一番上の数の桁数の和になる。

 
(length (concat (int-to-string height) Y-axis-tic)))

この値は、print-graph 関数の中の変数リストの所で、 full-Y-label-width の値として計算される。(我々が最初にこの関数を 考えた時には、この関数は変数リストの中に入れることは考えていなかったこと に注意しよう。)

縦軸を完成させるには、目盛記号を数と結合する必要がある。そして、その前に は数の桁数によって空白が付いたりする。というわけで、ラベルには三つのパー トがあることになる。先頭の空白 (無い場合もある)、数、そして目盛記号であ る。この関数には、指定された行に対する数の値と一番上の行の幅が渡される。 これらの値は print-graph によって (一回だけ) 計算される。

 
(defun Y-axis-element (number full-Y-label-width) 
  "Construct a NUMBERed label element.
A numbered element looks like this `  5 - ',
and is padded as needed so all line up with
the element for the largest number."
  (let* ((leading-spaces 
         (- full-Y-label-width
            (length
             (concat (int-to-string number)
                     Y-axis-tic)))))
    (concat
     (make-string leading-spaces ? )
     (int-to-string number)
     Y-axis-tic)))

Y-axis-element 関数は、(もしあれば) 先頭の空白と、文字列としての 数と、目盛記号を結合するものである。

先頭にいくつ空白をつければよいかは、ラベルの実質部分の長さ---数の長さと 目盛記号の長さの和---を望まれるラベルの長さから引くことで求められる。

空白は make-string 関数を使って挿入される。この関数は、二つの引数 を取る。一つは文字列の長さ、もう一つは特定の形式で書かれた挿入する文字の シンボルである。今の場合、この形式は `? ' のように、疑問符に続く空 白という形をしている。このような文字の表し方についての説明は、次を参照。 section `Character Type' in The GNU Emacs Lisp Reference Manual.

int-to-string 関数は、結合式の中で数を文字列に変換するために使わ れている。文字列に変換してから先頭の空白と目盛記号と結合するのである。


C.2.3 Y 軸全体の構成

前節までの関数で、縦軸のラベルとして挿入する番号と空白のついた文字列の リストを生成する関数を構成するのに必要なツールが全て揃う。

 
(defun Y-axis-column (height width-of-label)
  "Construct list of Y axis labels and blank strings.
For HEIGHT of line above base and WIDTH-OF-LABEL."
  (let (Y-axis)
    (while (> height 1)
      (if (zerop (% height Y-axis-label-spacing))
          ;; ラベル挿入
          (setq Y-axis
                (cons
                 (Y-axis-element height width-of-label)
                 Y-axis))
        ;; そうでなければ空白挿入
        (setq Y-axis
              (cons
               (make-string width-of-label ? )
               Y-axis)))
      (setq height (1- height)))
    ;; ベースライン挿入
    (setq Y-axis
          (cons (Y-axis-element 1 width-of-label) Y-axis))
    (nreverse Y-axis)))

この関数では、まず height の値から出発してその値を一つずつ減らし ていき、各々の引き算が終わった所でその値が Y-axis-label-spacing の整数倍になっているかどうかを判定する。そして、もしそうなっていれば、 Y-axis-element 関数を使って番号付きのラベルを作成し、そうでなけれ ば make-string 関数を使って空白のラベルを作成する。最下行は、番号 1と目盛記号からなっている。


C.2.4 print-Y-axis 最終版

(訳註:題名に最終版と書いてあるが、実際には グラフ全体の表示, に出ているものが真の最終版である。原文ではここに最終版 が載っていたのだが、そうすると、中で使っている y-axis-column 関数 が最終版でないために、その次に述べられているテストでエラーが出てしまう。)

Y-axis-column 関数によって構成されたリストは print-Y-axis 関数に渡される。これが実際にそのリストを挿入する。

 
(defun print-Y-axis
  (height full-Y-label-width)
  "Insert Y axis using HEIGHT and FULL-Y-LABEL-WIDTH.
Height must be the  maximum height of the graph.
Full width is the width of the highest label element."
;; Value of height and full-Y-label-width 
;; are passed by `print-graph'.
  (let ((start (point)))
    (insert-rectangle
     (Y-axis-column height full-Y-label-width))
    ;; グラフ挿入の準備としてポイントを移動
    (goto-char start)      
    ;; full-Y-label-width の値の分だけポイントを前方に移動 
    (forward-char full-Y-label-width)))

print-Y-axisY-axis-column によって作成された Y 軸のラ ベルを挿入するのに insert-rectangle を使っている。更に、グラフの 本体部分を挿入するための適切な位置にポイントを移動している。

以下のようにして print-Y-axis をテストしてみることが出来る。

  1. まず次の変数や関数をインストールする。

     
    Y-axis-label-spacing
    Y-axis-tic
    Y-axis-element
    Y-axis-column
    print-Y-axis
    

  2. 次のS式をコピーする。

     
    (print-Y-axis 12 5)
    

  3. `*scratch*' バッファに移り、カーソルを縦軸のラベルを挿入したい位置 まで移動する。

  4. M-: (eval-expression) とタイプする。(訳註:Emacs 19.28 ベー スの Mule では M-ESC.)

  5. print-Y-axis 式をミニバッファに C-y (yank) を使っ てヤンクする。

  6. このS式を評価するために RET を押す。

Emacs はラベルを縦に表示する。一番上は `10 - ' である。 (print-graph 関数は、height-of-top-line の値を渡すのだが、 今の場合これは15である。) (訳註:ここは原文も混乱しているみたいである。 print-Y-axisprint-graph の中で使われている。その中では print-Y-axis に第一引数として渡されるのは height-of-top-line の値なのであるが、print-graph 関数の中 の局所変数としての height の値が12で Y-axis-label-spacing の 値が5だったとすると、この height-of-top-line の値が15になるという ことを言いたかったのであろう。詳しくは最終版 print-graph を参照の こと。)


C.3 関数 print-X-axis

X 軸のラベルは Y 軸のラベルと大変似ている。違うのは、目盛記号が数の上に のっていることである。ラベルは次のようになる。

 
    |   |    |    |
    1   5   10   15

最初の目盛はグラフの最初の桁の下の部分にきており、その前には幾つかの空 白がある。これらの空白は、Y 軸のラベルが上に来る部分である。二番目、三番 目、四番目、あるいはそれに続く目盛は全て等間隔で並んでいる。この間隔は X-axis-label-spacing の値で決まる。

X 軸の二行目は、空白に続く数からなる。この数字の間隔もまた X-axis-label-spacing の値による。

変数 X-axis-label-spacing の値そのものは symbol-width とい う単位を元に決められるべきである。棒グラフを表示するためのシンボルの幅を 変更する時に、ラベルの付け方まで変えなければならないなんて事態は避けたい だろう。

print-X-axis 関数は多かれ少なかれ print-Y-axis 関数と同じ 形をしている。違うのは、目盛の行と数字の行の二行あるということである。こ の二つは各々別の関数で書いて、print-X-axis の中で一緒にすることに する。

以下が、これから行う三段階のステップである。

  1. X 軸の目盛記号を表示する関数 print-X-axis-tic-line を書く。

  2. X 軸の数字を表示する関数 print-X-axis-numbered-line を書く。

  3. この print-X-axis-tic-lineprint-X-axis-numbered-line を使って、上の二つの行を両方とも表示す る関数 print-X-axis を書く。

C.3.1 X 軸の目盛記号  横軸に目盛記号を入れる


C.3.1 X 軸の目盛記号

最初の関数では、X 軸の目盛記号を表示する。まずは、目盛記号とその間の記号 を指定しないといけない。

 
(defvar X-axis-label-spacing 
  (if (boundp 'graph-blank)
      (* 5 (length graph-blank)) 5)
  "Number of units from one X axis label to next.")

(graph-blank の値はまた別の defvar で設定されることに注意 しよう。boundp という述語は、既に何かの値がセットされているかどう かを判定するものである。もし何もセットされていなければ、boundpnil を返す。もし graph-blank が設定されず、また、この条件 分岐式がなかったとすると、Symbol's value as variable is void とい うエラーメッセージを受けとることになる。

 
(defvar X-axis-tic-symbol "|"
  "String to insert to point to a column in X axis.")

目標は、次のような行を作成することである。

 
       |   |    |    |

最初の目盛は、Y 軸のラベルのためのスペースを空けるためにインデントされた 最初の棒グラフの開始の桁と同じだけインデントしないといけない。

目盛の要素は、ある目盛から次の目盛までのスペースと目盛記号からなる。空白 の数は目盛記号の幅と X-axis-label-spacing から決まる。

コードは次の通りである。

 
;;; X-axis-tic-element
...
(concat  
 (make-string 
  ;; 空白の文字列を作成
  (-  (* symbol-width X-axis-label-spacing)
      (length X-axis-tic-symbol))
  ? )
 ;; 空白を目盛記号と結合
 X-axis-tic-symbol)
...

次に、最初の目盛をグラフの開始位置までインデントするために、どれだけ空白 が要るかを数える必要がある。これには、print-graph 関数によって渡 された full-Y-label-width の値を利用する。

X-axis-leading-spaces を作るためのコードは次の通りである。

 
;; X-axis-leading-spaces
...
(make-string full-Y-label-width ? )
...

また、横軸の長さも決めなければならない。これは、数のリストの長である。 更に、横軸の目盛の数も決める必要がある。

 
;; X-length 
...
(length numbers-list)              

;; tic-width
...
(* symbol-width X-axis-label-spacing)

;; number-of-X-tics 
(if (zerop (% X-length tic-width))
    (/ X-length tic-width)
  (1+ (/ X-length tic-width)))

これらを使うと、X 軸の目盛の行を表示する関数は次のようになる。

 
(defun print-X-axis-tic-line
  (number-of-X-tics X-axis-leading-spaces X-axis-tic-element)
  "Print tics for X axis." 
    (insert X-axis-leading-spaces)
    (insert X-axis-tic-symbol)  ; 最初の桁の下に
    ;; 右の位置に二番目の目盛記号を挿入
    (insert (concat  
             (make-string 
              (-  (* symbol-width X-axis-label-spacing)
                  ;; 二番目の目盛記号まで空白を挿入
                  (* 2 (length X-axis-tic-symbol)))
              ? )
             X-axis-tic-symbol))
    ;; 残りの目盛記号を挿入
    (while (> number-of-X-tics 1)
      (insert X-axis-tic-element)
      (setq number-of-X-tics (1- number-of-X-tics))))

数字の行も同様にして簡単に書ける。

まずは前に空白の付いた番号を作成する。

 
(defun X-axis-element (number)
  "Construct a numbered X axis element."
  (let ((leading-spaces 
         (-  (* symbol-width X-axis-label-spacing)
             (length (int-to-string number)))))
    (concat (make-string leading-spaces ? )
            (int-to-string number))))

次に、数字のついた行を表示するための関数を作る。これはまず最初の桁に数 "1" を付けるところから始まる。

 
(defun print-X-axis-numbered-line
  (number-of-X-tics X-axis-leading-spaces)
  "Print line of X-axis numbers"
  (let ((number X-axis-label-spacing))
    (insert X-axis-leading-spaces)
    (insert "1")
    (insert (concat  
             (make-string 
              ;; 次の数字の所まで空白を挿入
              (-  (* symbol-width X-axis-label-spacing) 2)
              ? )
             (int-to-string number)))
    ;; 残りの数字を挿入
    (setq number (+ number X-axis-label-spacing))
    (while (> number-of-X-tics 1)
      (insert (X-axis-element number))
      (setq number (+ number X-axis-label-spacing))
      (setq number-of-X-tics (1- number-of-X-tics)))))

最後に、print-X-axis-tic-lineprint-X-axis-numbered-line を使って、print-X-axis を書く。

この関数では print-X-axis-tic-lineprint-X-axis-numbered-line の両方で使われている局所変数の値を決定 してから、これらの関数を呼び出さなければならない。また、二つの行の区切り に復帰コードを表示する必要もある。

この関数は、5つの局所変数を指定する変数リストと二つの行を表示する関数の 呼び出しからなる。

 
(defun print-X-axis (numbers-list)
  "Print X axis labels to length of NUMBERS-LIST."
  (let* ((leading-spaces 
          (make-string full-Y-label-width ? ))
       ;; symbol-width は graph-body-print で与えられる。
       (tic-width (* symbol-width X-axis-label-spacing))
       (X-length (length numbers-list))
       (X-tic
        (concat  
         (make-string 
          ;; 空白の文字列を作成
          (-  (* symbol-width X-axis-label-spacing)
              (length X-axis-tic-symbol))
          ? )
         ;; 空白を目盛記号と結合
         X-axis-tic-symbol))
       (tic-number
        (if (zerop (% X-length tic-width))
            (/ X-length tic-width)
          (1+ (/ X-length tic-width)))))
    (print-X-axis-tic-line tic-number leading-spaces X-tic)
    (insert "\n")
    (print-X-axis-numbered-line tic-number leading-spaces)))

これで、print-X-axis のテストが出来る。

  1. まずは X-axis-tic-symbol, X-axis-label-spacing, print-X-axis-tic-line, などの変数とともに X-axis-element, print-X-axis-numbered-line, print-X-axis という関数をイン ストールする。

  2. 次のS式をコピーする。

     
    (progn
     (let ((full-Y-label-width 5)
           (symbol-width 1))
       (print-X-axis
        '(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16))))
    

  3. `*scratch*' バッファに移り、カーソルを軸のラベルを表示したい位置に まで動かす。

  4. M-: (eval-expression) とタイプする。(訳註:Emacs 19.28 ベー スの Mule では M-ESC.)

  5. C-y (yank) を使って、テストのS式をヤンクする。

  6. その式を評価するために、RET を押す。

これで Emacs は次のような横軸を表示してくれるはずだ。

 
     |   |    |    |    |
     1   5   10   15   20


C.4 グラフ全体の表示

さて、これでグラフ全体を表示する準備が整った。

きちんとしたラベルが付いたグラフを表示する関数を書く際には、以前作成した アウトラインに従うわけだが (ラベルと軸の付いたグラフ, 参照)、幾つか追加することがある。

アウトラインは以下のようなものだった。

 
(defun print-graph (numbers-list)
  "説明文字列..."
  (let ((height  ...
        ...))
    (print-Y-axis height ... )
    (graph-body-print numbers-list)
    (print-X-axis ... )))

最終的なバージョンは、上の計画とは二つの点で違っている。まず、変数リスト の中で追加して計算するものがある。次に、ラベルの増加のさせ方を指定するオ プションがある。後者の特徴はかなり本質的である。そうしないことには、画面 や紙面に合わなくなってしまう可能性があるからだ。

この新しい特徴を実現するには、Y-axis-column 関数にいくらか変更を 加えて vertical-step という変数を加える必要がある。同時に、 print-Y-axis も書き換える。新しい関数は次のようになる。

 
;;; 最終バージョン。
(defun Y-axis-column
  (height width-of-label &optional vertical-step)
  "Construct list of labels for Y axis.
HEIGHT is maximum height of graph.  
WIDTH-OF-LABEL is maximum width of label.
VERTICAL-STEP, an option, is a positive integer 
that specifies how much a Y axis label increments 
for each line.  For example, a step of 5 means 
that each line is five units of the graph."
  (let (Y-axis
        (number-per-line (or vertical-step 1)))
    (while (> height 1)
      (if (zerop (% height Y-axis-label-spacing))
          ;; ラベルの挿入
          (setq Y-axis
                (cons
                 (Y-axis-element
                  (* height number-per-line)
                  width-of-label)
                 Y-axis))
        ;; そうでない場合は, 空白の挿入
        (setq Y-axis
              (cons
               (make-string width-of-label ? )
               Y-axis)))
      (setq height (1- height)))
    ;; ベースラインの挿入
    (setq Y-axis (cons (Y-axis-element 
                        (or vertical-step 1)
                        width-of-label)
                       Y-axis))
    (nreverse Y-axis)))

 
;;; 最終バージョン。
(defun print-Y-axis
  (height full-Y-label-width &optional vertical-step)
  "Insert Y axis using HEIGHT and FULL-Y-LABEL-WIDTH.
Height must be the  maximum height of the graph.
Full width is the width of the highest label element.
Optionally, print according to VERTICAL-STEP."
;; Value of height and full-Y-label-width 
;; are passed by `print-graph'.
  (let ((start (point)))
    (insert-rectangle
     (Y-axis-column height full-Y-label-width vertical-step))
    ;; Place point ready for inserting graph.
    (goto-char start)      
    ;; Move point forward by value of full-Y-label-width
    (forward-char full-Y-label-width)))

グラフの最大の高さの値とシンボルの幅は print-graph の中の let 式によって計算される。従って、graph-body-print はそれ らを受け取ることが出来るように変更しなければならない。

 
;;; 最終バージョン。
(defun graph-body-print (numbers-list height symbol-width)
  "Print a bar graph of the NUMBERS-LIST.
The numbers-list consists of the Y-axis values.
HEIGHT is maximum height of graph.
SYMBOL-WIDTH is number of each column."
  (let (from-position)
    (while numbers-list
      (setq from-position (point))
      (insert-rectangle
       (column-of-graph height (car numbers-list)))
      (goto-char from-position)
      (forward-char symbol-width)
      ;; 各桁ごとにグラフを描写
      (sit-for 0)               
      (setq numbers-list (cdr numbers-list)))
    ;; X 軸のラベルのためにポイントを移動
    (forward-line height)
    (insert "\n")))

最後に、print-graph 関数のコードを載せておこう。

 
;;; 最終バージョン。
(defun print-graph
  (numbers-list &optional vertical-step)
  "Print labelled bar graph of the NUMBERS-LIST.
The numbers-list consists of the Y-axis values.

Optionally, VERTICAL-STEP, a positive integer,
specifies how much a Y axis label increments for
each line.  For example, a step of 5 means that
each row is five units."
  (let* ((symbol-width (length graph-blank))
         ;; height は最大の数でもあり、
         ;; 最大桁の数でもある。
         (height (apply 'max numbers-list))
         (height-of-top-line
          (if (zerop (% height Y-axis-label-spacing))
              height
            ;; else
            (* (1+ (/ height Y-axis-label-spacing))
               Y-axis-label-spacing)))
         (vertical-step (or vertical-step 1))
         (full-Y-label-width
          (length
           (concat
            (int-to-string
             (* height-of-top-line vertical-step))
            Y-axis-tic))))

    (print-Y-axis
     height-of-top-line full-Y-label-width vertical-step)
    (graph-body-print
     numbers-list height-of-top-line symbol-width)
    (print-X-axis numbers-list)))

C.4.1 print-graph のテスト  簡単なテスト
C.4.2 単語やシンボルの数のグラフ化  
C.4.3 表示されたグラフ  


C.4.1 print-graph のテスト

print-graph 関数を短い数のリストで試してみよう。

  1. (他のコードに加えて) Y-axis-columnprint-Y-axisgraph-body-print、そして print-graph の各最終バージョンを インストールする。

  2. 次のS式をコピーする。

     
    (print-graph '(3 2 5 6 7 5 3 4 6 4 3 2 1))
    

  3. `*scratch*' バッファに移り、カーソルをグラフを表示したい位置にまで 移動する。

  4. M-: (eval-expression) とタイプする。(訳註:Emacs 19.28 ベー スの Mule では M-ESC.)

  5. C-y (yank) を使って、上のS式をミニバッファにヤンクする。

  6. このS式を評価するために RET を押す。

Emacs は次のようなグラフを表示する。

 
10 -              
                  
                  
         *        
        **   *    
 5 -   ****  *    
       **** ***   
     * *********  
     ************ 
 1 - *************

     |   |    |    |
     1   5   10   15

一方、次のS式を評価して print-graphvertical-step とし て2を与えてみたとする。

 
(print-graph '(3 2 5 6 7 5 3 4 6 4 3 2 1) 2)

するとグラフは次のようになるはずだ。

 
20 -              
                  
                  
         *        
        **   *    
10 -   ****  *    
       **** ***   
     * *********  
     ************ 
 2 - *************

     |   |    |    |
     1   5   10   15

(疑問:横軸の左の `2' はバグだろうか、仕様だろうか。もしバグであると思う のなら、そして `2' ではなく `1' であるべきだと思うのなら (あるいは `0' であるべきだと思うのなら)、ソースを修正すればよい。)


C.4.2 単語やシンボルの数のグラフ化

さて、やっと目的のグラフの所まで来た。いよいよ単語やシンボルの数が10より 少ない関数定義がどれだけあるか、10から19までの間だとどれだけか、20から29 までだとどうか、といったことを示してくれるグラフを描く関数を書くわけであ る。

これは幾つかのプロセスに分けて行う。まずは、必要なコードを全てロードし てあることを確認しよう。

異なる値を設定してしまった場合に備えて、top-of-ranges の値を再設 定しておく方がよいだろう。それには以下を評価すればよい。

 
(setq top-of-ranges
 '(10  20  30  40  50
   60  70  80  90 100
  110 120 130 140 150
  160 170 180 190 200
  210 220 230 240 250
  260 270 280 290 300)

次に、各々の範囲に入っている単語やシンボルの数のリストを作成しよう。

まず次を評価する。

 
(setq list-for-graph
       (defuns-per-range
         (sort
          (recursive-lengths-list-many-files 
           (directory-files "/usr/local/emacs/lisp"
                            t ".+el$"))
          '<)
         top-of-ranges))

私の機械では、これに一時間程かかった。私の持っている Emacs version 19.23 だと303の Lisp ファイルを見ていることになる。この計算の後では、 list-for-graph の値は次のようになる。

 
(537 1027 955 785 594 483 349 292 224 199 166 120 116 99 
90 80 67 48 52 45 41 33 28 26 25 20 12 28 11 13 220)

これは、私の Emacs には10以下の単語やシンボルしか持たない関数定義が537個 あり、10以上19未満の単語やシンボルを持つものが1,027個、20から29までだと 955個、などというふうになっていることを示している。

このリストから明らかに見て取れることは、殆どの関数定義では、中に含まれる 単語やシンボルの数は、10から30までだということである。

さて、これをグラフに表示することにしよう。我々は、1,030行もの高さのグラ フは描きたくない。そうではなく、25行以下程度のグラフを描きたいの である。この高さのグラフであれば、大抵のモニタで表示出来るだろう。また、 紙にも印刷しやすい。

これは、今の場合、list-for-graph の各々の値を50分の1に縮小しない といけないということである。

この操作をやってくれる簡単な関数を、以下に挙げる。この関数には、今までに 出てこなかった二つの関数 mapcarlambda が出てくる。

 
(defun one-fiftieth (full-range)
  "Return list, each number one-fiftieth of previous."
 (mapcar '(lambda (arg) (/ arg 50)) full-range))

lambda  無名関数の書き方
関数 mapcar  要素のリストに関数を作用させる
まだバグがある...  


lambda

lambda は無名関数、即ち名前のない関数のシンボルである。無名関数を 使う場合には毎回本体を全て含めて書かなければならない。

従って、

 
(lambda (arg) (/ arg 50))

であれば、常に arg として渡されたものを50で割った値を返す、という 関数定義になる。

例えば、以前、multiply-by-seven 関数というのを説明したことがある。 これは引数を7倍するものであった。今回の関数も同様である。異なる点は、引 数を50で割るということと、名前がないということだけである。 multiply-by-seven を無名関数で書き直すと次のようになる。

 
(lambda (number) (* 7 number))

(特殊形式 defun, を参照。)

もし3を7倍したいなら、次のように書くことが出来る。

 
(multiply-by-seven 3)
 \_____________/ ↑
         |         │
      function  argument

このS式は21を返す。

同様に、次のようにも書ける。

 
((lambda (number) (* 7 number)) 3)
 \__________________________/ ↑
               |                │
      anonymous function     argument

100を50で割る場合は次のように書く。

 
((lambda (arg) (/ arg 50)) 100)
 \_____________________/  ↑
             |              │
    anonymous function    argument

このS式は2を返す。上の100は関数に渡されるもので、その数を関数が50で割っ ているのである。

lambda についてのより詳しいことは、section `Lambda Expressions' in The GNU Emacs Lisp Reference Manual, を参照 せよ。Lisp や lambda 式は、Lambda 計算から派生したものである。


関数 mapcar

mapcar は最初の引数を二番目の引数の各々の要素にともに、順に呼び出 す関数である。二番目の引数はシーケンス (sequence) でないといけない。

例えば、

 
(mapcar '1+ '(2 4 6))
     => (3 5 7)

という感じである。関数 1+ は引数に1を加える関数であるが、これがそ の後のリストの各要素について実行され、新しいリストを返すわけである。

これを apply と比較してみよう。こちらは最初の要素に対し、二番目以 降の全ての引数を引数として渡して実行させるものだった。(apply の説 明については グラフを描く準備, を参照。)

one-fiftieth の定義では、最初の引数は、

 
(lambda (arg) (/ arg 50))

という無名関数である。また、二番目の引数は full-range であり、こ れは list-for-graph にバインドされる。

S式全体は次の通りである。

 
(mapcar '(lambda (arg) (/ arg 50)) full-range))

mapcar についての詳細は section `Mapping Functions' in The GNU Emacs Lisp Reference Manual, を参照。

one-fiftieth 関数を使うと、各々の要素が、対応する list-for-graph の要素の50分の1であるようなリストを生成することが 出来る。

 
(setq fiftieth-list-for-graph
      (one-fiftieth list-for-graph))

結果として出来るリストは次のようになる。

 
(10 20 19 15 11 9 6 5 4 3 3 2 2 
1 1 1 1 0 1 0 0 0 0 0 0 0 0 0 0 0 4)

これは、今すぐにでも表示出来そうである! (もっともここで情報が失われてい ることにも気付く。上の方の範囲の多くの要素は0である。これは、その範囲の 単語やシンボルを持つ関数定義の数が50未満であることを示しているだけであっ て、そういう関数定義が全くないといっているわけではない。)


まだバグがある...

私は、「今すぐにでも表示出来そう」と書いた。勿論、print-graph に はバグがあるのだ... これには vertical-step オプションがあるが、 horizontal-step オプションはない。top-of-range の範囲は、 10ごとに10から300まである。しかし、print-graph は1ずつしか表示出 来ない。

これは、ある人々が最も見つけにくいタイプのバグだと考えている、考慮不足に よるバグ (訳註:原文では bug of omission. これはどう訳すべきか) の古典的 な例である。これは、コードを調べることで見つかるようなバグではない。コー ド自体に誤りはないからだ。これは単に何かが欠落しているということである。 このようなバグに対するもっとも正しい態度は、プログラムをなるべく早く、何 回もテストしてみることである。そして、コードを出来るだけ理解しやすく簡単 に変更出来るようにアレンジしていくのである。また、一度書いたものは、すぐ にではなくともいつかは書き換えられるものだということを頭に入れておくよう にしよう。これはなかなか実行が困難な格言ではあるが。

作業対象となるのは print-X-axis-numbered-line 関数である。また、 print-X-axisprint-graph もそれに合わせて書き換える必要 がある。もっともやることはそれ程多いわけではない。一つだけ面倒な部分があ る。数字が目盛記号の下に綺麗に並ばなければならないということである。この 点はちょっとばかし頭をひねる必要がある。

以下が修正した print-X-axis-numbered-line である。

 
(defun print-X-axis-numbered-line
  (number-of-X-tics X-axis-leading-spaces
   &optional horizontal-step)
  "Print line of X-axis numbers"
  (let ((number X-axis-label-spacing)
        (horizontal-step (or horizontal-step 1)))
    (insert X-axis-leading-spaces)
    ;; 頭の余計な空白を削除
    (delete-char
     (- (1-
         (length (int-to-string horizontal-step)))))
    (insert (concat  
             (make-string 
              ;; 空白を挿入
              (-  (* symbol-width
                     X-axis-label-spacing) 
                  (1-
                   (length
                    (int-to-string horizontal-step)))
                  2)
              ? )
             (int-to-string
              (* number horizontal-step))))
    ;; 残りの数を挿入
    (setq number (+ number X-axis-label-spacing))
    (while (> number-of-X-tics 1)
      (insert (X-axis-element
               (* number horizontal-step)))
      (setq number (+ number X-axis-label-spacing))
      (setq number-of-X-tics (1- number-of-X-tics)))))

もしあなたがこれを Info の中で読んでいるなら、print-X-axisprint-graph の新しいバージョンを見ることが出来る。もしこれを印刷 された本で読んでいるとしたら、変更する行が下に書かれているはずだ。(印刷 するにはコードがちょっと多過ぎるので。)

 
(defun print-X-axis (numbers-list horizontal-step)
  "Print X axis labels to length of NUMBERS-LIST.
Optionally, HORIZONTAL-STEP, a positive integer,
specifies how much an X  axis label increments for
each column."
;; Value of symbol-width and full-Y-label-width 
;; are passed by `print-graph'.
  (let* ((leading-spaces 
          (make-string full-Y-label-width ? ))
       ;; symbol-width は graph-body-print で与えらえる。
       (tic-width (* symbol-width X-axis-label-spacing))
       (X-length (length numbers-list))
       (X-tic
        (concat  
         (make-string 
          ;; 空白の文字列を作成。
          (-  (* symbol-width X-axis-label-spacing)
              (length X-axis-tic-symbol))
          ? )
         ;; 空白を目盛記号と結合。
         X-axis-tic-symbol))
       (tic-number
        (if (zerop (% X-length tic-width))
            (/ X-length tic-width)
          (1+ (/ X-length tic-width)))))

    (print-X-axis-tic-line
     tic-number leading-spaces X-tic)
    (insert "\n")
    (print-X-axis-numbered-line
     tic-number leading-spaces horizontal-step)))

 
(defun print-graph
  (numbers-list &optional vertical-step horizontal-step)
  "Print labelled bar graph of the NUMBERS-LIST.
The numbers-list consists of the Y-axis values.

Optionally, VERTICAL-STEP, a positive integer,
specifies how much a Y axis label increments for
each line.  For example, a step of 5 means that
each row is five units.

Optionally, HORIZONTAL-STEP, a positive integer,
specifies how much an X  axis label increments for
each column."
  (let* ((symbol-width (length graph-blank))
         ;; height は最大の数でもあり、
         ;; 最大桁の数でもある。
         (height (apply 'max numbers-list))
         (height-of-top-line
          (if (zerop (% height Y-axis-label-spacing))
              height
            ;; else
            (* (1+ (/ height Y-axis-label-spacing))
               Y-axis-label-spacing)))
         (vertical-step (or vertical-step 1))
         (full-Y-label-width
          (length
           (concat
            (int-to-string
             (* height-of-top-line vertical-step))
            Y-axis-tic))))
    (print-Y-axis
     height-of-top-line full-Y-label-width vertical-step)
    (graph-body-print
        numbers-list height-of-top-line symbol-width)
    (print-X-axis numbers-list horizontal-step)))


C.4.3 表示されたグラフ

全てをインストールし終ったら、print-graph コマンドを次のようにし て呼び出そう。

 
(print-graph fiftieth-list-for-graph 50 10)

次のようなグラフが表示されるはずだ。

 
1000 -  *                             
        **                            
        **                            
        **                            
        **                            
 750 -  ***                           
        ***                           
        ***                           
        ***                           
        ****                          
 500 - *****                          
       ******                         
       ******                         
       ******                         
       *******                        
 250 - ********                       
       *********                     *
       ***********                   *
       *************                 *
  50 - ***************** *           *
       |   |    |    |    |    |    |    |
      10  50  100  150  200  250  300  350

最も大きな関数のグループは、10から19までの単語やシンボルを含んでいる もののグループである。


[ << ] [ >> ]           [Top] [Contents] [Index] [ ? ]

This document was generated by Matsuda Shigeki on April, 10 2002 using texi2html