[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
Координатные оси помогают воспринимать график, обычно на них наносится масштаб. В предыдущих главах мы создали функции для печати тела графика, сейчас мы напишем функции для печати вертикальной и горизонтальной оси вместе с самим графиком.
Пример графика с пометками How a finished graph should look. C.2 The print-Y-axis
FunctionPrint a label for the vertical axis. C.3 The print-X-axis
FunctionPrint a horizontal label. C.4 Printing the Whole Graph The function to print a complete graph.
Так как вставляемые символы заполняют буфер вправо и вниз наша новая функция должна вначале печатать вертикальную ось или ось Y, затем тело графика и в конце ось 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
.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
print-graph
При создании функции print-graph
первая задача написать список
переменных для выражения let
. (Пока мы не будем задумываться
документировании функции и о том будет ли она интерактивной или нет.)
В списке переменных будет создано несколько значений. Ясно, что верхняя
метка для вертикальной оси должна быть по крайней мере равна высоте
графика, это значит что здесь нам понадобится эта информация. Отметим,
что функции print-graph-body
так же требуется эта информация.
Нет никаких оснований вычислять высоту графика в двух разных местах,
поэтому мы должны немного изменить предыдущую версию
print-graph-body
для того чтобы воспользоваться появившимися
преимуществами.
Аналогично и функции для печати оси X и функции print-graph-body
необходимо знать ширину каждого символа, поэтому мы можем произвести
вычисления ширины символа при создании списка аргументов и так же
немного изменить предыдущее определение print-graph-body
.
Последняя метка откладываемая по горизонтали должна быть по крайней мере
такой же длины как и график, однака эта информация используется только в
функции для печати горизонтальной оси, поэтому мы можем не вычислять ее
в списке переменных выражения let
.
Все это приводит нас к следующей форме списка переменных выражения
let
для функции print-graph
:
(let ((height (apply 'max numbers-list)) ; First version. (symbol-width (length graph-blank))) |
В последствии мы увидим, что это выражение не совсем правильно.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
print-Y-axis
Function
Задача функции print-Y-axis
пронумеровать вертикальную ось, что
будет выглядеть примерно следующим образом:
10 - 5 - 1 - |
В эту функцию надо передать высоту графика, для того чтобы она должным образом вставила соответствующие номера и метки.
Впрочем легко сказать или нарисовать как должна выглядеть ось Y; но
написать функцию это совсем другое дело. Будет не совсем правильно
сказать, что мы хотим номер и метку каждые пять строки: ведь только три
строки между `1' и `5' (строки 2, 3 и 4), а между `5' и
10
уже четыре строки (строки 6, 7, 8, 9). Лучше скажем, что нам
нужна отметка и номер в начале оси и потом метка с номером каждые
следующие пять строк.
Следующее, что мы должны обсудить чему должна быть равна метка,
предположим что максимальная высота колонки графика равна 7. Какой
должна быть верхняя отметка по оси Y `5 -', ведь тогда график будет
выше отметки? Или верхняя метка должна быть равна `7 -', чтобы мы
знали чему равен пик графика? А может быть верхняя метка должна быть
равна 10 -
, ведь 10 делится на пять, но тогда верхняя метка будет
расположена выше чем верхнее значение графика?
Лучше всего реализовать последний случай. Большинство графиков рисуется
в прямоугольниках, чьи оси пронумерованы дискретно 5 --- 5, 10, 15,
и так далее. Но как только мы решаем использовать дискретных шаг для
вертикальной оси, мы обнаруживаем, что простое выражение для списка
переменных вычисляющее высоту неправильно. Это выражение (apply
'max numbers-list)
, и оно вернет максимальную высоту столбца, а нам
необходимо значение округленное к ближайшему числу кратному пяти.
Значит необходимо более сложное выражение.
Как обычно сложная задача станет проще, если ее можно разделить на несколько небольших задач.
Во первых может получится, что высота графика число кратное пяти --- 5, 10, 15 или какое-нибудь большее число. В этом случая мы можем использовать это значение как высоту оси Y.
Самый простой способ определить кратность числа пяти это разделить число на пять и проанализировать остаток. Если остаток равен 0, значит число кратно пяти. Например семь при делении на пять дает в остатке 2, значит семь не кратно пяти. Если сказать совсем просто, как обычно говорять в школе пять входит в семь один раз с остатком два. Однако пять входит в делять дважды без остатка: значит десять кратно пяти.
C.2.3 Создание колонки оси Y Generate a list of Y axis labels.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
В Лиспе есть функция для вычисления остатка это %
. Эта функция
возвращает остаток от деления своего первого аргумента на свой второй
аргумент. Так случилось, что %
функция Emacs Lisp, которую вы не
сможете обнаружить с помощью g apropos
: поиск M-x apropos
RET remainder RET ничего не выдасть. Единственных способ
узнать о существовании %
только прочесть об этом в какой-нибудь
хорошей книге или изучив исходные тексты Emacs Lisp. Функция %
используется в определении функции rotate-yank-pointer
, об этом
можно прочесть в приложении (See section Тело функции rotate-yank-pointer
.)
Вы можете освоить функции %
вычислив следующие два выражения:
(% 7 5) (% 10 5) |
Первое выражение вернет 2, а второе выражение вернет 0.
Для проверки того, равно ли значение нулю или нет мы можем использовать
функцию zerop
. Эта функция вернет t
, если ее аргумент,
который должен быть числом, ноль.
(zerop (% 7 5)) => nil (zerop (% 10 5)) => t |
Таким образом следующее выражение вернет t
, если высота графика
кратна пяти:
(zerop (% height 5)) |
(Значение height
, можно найти с помощью выражения (apply
'max numbers-list)
.)
С другой стороны, если значение height
не кратно пяти, мы должны
установить его в большее значение, которое кратно пяти. Это достаточно
простое арифметическое действие, которое вполне можно выполнить уже
знакомыми нам функциями. Сначала мы разделим значение height
на
пять для того чтобы определить сколько раз пять входит в это число.
Например пять два раза входит в двенадцать, если мы добавим к частному
единицу и умножим его на пять, то мы получим большее число кратное
пяти. Пять два раза входит в двенадцать, добавив к двум один и умножив
на пять мы получим пятнадцать, число кратное пяти и большее чем
двенадцать. Соответствующее Лисп выражение приведено ниже:
(* (1+ (/ height 5)) 5) |
После вычисления следующего выражения вы получите 15:
(* (1+ (/ 12 5)) 5) |
Во время наших рассуждений мы использовали `пять', как значение
дискретного шага по оси Y; но мы вполне можем выбрать и какое-нибудь
другое значение, лучше всего, если мы заменим `пять' переменной, которой
мы сможем присвоить значение. Самое лучшее имя, которое я смог
придумать это Y-axis-label-spacing
. Используя новоизобретенную
переменную и выражение if
, мы получим следующее выражение:
(if (zerop (% height Y-axis-label-spacing)) height ;; else (* (1+ (/ height Y-axis-label-spacing)) Y-axis-label-spacing)) |
Это выражение возвращает значение height
, если высота кратна
значению Y-axis-label-spacing
или вычисляет и присваивает
переменной height
следующее большее число, кратное
Y-axis-label-spacing
.
Мы можем теперь включить это выражение в список переменных формы
let
для функции print-graph
(конечно после того, как
присвоим значение переменной Y-axis-label-spacing
):
(defvar Y-axis-label-spacing 5 "Масштаб по вертикальной оси.") ... (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)
и
после этого получившееся значение используется для вычисления
окончательного результата. See section The let*
expression, за дополнительной информацией о let*
.)
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
Когда мы печатаем вертикальные оси, мы вставляем строки, такие как `5 -' и `10 - ' каждые пять строк. Более того мы хотим, чтобы числа и тире шли по линии вверх, так что более короткие номера были выровнены ведущими пробелами. Если некоторые из строк используют двухцифровые числа, тогда строки с одной цифрой должны включать ведущий пробел перед числом.
Для того чтобы вычислить длину числа используется функция
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 " - " "Строка следующая за числом по оси Y.") |
Длина метки Y это сумма длин Y-axis-tic
и длины наибольшего
числа отложенного по оси Y.
(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
. Этой функции
требуются два аргумента: первый задает, какой длины должна быть строка,
а второй символ, который нужно вставить, заданный в особом формате. В
нашем случае формат это знак вопроса за которым следует пробел, например
`? '. See section `Character Type' in The GNU Emacs Lisp Reference Manual, за подробным описанием синтаксиса для символов.
Функция int-to-string
используется при конкатенции выражения, для
того, чтобы конвертировать число в строку, которая будет слита с
пробелами и знаком тире.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
Предыдущая функция обеспечила все что нам необходимо для того, чтобы составить функцию, которая произведет список пронумерованных и пустых строк, для того чтобы нанести масштаб по вертикальной оси:
(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)) ;; Insert label. (setq Y-axis (cons (Y-axis-element height width-of-label) Y-axis)) ;; Else, insert blanks. (setq Y-axis (cons (make-string width-of-label ? ) Y-axis))) (setq height (1- height))) ;; Insert base line. (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
. Базовая линия состоит из числа единица, за
которым следуюет временная пометка.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
print-Y-axis
Список созданный функцией Y-axis-column
передается функции
the print-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-Y-axis
использует функцию insert-rectangle
для вставки меток оси Y, созданных функцией Y-axis-column
. В
добавление к этому, она помещает точку в правильную позицию для
вывода тела графика.
Вы можете проверить работу print-Y-axis
:
Y-axis-label-spacing Y-axis-tic Y-axis-element Y-axis-columnprint-Y-axis |
(print-Y-axis 12 5) |
eval-expression
).
graph-body-print
в минибуфер с помощью
клавиши C-y (yank)
.
Emacs will print labels vertically, the top one being
`10 - '. (The print-graph
function
will pass the value of height-of-top-line
, which
in this case would be 15.)
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
print-X-axis
Function X axis labels are much like Y axis labels, except that the tics are on a line above the numbers. Labels should look like this:
| | | | 1 5 10 15 |
The first tic is under the first column of the graph and is preceded by
several blank spaces. These spaces provide room in rows above for the Y
axis labels. The second, third, fourth, and subsequent tics are all
spaced equally, according to the value of X-axis-label-spacing
.
The second row of the X axis consists of numbers, preceded by several
blank spaces and also separated according to the value of the variable
X-axis-label-spacing
.
The value of the variable X-axis-label-spacing
should itself be
measured in units of symbol-width
, since you may want to change
the width of the symbols that you are using to print the body of the
graph without changing the ways the graph is labeled.
The print-X-axis
function is constructed in more or less the
same fashion as the print-Y-axis
function except that it has
two lines: the line of tic marks and the numbers. We will write a
separate function to print each line and then combine them within the
print-X-axis
function.
This is a three step process:
print-X-axis-tic-line
.
print-X-axis-numbered-line
.
print-X-axis
function,
using print-X-axis-tic-line
and
print-X-axis-numbered-line
.
C.3.1 X Axis Tic Marks Create tic marks for the horizontal axis.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
The first function should print the X axis tic marks. We must specify the tic marks themselves and their spacing:
(defvar X-axis-label-spacing (if (boundp 'graph-blank) (* 5 (length graph-blank)) 5) "Number of units from one X axis label to next.") |
(Note that the value of graph-blank
is set by another
defvar
. The boundp
predicate checks whether it has
already been set; boundp
returns nil
if it has not. If
graph-blank
were unbound and we did not use this conditional
construction, we would receive an error message saying `Symbol's
value as variable is void'.)
(defvar X-axis-tic-symbol "|" "String to insert to point to a column in X axis.") |
The goal is to make a line that looks like this:
| | | | |
The first tic is indented so that it is under the first column, which is indented to provide space for the Y axis labels.
A tic element consists of the blank spaces that stretch from one tic to
the next plus a tic symbol. The number of blanks is determined by the
width of the tic symbol and the X-axis-label-spacing
.
The code looks like this:
;;; X-axis-tic-element ... (concat (make-string ;; Make a string of blanks. (- (* symbol-width X-axis-label-spacing) (length X-axis-tic-symbol)) ? ) ;; Concatenate blanks with tic symbol. X-axis-tic-symbol) ... |
Next, we determine how many blanks are needed to indent the first tic
mark to the first column of the graph. This uses the value of
full-Y-label-width
passed it by the print-graph
function.
The code to make X-axis-leading-spaces
looks like this:
;; X-axis-leading-spaces ... (make-string full-Y-label-width ? ) ... |
We also need to determine the length of the horizontal axis, which is the length of the numbers list, and the number of tics in the horizontal axis:
;; 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)))) |
All this leads us directly to the function for printing the X axis tic line:
(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) ; Under first column. ;; Insert second tic in the right spot. (insert (concat (make-string (- (* symbol-width X-axis-label-spacing) ;; Insert white space up to second tic symbol. (* 2 (length X-axis-tic-symbol))) ? ) X-axis-tic-symbol)) ;; Insert remaining tics. (while (> number-of-X-tics 1) (insert X-axis-tic-element) (setq number-of-X-tics (1- number-of-X-tics)))) |
The line of numbers is equally straightforward:
First, we create a numbered element with blank spaces before each number:
(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)))) |
Next, we create the function to print the numbered line, starting with the number "1" under the first column:
(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 ;; Insert white space up to next number. (- (* symbol-width X-axis-label-spacing) 2) ? ) (int-to-string number))) ;; Insert remaining numbers. (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))))) |
Finally, we need to write the print-X-axis
that uses
print-X-axis-tic-line
and
print-X-axis-numbered-line
.
The function must determine the local values of the variables used by both
print-X-axis-tic-line
and print-X-axis-numbered-line
, and
then it must call them. Also, it must print the carriage return that
separates the two lines.
The function consists of a varlist that specifies five local variables, and calls to each of the two line printing functions:
(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 is provided by graph-body-print (tic-width (* symbol-width X-axis-label-spacing)) (X-length (length numbers-list)) (X-tic (concat (make-string ;; Make a string of blanks. (- (* symbol-width X-axis-label-spacing) (length X-axis-tic-symbol)) ? ) ;; Concatenate blanks with 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))) |
You can test print-X-axis
:
X-axis-tic-symbol
, X-axis-label-spacing
,
print-X-axis-tic-line
, as well as X-axis-element
,
print-X-axis-numbered-line
, and print-X-axis
.
(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)))) |
eval-expression
).
yank)
.
Emacs will print the horizontal axis like this:
| | | | | 1 5 10 15 20 |
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
Now we are nearly ready to print the whole graph.
The function to print the graph with the proper labels follows the outline we created earlier (@xref{Full Graph, , A Graph with Labelled Axes}), but with additions.
Here is the outline:
(defun print-graph (numbers-list) "documentation..." (let ((height ... ...)) (print-Y-axis height ... ) (graph-body-print numbers-list) (print-X-axis ... ))) |
The final version is different from what we planned in two ways: first, it contains additional values calculated once in the varlist; second, it carries an option to specify the labels' increment per row. This latter feature turns out to be essential; otherwise a graph may have more rows than fit on a display or on a sheet of paper.
This new feature requires a change to the Y-axis-column
function, to add vertical-step
to it. The function looks like
this:
;;; Final version. (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)) ;; Insert label. (setq Y-axis (cons (Y-axis-element (* height number-per-line) width-of-label) Y-axis)) ;; Else, insert blanks. (setq Y-axis (cons (make-string width-of-label ? ) Y-axis))) (setq height (1- height))) ;; Insert base line. (setq Y-axis (cons (Y-axis-element (or vertical-step 1) width-of-label) Y-axis)) (nreverse Y-axis))) |
The values for the maximum height of graph and the width of a symbol
are computed by print-graph
in its let
expression; so
graph-body-print
must be changed to accept them.
;;; Final version. (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) ;; Draw graph column by column. (sit-for 0) (setq numbers-list (cdr numbers-list))) ;; Place point for X axis labels. (forward-line height) (insert "\n"))) |
Finally, the code for the print-graph
function:
;;; Final version.
(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))
;; |
C.4.1 Testing print-graph
Run a short test. C.4.2 Graphing Numbers of Words and Symbols Executing the final code. C.4.3 The Printed Graph The graph itself!
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
print-graph
We can test the print-graph
function with a short list of numbers
Y-axis-column
,
graph-body-print
, and print-graph
(in addition to the
rest of the code.)
(print-graph '(3 2 5 6 7 5 3 4 6 4 3 2 1)) |
eval-expression
).
yank)
.
Emacs will print a graph that looks like this:
10 - * ** * 5 - **** * **** *** * ********* ************ 1 - ************* | | | | 1 5 10 15 |
On the other hand, if you pass print-graph
a
vertical-step
value of 2, by evaluating this expression:
(print-graph '(3 2 5 6 7 5 3 4 6 4 3 2 1) 2) |
The graph looks like this:
20 - * ** * 10 - **** * **** *** * ********* ************ 2 - ************* | | | | 1 5 10 15 |
(A question: is the `2' on the bottom of the vertical axis a bug or a feature? If you think it is a bug, and should be a `1' instead, (or even a `0'), you can modify the sources.)
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
Now for the graph for which all this code was written: a graph that shows how many function definitions contain fewer than 10 words and symbols, how many contain between 10 and 19 words and symbols, how many contain between 20 and 29 words and symbols, and so on.
This is a multi-step process. First make sure you have loaded all the requisit code.
It is a good idea to reset the value of top-of-ranges
in case
you have sent it to some different value. You can evaluate the
following:
(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) |
Next create a list of the number of words and symbols in each range.
Evaluate the following:
(setq list-for-graph (defuns-per-range (sort (recursive-lengths-list-many-files (directory-files "/usr/local/emacs/lisp" t ".+el$")) '<) top-of-ranges)) |
On my machine, this takes about an hour. It looks though 303 Lisp
files in my copy of Emacs version 19.23. After all that computing,
the list-for-graph
has this value:
(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) |
This means that my copy of Emacs has 537 function definitions with fewer than 10 words or symbols in them, 1,027 function definitions with 10 to 19 words or symbols in them, 955 function definitions with 20 to 29 words or symbols in them, and so on.
Clearly, just by looking at this list we can see that most function definitions contain ten to thirty words and symbols.
Now for printing. We do not want to print a graph that is 1,030 lines high ... Instead, we should print a graph that is fewer than twenty-five lines high. A graph that height can be displayed on almost any monitor, and easily printed on a sheet of paper.
This means that each value in list-for-graph
must be reduced to
one-fiftieth it present value.
Here is a short function to do just that, using two functions we have
not yet seen, mapcar
and lambda
.
(defun one-fiftieth (full-range) "Return list, each number one-fiftieth of previous." (mapcar '(lambda (arg) (/ arg 50)) full-range)) |
A lambda
ExpressionHow to write an anonymous function. The mapcar
FunctionApply a function to elements of a list. Another Bug ... Most Insidious Yet another ... of a most insidious type.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
lambda
Expression
lambda
is the symbol for an anonymous function, a function
without a name. Every time you use an anonymous function, you need to
include its whole body.
Thus,
(lambda (arg) (/ arg 50)) |
is a function definition that says `return the value resulting from
dividing whatever is passed to me as arg
by 50'.
Earlier, for example, we had a function multiply-by-seven
; it
multiplied its argument by 7. This function is similar, except it
divides its argument by 50; and, it has no name. The anonymous
equivalent of multiply-by-seven
is:
(lambda (number) (* 7 number)) |
(See section The defun
Special Form.)
If we want to multiply 3 by 7, we can write:
(multiply-by-seven 3) \_______________/ ^ | | function argument |
This expression returns 21.
Similarly, we can write:
((lambda (number) (* 7 number)) 3) \____________________________/ ^ | | anonymous function argument |
If we want to divide 100 by 50, we can write:
((lambda (arg) (/ arg 50)) 100) \______________________/ \_/ | | anonymous function argument |
This expression returns 2. The 100 is passed to the function, which divides that number by 50.
See section `Lambda Expressions' in The GNU Emacs Lisp Reference Manual, for more about lambda
. Lisp and lambda
expressions derive from the Lambda Calculus.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
mapcar
Function
mapcar
is a function that calls its first argument with each
element of its second argument, in turn. The second argument must be
a sequence.
For example,
(mapcar '1+ '(2 4 6)) => (3 5 7) |
The function 1+
which adds one to its argument, is executed on
each element of the list, and a new list is returned.
Contrast this with apply
, which applies its first argument to
all the remaining.
(See section Изготовляем график, for a explanation of
apply
.)
In the definition of one-fiftieth
, the first argument is the
anonymous function:
(lambda (arg) (/ arg 50)) |
and the second argument is full-range
, which will be bound to
list-for-graph
.
The whole expression looks like this:
(mapcar '(lambda (arg) (/ arg 50)) full-range)) |
See section `Mapping Functions' in The GNU Emacs Lisp Reference Manual, for more about mapcar
.
Using the one-fiftieth
function, we can generate a list in
which each element is one-fiftieth the size of the corresponding
element in list-for-graph
.
(setq fiftieth-list-for-graph (one-fiftieth list-for-graph)) |
The resulting list looks like this:
(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) |
This we are almost ready to print! (We also notice the loss of information: many of the higher ranges are 0, meaning that fewer than 50 defuns had that many words or symbols --- but not necessarily meaning that none had that many words or symbols.)
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
I said `almost ready to print'! Of course, there is a bug in the
print-graph
function ... It has a vertical-step
option, but not a horizontal-step
option. The
top-of-range
scale goes from 10 to 300 by tens. But the
print-graph
function will print only by ones.
This is a classic example of what some consider the most insidious type of bug, the bug of omission. This is not the kind of bug you can find by studying the code, for it is not in the code; it is an omitted feature. Your best actions are to try your program early and often; and try to arrange, as much as you can, to write code that is easy to understand and easy to change. Try to be aware, whenever you can, that whatever you have written, will be rewritten, if not soon, eventually. A hard maxim to follow.
It is the print-X-axis-numbered-line
function that needs the
work; and then the print-X-axis
and the print-graph
functions need to be adapted. Not much needs to be done; there is one
nicety: the numbers ought to line up under the tic marks. This takes
a little thought.
Here is the corrected 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 extra leading spaces. (delete-char (- (1- (length (int-to-string horizontal-step))))) (insert (concat (make-string ;; Insert white space. (- (* symbol-width X-axis-label-spacing) (1- (length (int-to-string horizontal-step))) 2) ? ) (int-to-string (* number horizontal-step)))) ;; Insert remaining numbers. (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))))) |
If you are reading this in Info, you can see the new versions of
print-X-axis
print-graph
and evaluate them. If you are
reading this in a printed book, you can see the changed lines here
(the full text is too much to print).
(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 is provided by graph-body-print (tic-width (* symbol-width X-axis-label-spacing)) (X-length (length numbers-list)) (X-tic (concat (make-string ;; Make a string of blanks. (- (* symbol-width X-axis-label-spacing) (length X-axis-tic-symbol)) ? ) ;; Concatenate blanks with 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))
;; |
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
When made and installed, you can call the print-graph
command
like this:
(print-graph fiftieth-list-for-graph 50 10) |
Here is the graph:
1000 - * ** ** ** ** 750 - *** *** *** *** **** 500 - ***** ****** ****** ****** ******* 250 - ******** ********* * *********** * ************* * 50 - ***************** * * | | | | | | | | 10 50 100 150 200 250 300 350 |
The largest group of functions contain 10 --- 19 words and symbols each.
[ << ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |