Lisp-Stat翻译 —— 第九章 统计绘图窗体

痞子三分冷 提交于 2019-12-07 18:41:10

第九章 统计绘图窗体

    除了前几章略述的绘图窗体原型提供的基本绘图工具之外,Lisp-Stat里的统计绘图还需要用来管理数据和将那些数据转换成屏幕上的图形的工具集。这些工具由绘图原型graph-proto提供。更多的专业绘图工具,比如直方图和散点图矩阵,它们都是基于继承自graph-proto的原型。本章的第一节描述了绘图原型,第二节略述了更加专用的原型,下一章将展示那些描述如何从这些原型来开发新的绘图工具类型的例子。

9.1 绘图原型

    graph-proto原型实现了一个散点图,该散点图用来表示在m维空间中的点和线的二维视图。该视图是这样构造的,首先对数据进行中心化和尺度化,然后使用线性变换,比如旋转变换,最后产生该变换的图形里的维度的两个维度的一个散点图。该原型的:resize和:redraw方法可以保证:当窗体出现或改变大小时,图形可以得到合适的重画。鼠标点击和移动方法支持以下语法:即在第2.5节描述的在选择模式和刷模式里使用的绘图方式。该原型也提供了一个基本的菜单用来与图形交互。

    为了给出该原型提供的机制的详细的说明,本节使用一个图形来检测5.6.2节引入的stack loss数据。

9.1.1 构造一个新的图形

    graph-proto原型继承自graph-window-proto原型,graph-proto的:isnew方法需要一个参数,即表示将被视图化的那个空间的维度的整型值m。stack loss数据由4个变量组成,气流(Air)、温度(Temp)、浓度(Conc)和氨损耗(Loss)。视图化这些数据的图形可以这样构造:

> (setf w (send graph-proto :new 4))
#<Object: 141e7e8, prototype = GRAPH-PROTO>
图形里的变量数目可以使用:num-variables消息来获取:
> (send w :num-variables)
4
但是,一个图形创建,该值就不能改变。

    图形包含为每个维度描述的标签字符串,这些字符串可以使用:variable-label消息来设置和获取。初始情况下,这些字符串是空值:

> (send w :variable-label 0)
""
可以通过使用一个字符串作为其第二个参数的方式来改变它的值:
> (send w :variable-label 0 "Air")
"Air"
这个消息的方法是矢量化的。表示三个维度的标签可以通过下式指定:
> (send w :variable-label '(1 2 3) (list "Temp." "Conc" "Loss"))
("Temp." "Conc" "Loss")
现在我们可以获得这4个变量了:
> (send w :variable-label '(0 1 2 3))
("Air" "Temp." "Conc" "Loss")
    :graph-proto原型的:isnew方法可以接受graph-window-proto原型的:isnew方法能够接受的所有关键字参数。此外,:variable-label关键字可用来指定一个m维度的初始化变量标签字符串。还有一个关键字:scale-type将在接下来的9.1.3节里描述。

9.1.2 增加数据可坐标轴

点数据

    一个图形里可以包含两类数据:点数据和线数据。点由m维空间里的坐标和一些附加信息组成,这些附加信息比如像用来绘制点的颜色和符号。初始情况下,图形里不包含点数据:

> (send w :num-points)
0
点数据可以使用:add-points消息来添加,该消息的方法需要一个参数:一个m维的列表,表示将要添加的点的坐标值。下边的表达式将stack loss数据添加到图形里:
> (def air '(80 80 75 62 62 62 62 62 58 58 58 58 58 58 50 50 50 50 50 56 70))
AIR
> (def temp '(27 27 25 24 22 23 24 24 23 18 18 17 18 19 18 18 19 19 29 29 29))
TEMP
> (def conc '(89 88 90 87 87 87 93 93 87 80 89 88 82 93 89 86 72 79 80 82 91))
CONC
> (def loss '(42 37 37 28 18 18 19 20 15 14 14 13 11 12 8 7 8 8 9 15  15))
LOSS
> (send w :add-points (list air temp conc loss))
NIL
数据集里有21个点:
> (send w :num-points)
21
:add-points方法会在屏幕上绘制了新的点,除非提供了值为nil的:draw关键字。该方法也允许使用:point-label关键字的标签字符串列表。

    尽管现在该图形包含这些数据,在它的窗体上不会显示任何点。原因是这些数据点在初始情况下,当做在每一个变量的单位间隔组成的一个数据范围。为了调整图形视图化的范围以适应这些数据,你可以向图形发送:adjust-to-data消息,你可以使用下边的表达式发送该消息:

> (send w :adjust-to-data)
NIL
或者也可以从图形的菜单里选择Rescale Plot菜单项。这个消息的方法将调整图形视图化的数据范围以精确地适应对应的数据的范围跨度。发送该消息之后,图形应该显示了一个散点图,该散点图表示出数据集中前两个变量。结果图形如图9.1所示。

    当向一个图形里加入新点的时候,每一个点都会分给一个默认的符号、颜色和标签,对于第一个点:

> (send w :point-symbol 0)
DISK
> (send w :point-color 0)
NIL
> (send w :point-label 0)
"0"

图9.1 stack loss数据集中气流与温度变量图示

    默认符号是一个叫disk的符号。默认颜色是nil,意思是改点使用当前绘图窗体的颜色来绘制。默认标签是该点索引的字符串形式。通过向这些消息传递第二个参数来为这些属性指定新值。符号应该取自plot-symbol-symbols函数返回的列表。颜色值应该是nil或者由color-symbols返回的列表的一个值。:point-symbol, :point-color和:point-label消息的方法是矢量化的,因此将所有21个点的符号设置到diamond变量里。这三个消息都不会引起图形重画;为了看到屏幕上改变的影响,你不得不向图形发送一个重画命令。

    每个点还有一个状态值,该值可以使用:point-stat消息来设置和获取。该状态值可以是invisible, normal, hilited和selected这四个符号中的一个。点数据状态用做链接机制的一部分,它将在9.1.5节详细描述。

    :point-coordinate消息可以用来为某一特定点获取和设置单一变量坐标的值。该消息的方法需要两个参数:变量的索引和点数据的索引。因此,第一次观察到的气流、温度、浓度和氨损耗的值是:

> (send w :point-coordinate 0 0)
80.0
> (send w :point-coordinate 1 0)
27.0
> (send w :point-coordinate 2 0)
89.0
> (send w :point-coordinate 3 0)
42.0
:point-coordinate消息的方法也是矢量化的。新的坐标值可以以第三个参数的形式来指定。在强调一次,提供新值的时候,方法不会重画图形。

练习 9.1
略。

线数据

    线数据表示从处在m维空间里叫做linestart的点位开始,每个起点都包含额外的信息(比如在绘制线段时使用的宽度和线型信息),还有用来作为线段终点的另一个linestart的索引,nil的下标表示该linestart仅用作在其它地方凯斯的线段的终点。环形定义是是运行的:这不会带来什么问题,因为绘图路径斤通过linestart集合一次。

    当创建一个图形的时候,它没有linestart:

> (send w :num-lines)
0

    stack loss数据事实上是随着时间收集到的。通过绘制一条从第一个观察点到第二个观察点、从第二个到第三个观察点等等的直线的方式,来表示时间关系,这可能是很有用的。:add-line消息可以增加这样一个线段序列,它的方法需要一个参数,一个针对linestart的坐标的m维列表的列表。因此,下式将针对stack loss数据向我们的图形里添加连在一起的线段序列。这些线数据有助于说明,在数据集的前10个观测量里,气流和温度是下降的。

> (send w :add-lines (list air temp conc loss))
NIL

    :add-lines消息的方法还允许使用:type关键字来提供线型,这时直线会会自导图形上,除非使用了值为nil的:draw关键字。

    每一个linestart都有一个宽度、类型和颜色,用来从一个linestart绘制到下一个linestart。对于第一个linestart,对应第一个数据点:

> (send w :linestart-width 0)
1
> (send w :linestart-type 0)
SOLID
> (send w :linestart-color 0)
NIL

该方法也是矢量化的,通过提供一个新值作为第二个参数它可以被用来改变linestart属性的值,数值的改变不会引起任何绘图行为的发生。

    每个linestart都包含序列里下一个linestart的索引,在绘图中用来作为一个线段的终点,:add-lines方法将这些linestart连接成序列,因此:

> (send w :linestart-next 0)
1
> (send w :linestart-next 1)
2
序列里的最后一个linestart没有关于下一个linestart的索引:
> (send w :linestart-next 20)
NIL
可以通过在线段的开始处为linestart设置下一个线段的值为nil,来移除一个线段。例如:
> (send w :linestart-next 7 nil)
NIL
上式移除了点号索引为7和8的点之间的线段,为了使该改变可见,你可以向图形发送:redraw消息。

    就想点一样,你也可以获取和改变linestart的坐标。第一个linestart的前两个坐标值可以这样给出(对应气流和温度变量):

> (send w :linestart-coordinate 0 0)
80.0
> (send w :linestart-coordinate 1 0)
27.0
:linestart-coordinate消息的方法是矢量化的,可以用来改变坐标的值。如果坐标改变了,图形不会重画。

练习 9.2

略。

坐标轴和当前变量

    由graph-proto原型实现的散点图,使用:x-axis和:y-axis消息,可以用来表示x和y坐标轴。不提供参数,这些消息返回当前坐标轴的状态。例如:

> (send w :x-axis)
(NIL NIL 0)
列表的三个元素表示该坐标轴是否正处于显示状态,是否有标签,它使用的刻度的数目。发送一个值为t的参数将重画带坐标轴的图形。默认地,不使用标签,使用4为刻度:
> (send w :x-axis t)
(T NIL 4)
你可以使用可选的第二、三个参数,来指定一个替代的选项。:y-axis方法是相同的。当坐标轴状态改变时,这两个方法都向图形发送:resize和:redraw消息,除非使用了值为nil的:draw关键字。为了使用该关键字,你需要给出所有这3个可选参数。在加入x、y坐标轴之后,我们的图形如图9.2所示。

图9.2 stack loss数据集中气流和温度变量的x坐标与y坐标图形,连续观测量由直线相连

    :adjust-to-data方法会将被一个图形视图化的数据范围设置到数据范围,该方法通常不会产生一个效果很好的坐标轴标签。你也可以使用:range消息为每个变量获取和改变数据范围。对于气流和温度变量:

> (send w :range 0)
(50.0 80.0)
> (send w :range 1)
(17.0 29.0)
结果列表的元素表示该数据范围的高低边界。为了改变一个变量的数据范围,你需要使用两个附加值,新的高低边界。
> (send w :range 1 15 30)
(15.0 30.0)
上式将温度变量的数据范围设置到区间[15, 30]之间。当数据范围改变的时候,图形将重画,除非使用了值为nil的:draw关键字。:range消息的方法是矢量化的。

    get-nice-range函数可用来帮助找到数据范围和刻度的一个好的组合。该函数带3个参数,区间的高、低边界端点和刻度值的整型数值。它返回一个3值列表,表示包含原始区间的区间端点,和接近指定数值得刻度的数值。新值应该产生合理的坐标轴。例如,对于大约有4刻度值的温度变量的数据范围来说:

> (get-nice-range 17 27 4)
(16.0 28.0 7)
对于这个变量推荐的设置范围是[16, 28],表达式如下:
> (send w :range 1 16 28)
(16.0 28.0)
将y轴设置为使用7作为刻度值:
> (send w :y-axis t t 7)
(T T 7)
结果坐标轴标记为16, 18, 20, ..., 28.

    到目前为止,我们的图形只显示了我们的4变量数据集的前两个。:current-variables消息可用来设置获取构造该图形的两个当前变量。默认的情况是显示前两个变量:

> (send w :current-variables)
(0 1)
下边的表达式命令图形切换到使用其它两个变量。该方法向图像发送:redraw消息,除非使用了值为nil的:draw关键字。

练习9.3

略。

清除绘图数据

    图形里当前的数据可以使用:clear, :clear-points和:clear-lines消息来清除。:clear-points消息移除内部数据,并将点数设为0,该消息会重画图形,除非使用了值为nil的:draw关键字。用来清除linestart数据的:clear-lines消息是相同的。:clear消息会将点数据和线数据一并移除。

    这些消息对于动画是非常有用的,即显示在图形里的快速变化的数据动画。鉴于之后第9.1.6节里描述的重画方法使用了双缓冲技术,该技术会产生一个平滑的动画效果。

9.1.3 缩放与变换

    graph-proto原型最主要的特征就是其允许对数据进行线性变换的能力,尤其是旋转变换。变换过程可以分解为两个阶段。第一阶段由数据的中心化和缩放组成;第二阶段由对已经中心化和缩放过的诗句使用一个变化矩阵组成。然后,将显示一个散点图,即该变换的结果的坐标中的两个数据。

缩放和中心化

    缩放和中心化阶段引入了一个新的坐标系统,即缩放坐标系。作为对比,原来指定数据的那个坐标系叫做真实坐标系。可以使用人造的缩放体系,但是有两个叫定缩放和变缩放的标准的缩放体系对大多数情况来说是足够用了。这些缩放类型可以通过使用:adjust-to-data方法和:scale-type方法来实现。在处理之前,我们可以私用下边两个表达式将坐标轴从图形中移除:

> (send w :x-axis nil)
(NIL NIL 4)
> (send w :y-axis nil)
(NIL NIL 4)
对于变换后的数据来说,坐标轴是没有意义的。

    当构造一个新的图形时,它的缩放类型是nil:

> (send w :scale-type)
NIL
这意味着没有进行中心化和缩放,每个维度内的视图范围设置成真实坐标系的对应维度里的数据范围。缩放坐标系里的数据范围可以通过使用:scale-range消息来获取:
> (send w :range 0)
(50.0 80.0)

> (send w :scaled-range 0)
(50.0 80.0)
因为在初始情况下没有进行缩放,缩放过的和真实的数据范围是相等的。

    为了检测这个新的缩放类型,我们可以通过将我们的图形的缩放类型设置为variable符号来检测,表达式如下:

> (send w :scale-type 'variable)
VARIABLE
:scale-type消息的方法将发送:adjust-to-data消息,并重画图形,除非使用了值为nil的:draw关键字。对于一个使用变化的缩放的图形,:adjust-to-data方法为每个变量在数据范围的中心位置进行中心化,在将每个中心化后的变量缩放到[-1, 1]区间上,最后将缩放后的数据范围设置到[-sqrt(m), sqrt(m)]区间。这将确保数据进行旋转之后都在缩放范围之内。在将缩放类型设置成variable之后,图形的第一个维度的数据范围和缩放后的数据范围如下:
> (send w :range 0)
(34.99999999999999 95.0)
> (send w :scaled-range 0)
(-2.0 2.0)
    当变量在可比较的尺度内无法量度的时候,可变缩放是合适的。如果变量在可比较的尺度上是可以量度的,通过在每个维度上使用相同的比例系数,我们可能想要在数据内保持一定的角度,这在固定缩放策略里是一体的。如果尺度类型设置为符号fixed,那么:adjust-to-data方法将在数据区间的中间进行数据的中心化,并为每个维度选择缩放系数为1,然后为每个变量设置其缩放后的区间,该数值为数据中心化的数据最大范围的sqrt(m)倍。

    如果可变缩放和固定缩放体系都不能胜任你的需求,你可以通过定义一个新的:adjudt-to-data方法来定义你自己的缩放体系。该方法可以使用:scale-range方法来设置一个新的缩放范围,再使用:center和:scale消息来设置或者获取中心和缩放系数。对于我们的图形,因为是可变缩放,第一个变量的缩放系数和中心是:

> (send w :scale 0)
15.0
> (send w :center 0)
65.0
当使用:scaled-range消息设置缩放范围时,原始坐标系内的数据范围可会调整以适应该变化。:scale, :center和:scaled-range等消息是矢量化的。当使用该方法设置新值时,将会重画图形,除非使用了值为nil的:draw关键字。

    对于定义自定义的缩放体系来说另一个重要的消息是:visible-range消息。该消息的方法接受维度索引作为参数,返回在那个维度里所有可见的点数据和linestart的数据范围。例如,对于图形里的第一维度:

> (send w :visible-range 0)
(50.0 80.0)
    一个图形的初始缩放类型可以这样指定——在graph-proto原型的:isnew方法里指定:scale-type关键字。

练习 9.4
略。

变换

    在选定一个缩放体系之后,比如说可变缩放,现在我们可以使用变换了。初始情况下,是没有变换的:

> (send w :transformation)
NIL
我们可以通过向:transformation消息传递一个参数,对数据中的每一个点和linestart使用一个转换矩阵。浙江转换每一个数据点,这些数据点被视为一个列向量,通过在该列向量左侧乘一个变换矩阵完成变换。除非使用了值为nil的:draw关键字,否则在给定一个新的变换时:transformation方法将重画该图形。举个例子,如果这样给定当前变量:
> (send w :current-variables)
(0 1)

那么我们可以使用下边这个表达式使用一个旋转变换,该变换使用浓度变量代替气流变量,氨损失代替温度。

> (send w :transformation
        '#2A((0 0 -1 0)
             (0 0 0 -1)
             (1 0 0 0)
             (0 1 0 0)))
#2A((0 0 -1 0) (0 0 0 -1) (1 0 0 0) (0 1 0 0))
下式将返回一个未变换状态的图形:
> (send w :transformation nil)
NIL
    对于变换的使用,一些其它方法也是可用的。为了更容易理解原始图形中的温度相对于气流的点数据,与变换后图形中的氨损失相对于浓度的点数据,这两种点数据之间的对应关系,从第一个图形平滑地旋转到第二个图形时有用的。这样的从第一个散点图到另一个散点图的旋转叫做图形差值。通过将变换矩阵设置成一系列不同的中间旋转变换,你可以执行这个旋转。另一个方法就是使用:apply-transformation消息。该消息的方法接受一个增加的变换矩阵,然后通过使用该增加的变换左乘当前变换来构造一个新的图形变换。默认地,该方法重画图形。因为该重画使用缓冲区,下式将花费10个步骤将原始图形平滑地旋转成为变换后的图形:
> (let* ((c (cos (/ pi 20)))
         (s (sin (/ pi 20)))
         (m (+ (* c (identity-matrix 4))
               (* s '#2A((0 0 -1 0)
                         (0 0 0 -1)
                         (1 0 0 0)
                         (0 1 0 0))))))
    (dotimes (i 10) (send w :apply-transformation m)))
NIL
旋转后的图形数据

    :rotate-2消息可用来使用一个由两个变量索引定义的平面的二维旋转变换。该消息需要一个旋转角度作为第三个参数。例如,下式将当前已经变换后的数据的0维到2维的数据旋转pi/20角度。通过在你的图形里将变换重设为nil,我们可以再表达式里使用:rotate-2消息,目的是将温度对气流图形平滑地变换为氨损失对浓度图形。每步都由两个二维旋转组成。图形仅在第二次旋转之后重画。

    可以对:transformation和:apply-transformation方法传递一个维度k<m的方阵作为参数。这种情况下改变换将作用到当前数据空间的前k维上。这对于一些原型来说是很有用的,比如说继承自graph-proto原型的histogram原型。还可以向:apply-transformation消息里传递一个与矩阵参数相同维度的序列,这里的矩阵参数是带;basis关键字的。该序列应该包含nil和非nil元素。该变换系统会忽略基里值为nil的对应位置处旋转矩阵的元素。该特性允许低维旋转变换可以在高维图形里有效地使用。

练习 9.5
略。

获取转换后的数据

    graph-proto原型提供了用来获取转换后的点数据和linestart坐标数据当前值的消息。例如,第一个转换后的点数据和linestart数据可以这样给出:

> (send w :point-transformed-coordinate 0 0)
-0.6190476190476191
> (send w :point-transformed-coordinate 1 0)
-1.0000000000000002
> (send w :linestart-transformed-coordinate 0 0)
-0.6190476190476191
> (send w :linestart-transformed-coordinate 1 0)
-1.0000000000000002

9.1.4 鼠标事件和鼠标模式

    graph-proto原型的:do-click和:do-motion方法是用来支持这样的语法的——即使图形可以在不同的鼠标模式里。新的模式可以添加进来,你可以通过使用一个由Mouse Mode菜单项产生的对话框,或者向图形发送一个消息,在几个可用模式之间进行切换。鼠标模式可以使用一个Lisp符号来区别。每个鼠标模式都有一个标签字符串,用在用来切换模式的那个对话框里,还有一个光标用来提供当前模式、自身点击和行为动作的可视化暗示。为了自定义一个图形和一个新的原型,通常不需要覆盖:do-click和:do-motion方法。添加新的鼠标模式或者覆盖由标准鼠标模式使用的消息是足够的。

增加新的鼠标模式

    初始情况下,一个图形由两个鼠标模式,选择模式和刷模式。:mouse-modes返回区别可用模式的符号列表:

> (send w :mouse-modes)
(SELECTING BRUSHING)
    :add-mouse-mode消息添加一个新的鼠标模式,或者重定义一个现有的模式。该消息需要一个参数,即鼠标模式符号,还接受一些关键字参数。这些关键字包括用来指定标签字符串的:title,用来指定光标符号的:cusor。:click和:motion关键字可以用来指定消息选择器,当图形在新的模式里的时候该消息选择器用来处理点击和动作事件。

    举个例子,我们可以向图形里添加一个新的模式,该图形仅展示在窗体上点击的鼠标的坐标。该模式可以这样添加:

> (send w :add-mouse-mode 'show-coordinates
        :title "Show Coordinates"
        :click :do-show-coordinates
        :cursor 'finger)
SHOW-COORDINATES
添加了这个模式之后,该图形就有3个可用的模式了:
> (send w :mouse-modes)
(SELECTING BRUSHING SHOW-COORDINATES)
当图形处于show-coordinate模式的时候,光标设成了finger(手指形)。当在该模式里点击鼠标时,图形将向:do-click消息发送:do-show-coordinates消息,该消息带4个参数。因为没有指定动作,动作事件将被忽略。

    在使用该新模式之前,我们需要定义:do-show-coordinates方法。为了避免重画图形,我们可以使用XOR绘图,在按钮按下的时候绘制一个表示点击位置的字符串。当按钮释放的时候,我们可以再次绘制字符串以移除字符串映像:

> (defmeth w :do-show-coordinates (x y m1 m2)
    (let ((s (format nil "~s" (list x y)))
          (mode (send self :draw-mode)))
      (send self :draw-mode 'xor)
      (send self :draw-string s x y)
      (send self :while-button-down #'(lambda (x y) nil))
      (send self :draw-string s x y)
      (send self :draw-mode mode)))
:DO-SHOW-COORDINATES
:while-button-down动作仅仅是等待鼠标按钮弹起。

    现在我们使用图形菜单提供的对话框切换到新的模式,或者向图形发送一个参数为模型符号的:mouse-mode消息,向图形发送一个不带参数的:mouse-mode消息将返回当前模式的符号:

> (send w :mouse-mode 'show-coordinates)
SHOW-COORDINATES
> (send w :mouse-mode)
SHOW-COORDINATES

    我们可以展示缩放后的坐标系统里的当前变量的当前坐标,而不是展示鼠标点击处的图上坐标。:canvas-to-scaled消息带两个整型参数——代表画布上的一个点,返回两个实数——代表缩放后坐标系统的两个当前变量的对应的点:

> (send w :canvas-to-scaled 100 150)
(-0.4 -0.3904382470119522)
:canvas-to-real消息试图转换回是坐标系,但是它仅在图形变换为nil时才合适地工作。为了允许你在显示画布、缩放和真实坐标系,我们可以定义一个:do-show-coordinates消息来检查修饰符,在点击时同时按下shift时显示实坐标,在alt使用时显示缩放坐标。如果没有按下这些扩展修饰符,它仅显示图上坐标:
> (defmeth w :do-show-coordinates (x y m1 m2)
    (let* ((xy (cond (m1 (send self :canvas-to-real x y))
                 (m2 (send self :canvas-to-scaled x y))
                 (t (list x y))))
           (s (format nil "~s" xy))
           (mode (send self :draw-mode)))
      (send self :draw-mode 'xor)
      (send self :draw-string s x y)
      (send self :while-button-down #'(lambda (x y) nil))
      (send self :draw-string s x y)
      (send self :draw-mode mode)))
:DO-SHOW-COORDINATES
    :scaled-to-canvas和:real-to-canvas消息带两个实数参数,表示缩放坐标或实数坐标系中当前变量的坐标,然后返回响应画布坐标的列表。

    为了助于开发新的鼠标模式,一些其它的消息也是可用的。为了说明这些消息中的一些,我们可以构造一些鼠标模式,用于当鼠标在某点附近点击和按下的时候通过在附近放置一个标签来标示该点。对于使所有高亮的或选中的点显示它们的标签这一标准选项,我们提供的方法是对其的替代物。首先,我们可以通过使用以下表达式移除定义的模式:

(send w :delete-mouse-mode 'show-coordinates)
该操作不会移除与模式有联系的鼠标点击或者动作消息。该消息需要每个图形至少有一个鼠标模式。其次,为了识别各点,我们可以制定一个新模式:
(send w :add-mouse-mode 'identify
        :title "Identify"
        :click :do-identify
        :cursor 'finger)
为了确定一次点击足够接近一个点,新模式的点击方法需要设置一个容差值。每个图形都保持一个容差值范围,该范围可以通过:click-range消息设置或获取。该消息返回一个两个整型数的列表,用像素表示的该容差值的宽度和高度。默认范围如下:
> (send w :click-range)
(4 4)
该范围被标准selection模式采用,当鼠标点击的时候用来确定选择的点。可以通过发送带两个整型参数的消息来指定一个新的点击范围,这两个整型参数是新范围的宽度和高度。

    使用范围和鼠标点击位置的坐标,我们可以下个图形发送:points-in-rect消息来获取落在指定矩形区域内的影响的所有点的索引列表。例如:

(send w :points-in-rect 100 150 10 15)
然后,返回位于左上角坐标为(100, 150),宽10像素,高15像素的矩形区域内的影像的所有点的索引列表。如果该该区域内没有点数据,返回nil。

    使用这两个详细,我们的:do-identify消息可以定义成下面的样子:

> (defmeth w :do-identify (x y m1 m2)
    (let* ((cr (send self :click-range))
           (p (first (send self :points-in-rect
                           (- x (round (/ (first cr) 2)))
                           (- y (round (/ (second cr) 2)))
                           (first cr)
                           (second cr)))))
      (if p
          (let ((mode (send self :draw-mode))
                (label (send self :point-label p)))
            (send self :draw-mode 'xor)
            (send self :draw-string label x y)
            (send self :while-button-down #'(lambda (x y) nil))
            (send self :draw-string label x y)
            (send self :draw-mode mode)))))

:points-in-rect消息使用中心点为鼠标点击处的矩形,这里的点击位置的宽度和高度与点击范围内指定的值相等。函数first返回由:points-in-rect消息返回的列表的第一个索引,如果列表为空则返回nil。因此,如果在点击处附近的发现一个点,变量p是一个整数,如果没有这样的一个点,变量p就是nil。

    为了说明另一个消息:drag-point,我们可以通过在图形里移动数据点来定义一个鼠标模式,用来编辑我们的数据。我们可以这样设置新的模式:

(send w :add-mouse-mode 'point-moving
        :title "Point Moving"
        :cursor 'hand
        :click :do-point-moving)
在图形里,:drag-point消息用来拖动一个点,该消息带两个参数,鼠标点击的图上坐标和检测某点是否接近点击位置。如果存在这样的点,将在窗体周围拖拽出一个灰色的长方形直到鼠标按键释放。在此期间,该点的坐标将改变同时图形重画,除非使用了一个值为nil的:draw关键字。新的坐标将使用上边提到的变换消息来计算。对于一个转换后的图形,;drag-point消息试图移动与当前变量相关的平面上的点,并保持正交分量不变。如果变换是正交的,这才会正确地起作用。:drag-point消息的方法返回被拖动的点的索引,如果没有这样的足够接近点击处的点将返回nil。使用:drag-point,我们可以这样为:do-point-moving消息定义一个方法:
(defmeth w :do-point-moving (x y m1 m2)
    (let ((p (send self :drag-point x y)))
      (if p (format t "Point ~d has been moved.~%" p))))
练习 9.6
略。

标准鼠标模式

    初始情况下,由graph-proto原型构造的图形包括两个模式:selecting模式和brushing模式。selecting模式使用arrow光标,仅需要一个点击方法:do-select-click;brushing模式使用brush光标,它的点击消息和动作消息是:do-brush-click和:do-brush-motion。

    针对selecting和brushing模式的点击和动作手势是被用来设计基本的用户接口以针对这些鼠标模式。实际的选择和高亮操作是通过两个其它消息实现的,:unselect-all-points和:adjust-points-in-rect。:unselect-all-points消息不带参数。:adjust-points-in-rect消息需要5个参数,前4个参数矩形的整型坐标值,左上角的两个坐标值,还有矩形的宽和高。第5个参数是点状态符号selected或hilited中的的一个。当发生一个点击事件时,该图形处于selecting模式,:do-select-click方法以下边的方式使用这些消息:

  • 如果点击操作不包括扩展修饰器(即shift或alt键),:unselect-all-points消息发向任意当前选中点集中未被选中的点。
  • 然后,:adjust-points-in-rect消息被发送,其作用范围是以点击点为中心的,由当前点击范围指定的宽和高的矩形区域的坐标范围。第5个参数是selected。这个消息的方法应该会选择指定矩形范围内的所有可视点。
  • 当鼠标按键按下时,一个角固定在鼠标点击处的虚线矩形将拖拽出来并覆盖图形。当鼠标按键释放时,一个带有最终矩形坐标和符号selected作为参数的:adjust-points-in-rect消息将被发送出去。

    当图形处于brushing模式时,:do-brush-click方法首先取消对所有点的选择操作,除非给定了扩展修饰符;然后,该方法将发送一个以当前刷坐标和符号'selected为参数的:adjust-points-in-rect消息。每次鼠标按下并移动的时候,它都会持续发送该消息。:do-brush-motion方法将发送参数为当前刷坐标和符号hilited为参数的消息。当第5个参数是hilited时,该消息的方法有望高亮矩形区域内所有可见的点,然后将矩形外高亮的点取消高亮效果。

    当前刷的位置和大小可以使用:brush消息来获取。刷由4个整数指定:右下角的画布坐标x、y,连接鼠标的角,还有刷的宽度和高度。通过发送一个带四个整数参数(即新坐标)的:brush消息,可以指定新刷的坐标;:resize-brush消息提供了一个对话框来交互地设置新刷的大小,该消息可以通过使用图形菜单里的Resize Brush菜单项来发送给该图形。

    使用:unselect-all-points和:adjust-points-in-rectx消息的协议允许开发新的图形,而不用修改刷模式或选择模式方法自身。例如,在彩色显示器的系统上,我们可以为这两个消息修改这些方法,以确保选中的点着红色,未选中的点着绘图色。在发送下边这条消息以确保使用彩色绘图之后,

(send w :use-color t)
我们可以这样为:unselect-all-points定义消息:

(defmeth w :unselect-all-points ()
    (send self :point-color (iseq (send self :num-points)) nil)
    (call-next-method))
该方法将所有点的颜色设置为nil,然后调用从graph-proto原型继承来的方法。新的:adjust-points-in-rect方法可以这样定义:

(defmeth w :adjust-points-in-rect (x y w h s)
    (if (eq s 'selected)
        (let ((p (send self :points-in-rect x y w h)))
          (if p (send self :point-color p 'red))))
    (call-next-method x y w h s))
变量p包含指定的矩形区域内的点的索引列表,如果该列表不是nil的则金发送:point-color消息。

9.1.5 连接

    图形里的点可能处于一些不同的状态,selecting和brushing操作提供了一种容易的方式来改变图形里点的状态。为了浏览数据集的一些视图之间的关系,Lisp-Stat绘图系统允许图形之间的互连操作。当两个或更多图形连接到一起时,系统将试图保证连接在一起的图形里的点状态时相同的。

    默认的Lisp-Stat连接系统基于一个松散的联合模型,在这个模型里,被连接到一起的不同图形的点通过他们的索引值彼此关联。因此,在一个图形里选择索引值为3的点,同时也会选中与该图形连接的图形里索引值同为3的点。在不存在点的其它属性匹配的意义下,这个联合是松散的。在连接的图形中的点可以有不同的标签、颜色或者符号。其它的连接模式也是可能的,其中一个可替换的连接模式将在10.6节里说明。

确定哪些图形可以连接

    确定哪些图形可以连接到其它图形上的系统依赖:linked和:links两个消息。当向图形发送:links消息时,如果该图形没有被连接则返回nil;如果该图形被连接了,它应该返回一个列表,该类表包含所有连接到该图形的接收了该消息的其它图形,图形自身可能是该列表的一个元素。:linked消息可以以不带参数的形式发送,用来确定该图形是否已经被连接,如果已经被连接了,返回t;否则,返回nil。该消息可以接受一个可选参数,该参数为t就是连接该图形,该参数为nil就是解开连接。

    graph-proto原型的:links和linked方法将维护一个包含已连接图形的单列表,该列表由被连接的图形的:links消息返回,该列表也可以通过使用linked-plots函数好获取,当某个图形被连接时,该图形的点的状态传递到其它被连接的图形,就像由:links消息返回的结果确定的一样。

    因为被连接图形的确认完全基于这两个消息,定义一个可代替的策略是相当简单的。例如,如果我们想确保所有与stack loss data相关的图形都与另一个数据相连而不是与所有的图形都相连,我们可以这样设置一个stack loss图形的列表:

(setf *stack-plots* nil)
然后,给予每个stack图形它们自己的:links和:linked方法,它们这样定义:

(defmeth w :links ()
    (if (member self *stack-plots*) *stack-plots*))

(defmeth w :linked (&optional (link nil set))
    (when set
          (setf *stack-plots*
                (if link
                    (cons self *stack-plots*)
                    (remove self *stack-plots*)))
          (call-next-method)))
避免为每个新图形添加这些方法的一个方式是定义一个独立的stack loss图形原型。

点状态和连接

    图形里的每个点都是四种状态中的一种,对应的符号为invisible, normal, hilited和selected。graph-proto绘图方法可以绘制高亮的点和由点的符号的高亮版本选中的点。处于正常状态的点使用正常符号来绘制,处于invisible状态的点不被绘制。点的状态可以通过:point-state消息来确定和绘制。对于处于stack loss数据图形里的第一个点:

> (send w :point-state 0)
NORMAL
:point-state消息的方法是矢量化的,所以它可以用来确定图形里的所有点的状态,方法如下:
> (send w :point-state (iseq 21))
(NORMAL NORMAL NORMAL NORMAL ...)
    为了改变点的状态,可以向:point-state消息的第二个参数赋值为一个新状态,下边的表达式将第一个点的状态改为selected。当图形里的一个点的状态发生了改变,在所有被连接的图形了对应的点的状态也会发生改变。另外,会向每个图形发送一个带一个参数的:adjust-screen-point方法,该参数是想要改变的那个点的索引值。该方法负责改变屏幕上点的影像。

    :adjust-screen-point方法采取的动作即取决于改点的新状态也取决于改点的上一个状态。当发送:adjust-screen-point消息的时候,:point-state消息返回的值将表达新的状态,点的上一个状态可以通过向:last-point-state消息发送一个参数来获取,该参数是改点的索引值。

    为了在所有连接在一起的图形该处最佳的对应关系,:adjust-screen-ponit方法试图尽可能快地重画点数据,以表达它们之间的任何状态变化。对于有些状态改变来说,这是相当简单的。如果点的状态在normal, hilited和selected这些值之间变化的话,那么:adjust-screen-point方法将使用上一章引入的:replace-symbol方法来改变屏幕上显示的符号。不幸的是,如果点的新状态是不可见的,不重画图形而从屏幕上清晰地移除改点是很困难的。由于每次将一点设置成不可见状态都要进行重画操作是很浪费的,所以:adjust-screen-point方法采取了一个间接的方法:它向图形发送一个标识位来指示是否需要调整以适合窗体,:needs-adjusting消息用来设置和获取这个标识位的值。在将一个点的状态设置为不可见后,该标识位的值为t:

> (send w :needs-adjusting)
NIL

> (send w :point-state 0 'invisible)
INVISIBLE

> (send w :needs-adjusting)
T
在方便的时候,也可以向图形发送:adjust-screen消息,如果需要的话,该消息的方法将检查该标志位、重设它,然后重画图形。因此,在将一个点的状态设置为不可见之后,在对所有需要改变状态的点进行调整之后,:adjust-points-in-rect方法将向所有连接的图形发送:adjust-screen消息。

    作为这些想法的一个简单的展示,我们可以为我们的图形定义一个新的:adjust-screen-point方法,该方法将所有高亮的点和选中的点设置为红色,然后将所有未高亮的点设置为nil:

> (defmeth w :adjust-screen-point (i)
    (let* ((state (send self :point-state i))
           (color (if (member state '(selected hilited)) 'red)))
      (send self :point-color i color))
    (call-next-method i))
该方法与早先我们使用的那个为:adjust-points-in-rect方法修改的方法有些不同,之前的方法,仅当在图形自身内部的点的状态被刷模式或选择模式改变的时候才会着色;而现在的方法可以确保即使是在被连接的图形里由刷模式或选择模式改变状态的点,其颜色也会调整。

练习 9.7
略。

高级状态消息

    对于获取和改变点的状态,还有一些其它消息是可用的。:point-showing, :point-hilited和:point-selected消息需要一个点的索引做参数,然后返回t或nil来分别指示点的状态是否是可见的、高亮的或被选中的;对这些消息的第二个参数赋值为t或nil,将会对其状态做合适的改变。这3个方法都是矢量化的,这3个方法在返回之前都会发送:adjust-screen消息。

    :selection消息返回当前被选中的所有点的列表,如果向该消息传递一个索引列表作为它的可选参数的话,该消息将取消所有选中的点,然后选中参数列表里指定的所有可见的点,他在返回前将发送:adjust-screen消息。:points-selected消息的方法与:selection方法是等效的;:points-hilited和:points-showing的方法也是相同的。

    :erase-selection方法将所有当前选中的点的状态改变成不可见状态,:show-all-points方法将所有点的状态改为正常状态;:focus-on-selection方法将所有未选中的方法设置为不可见状态。这3个方法在返回之前都发送:adjust-screen消息。

    这里有两个有用的谓词消息是可用的。:any-points-selected-p消息的方法在图形里任何一个点被选中时都返回t,否则返回nil。:all-points-showing-p消息在所有点都是可见状态小返回t,任意一个点为不可见状态则返回nil。

9.1.6 窗体布局、缩放和重画

缩放方法

    graph-proto原型的:resize方法基于一定的假设,该假设是关于图形的布局的。图形的内容,点数据与线数据的影像被限制在一个叫做内容矩形的矩形里。如果图形里包含坐标轴,它们将立即在内容矩形的外部被画出来。由内容和坐标轴组成的整个图形被一个边缘包裹起来,这个边缘被用来束缚图形,像标准旋转图形的旋转控制,这个边缘沿着窗体的画布的左侧、上侧、右侧和下侧都有一个固定的像素数的留白。当该图形变换大小的时候,:resize消息会保持边缘不变,然后内容矩形的大小以填充画布的剩余部分。内容矩形的精确的大小和形状依赖于该空间是否需要x和y坐标轴,坐标轴是否拥有标签,内容矩形是否保持固定的方位比例。对于一个固定的方位比例,内容多边形是这样的一个最大矩形,它是能装下边缘的并为坐标轴留下空间的那个最大正方形。如果该图形允许方位比例改变,内容多边形将是满足这个限制的最大的矩形。图9.3针对一个带有坐标轴和固定方位比例的图形的边缘和内容矩形。

图9.3 带有坐标轴和固定方位比例的图形的边缘和内容矩形

内容矩形可以通过使用:content-rect消息来设置和获取。不适用参数的话,该消息将返回内容矩形的坐标的列表(比如说一个包含左上角两个坐标值、宽度和高度的列表):

> (send w :content-rect)
(0 0 250 251)
如果该消息使用四个参数来调用的话,它将会把内容矩形设置成指定的坐标。

    图形的边缘可以使用:margin消息来设置和获取。不适用参数调用,该消息返回一个四个整型数表示的列表,代表边缘距离画布的上侧、顶侧、右侧和下侧的距离的像素值。初始情况下,一个从graph-proto原型创建的图形的边缘,其值都是0:

> (send w :margin)
(0 0 0 0)
为了设置一个新的边缘,可以给:margin消息赋予4个参数。

    图形的方位比例可以使用:fixed-aspect消息确定和设置,对于固定的方位比例该结果是一个非空值,对于一个可变的方位比例该结果返回nil。对于我们的例子,方位比例初始情况下是可变的:

> (send w :fixed-aspect)
NIL
为了改变方位比例,你可以向该消息发送一个nil和一个非空的参数,以下表达式给予我们的图形一个固定的方位比例。

    如果使用:margin, :fixed-aspect, x-axis 或 :y-axis消息,布局特征的任意一个发生改变的话,那么这些消息的方法将向图形发送:resize和:redraw消息,除非:draw关键字被赋予了值为nil的参数。

    在返回之前,:resize方法将向图形发送:redraw-overlays消息,这里的叠置(Overlays)将在9.1.7节里描述,它提供方便的方法来实现图形控制,比如对旋转图形的旋转控制。

重画方法

    绘图原型的重画方法:redraw将重画过程打断成几个阶段。首先,它发送:redraw-backgroud消息,该消息的方法将擦除画布,然后当前图形设置需要的所有坐标轴。下一步,:redraw消息向图形发送:redraw-overlays消息。最后,向图形发送:redraw-content消息,该消息对应的方法负责点数据和线数据自身。:redraw, :redraw-content和:redraw-backgroud消息对应的方法都使用了缓冲区。

    将背景绘制与内容绘制分离的原因是,绘制坐标轴和叠置操作可能很耗时,并且仅有内容改变时这是不需要的。结果,像:adjust-screen和:rotate-2这样的消息对应的方法仅需要向图形发送:redraw-content消息以改变图形的内容。

    开发新图形时,很少要覆盖:graph-proto的:redraw方法,通常写一个新的:redraw-content方法就最够了。

下层消息

    为了有助于绘图方法,graph-proto原型转换系统保持将待转换的点数据和线数据坐标的整型版本(即转换后的坐标数据为整型值)。整型数的范围,即所谓的画布范围,与每个变量都有关系。该系统将被变换坐标值从被缩放的比例缩放到画布的比例。那么如果一个变量的范围是从0到200,那么在-1.0到1.0范围内坐标值为0.5的坐标对应的画布坐标是150。

    变量的画布范围可以使用:canvas-range消息进行获取和设置。对于我们的事例图形里的第一个变量,它的范围就是:

> (send w :canvas-range 0)
(0 232)

对于当前变量的第一个,:resize方法将画布范围设置为从0到内容矩形宽度这样的范围,对于所有其它变量来说,该范围被设置到从0到内容多边形高度这一范围中。

    点数据和直线中的点数据的画布坐标可以使用:point-canvas-coordinate和:linestart-canvas-coordinate消息来获取。这些消息可以向我们在上边介绍的其它的坐标消息一样来使用。第一个点的前两个变量的画布坐标可以这样获取:

> (send w :point-canvas-coordinate 0 0)
80

> (send w :point-canvas-coordinate 1 0)
58
    绘图系统还会使用一个在内容矩形里的叫做内容原点的点,该原点可以使用:content-origin消息来设置和获取:
> (send w :content-origin)
(9 250)
:resize方法将该原点放置在内容矩形的左上角稍低点的位置。:redraw-content方法使用画布坐标作为偏移量。

    在这个上下文里使用的一个终极消息是:content-variables,该消息对应的方法与:current-variables方法等价,除非在这样的情况下——当该变量变化时它没有重画图形。

9.1.7 图形叠置

    叠置可以被视为一张透明的纸,这张纸在内容绘制之前放置在图形的背景上,这些纸保持一定的顺序,并从下向上绘制。为了不被图形的内容覆盖,叠置层通常只在图形的边缘里绘制。

    图形叠置的一个关键的特性是它们的拦截或者说是过滤鼠标点击的能力。graph-proto原型的:do-click方法允许每个叠置层,从顶层逐层向下,来接收或者传递一个鼠标点击事件。如果该点击事件未被任何叠置层接收,那么它仅能传递到当前鼠标模式上。该特性使使用叠置层来实现简单的图形控制成为可能,比如让某个图形执行特殊的动作。对于旋转图形的旋转操作就是以叠置层实现的。

    图形叠置层是一个对象,它继承自graph-overlay-proto原型。通过向图形发送带一个参数的:add-overlay消息(该参数是叠置层对象)来向一个图形里添加叠置层。该操作将新的叠置层置于所有其它叠置层之上;通过向图形发送带一个参数的:delete-overlay消息来移除叠置层(该参数是要移除的叠置层对象)。

    当图形放缩的时候,:resize方法在返回之前向图形发送:resize-overlays消息,该消息对应的方法向图形里的每个叠置层发送:resize消息。当图形重画时,:redraw方法向图形发送:redraw-overlays消息,该消息对应的方法向每个叠置层发送:redraw消息,从最底层的叠置层开始一直向上直到叠置堆栈的最顶端。

    graph-proto :do-click方法向叠置层发送:do-click消息,该消息带它接受到的一些参数,该过程从最上边的叠置层开始,持续穿过其它叠置层,直到返回一个非nil值作为点击的结果。如果所有的叠置层对点击动作的反应都返回nil,那么对于当前的鼠标模式该click事件将被传递给click方法。

    举个例子,我们可以向我们的氨气损耗数据图形里添加一个叠置层,那个图形包含一个按钮的和一个跟着一个标签的正方形。当在正方形里发生点击事件时,图形将接受指令运行一个平滑差值算法,该算法从温度对气流的图形到氨气损失到浓度的图形。为了避免图形缩放中需要重新定位按钮,我们可以将它放置在图形的左上角位置。

> (let ((h (+ (send w :text-ascent) (send w :text-descent))))
     (send w :margin 0 (round (* 1.5 h)) 0 0))
    上式设置了图形上边距,目的是为窗体的字体里的一行文本留下足够的空间。通过向叠置层原型发送:new-message消息,我们可以构造叠置层对象:
> (setf interp-overlay (send graph-overlay-proto :new))

接下来,我们可以给予该叠置层一个槽,用来容纳描述按钮容器和标签字符串的位置的信息。

> (let* ((ascent (send w :text-ascent))
         (x ascent)
         (y (round (* 1.5 ascent)))
         (box ascent))
     (send interp-overlay :add-slot 'location
           (list x y box (round (+ x (* 1.5 box))))))
然后,我们可以为该槽定义一个读取方法:
> (defmeth interp-overlay :location () (slot-value 'location))
    现在可以定义该叠置层的:redraw方法来获取位置信息,这里使用:location消息,然后在指定位置绘制一个盒子容器和一个标签:
> (defmeth interp-overlay :redraw ()
    (let* ((loc (send self :location))
           (x (first loc))
           (y (second loc))
           (box (third loc))
           (string-x (fourth loc))
           (graph (send self :graph)))
      (send graph :frame-rect x (- y box) box box)
      (send graph :draw-string "Interpolate" string-x y)))
该方法向叠置层发送了:graph消息,目的是某图形是否含有某叠置层。当某叠置层安装到一个图形以后,该叠置层系统将确保该消息返回合适的结果。

    :do-click消息将返回nil,除非点击落在按钮的盒子容器的内部;如果点击发生在盒子容器里,该方法将向图形发送:interpolate消息然后返回t,进一步确保点击事件没有被做进一步处理:

> (defmeth interp-overlay :do-click (x y m1 m2)
    (let* ((loc (send self :location))
           (box (third loc))
           (left (first loc))
           (top (- (second loc) box))
           (right (+ left box))
           (botton (+ top box))
           (graph (send self :graph)))
      (when (and (< left x right) (< top y bottom))
            (send graph :interpolate)
            t)))
为了完成这个例子,我们需要为我们的图形定义一个:interpolate方法。可以基于9.1.3节的平滑旋转循环,做一个简单的定义,例如:

图 9.4 叠置层里带差值按钮的烟雾损失数据图形

> (defmeth w :interpolate ()
    (send self :transformation nil)
    (dotimes (i 10)
             (send self :rotate-2 0 2 (/pi 20) :draw nil)
             (send self :rotate-2 1 3 (/ pi 20))))
最后,通过使用下式将叠置层安装到我们的图形里:
> (send w :add-overlay interp-overlay)
结果见图9.4,图形内容部分发生的点击像以前一样被处理了,但是在按钮里的点击引起图形运行了差值算法。

    待使用叠置层试验之后,我们可以使用以下表达式来移除该叠置层:

> (send w :delete-overlay interp-overlay)
练习9.8

练习9.9

练习9.10

9.1.8 菜单和菜单项

    graph-proto原型的:isnew方法可以通过向图形对象发送:new-menu消息来添加一个菜单,该消息对应的方法按顺序向对象发送:menu-title和:menu-template消息。:menu-title消息返回一个标题字符串:

> (send w :menu-title)
"Plot"
在构建新的图形菜单的时候,该消息可以用来安装一个新的菜单标题,在给予一个新的原型一个更合适的菜单标题上,这是非常有用的。

    :menu-template方法预期会返回一个列表,该列表用来构建菜单项,该列表可以包含菜单项对象,这些菜单项对象可以简单地安装在菜单里,或者也可能包含一些用来指定标准菜单项的符号。该消息的graph-proto原型对应的方法返回一个符号列表:

> (send w :menu-templates)
(LINK SHOWING-LABELS MOUSE REIZE-BRUSH ...)
  • :new-ment方法为这些符号里的每个符号都构造了一个合适的菜单。下边的符号可以用来指定标准菜单项:
  • color——发送:set-sselection-color消息,该消息会显示一个对话框,用来改变选中的点的颜色。
  • dash——一个处于不可用状态的分隔符。
  • focus-on-selection——发送:focus-on-selection消息。
  • link——使用:linked消息切换图形的链接。
  • mouse——发送:choose-mouse-mode消息,该消息显示一个对话框,用来改变鼠标模式。
  • option——向图形发送:set-options消息,该消息显示一个对话框,用来设置一些选项。
  • redraw——发送:redraw消息。
  • erase-selection——发送:erase-selection消息。
  • rescale——发送:adjust-to-data消息。
  • save-image——发送:ask-save-image消息,该消息显示一个对话框,用来将图像保存成文件。
  • selection——发送:selection-dialog消息。
  • show-all——发送:show-all-points消息。
  • show-labels——使用:showing-labels消息切换显示标签。
  • symbol——发送:set-selection-symbol消息,该消息显示一个对话框,用来改变选中的点的符号。

这些菜单项的一些使用:any-points-selected-p或者:all-points-showing-p来确定他们是否处于可用状态(使能)。

    :new-menu消息还可以被赋予一个可替代的标题字符串参数,该参数用来代替:menu-title消息的结果。菜单项的可选列表可以通过使用:items关键字来使用,该列表可以包含上边列出的符号中的任意多个,目的是指定标准菜单项。例如:

> (send w :new-menu "Stack Loss" :items '(link dash rescale))
该表达式赋予stack loss图形一个菜单,它仅包括link项和rescale项,中间用一条直线分开。可替代的方法就是去定义一个新的:menu-template方法,然后发送:new-menu不带:items关键字的消息给它。对于开发一个新的原型是很有用的。

9.1.9 杂项消息

    还有一些其它消息在graph-proto原型里是可用的。:showing-labels消息用于设置和获取标签选项的状态,如果该值为非nil值,绘图方法将把它放置于高点的选中的点数据附近。

    有些方法是专为2维图形设计的。add-function消息接受一个带一个实数参数的函数,代表一个区间的高低界限的两个实数,还有一个指定一定数量点数据的关键字参数。该消息对应的方法构造了一个网格,并在该网格上计算函数,然后将对应的线集合添加到图形上。它也接受关键字参数:color和:type,用来指定颜色和线型。该图形将被设置:redraw-content消息,除非使用了参数为nil的:draw关键字。
    对于线性函数,:abline消息带两个参数,一个截矩和一个斜率,然后将该直线加到图形上。该独立变量的范围就是x轴的当前范围。
    :adjust-depth-cuing消息主要由旋转图形来使用,但是它也可以被其它图形使用,它将整数维的索引作为参数,对于那些维度使用画布坐标(它们被视为超出屏幕的坐标),目的是为图形里的点数据和线数据使用深度标识。通过将点数据的符号改变为符号dot1,dot2,dot3和dot4中的一个,该点数据是深度标识的。符号dot1是为离视点最远的点使用的,dot4用于里视点最近的点。通过改变线的宽度也可以达到显得深度标识效果。

9.2 一些标准绘图原型

    第二章里介绍的那5个标准图形类型,它们是使用从graph-proto原型继承来的原型构造的。尽管这些图形在外观上相差很大,但是它们仅需要向graph-proto提供的原型,覆盖和增加少量的方法。

9.2.1 散点图

    基础散点图原型scatterplot-proto用于构造一类图形,即由plot-points和plot-lines函数构造的图形。它至少需要二个维度的数据。针对:add-points和:add-lines消息的二维散点图,允许这样指定新数据——使用两个分离的列表代替所需的列表的列表。那样的话,如果s是一个二维散点图,x和y是等长度的数值型列表,那么下边的两个语句是等价的:

> (send s :add-points x y)
> (send s :add-points (list x y))
    如果图形的尺度类型是nil的话,:adjust-to-data方法向图形里添加一个坐标轴,它还会将坐标范围设置到坐标轴上产生好看的坐标刻度。

9.2.2 散点图矩阵

    散点图矩阵原型scatmat-proto至少需要2个维度的数据,它使用一个固定的长宽比例,它的内容的原点坐标在内容矩形的左下角,每个变量在画布上的范围设置在一定范围内——子图的列的左右范围,从原点开始量度。通过修改:do-click和:do-motion方法,将内容变量设置为包含光标的那个子图的索引上,这个操作允许很多继承它的方法可以不经修改就正常工作。

    与:add-lines, :add-points, :adjust-screen-point和:redraw-content消息相对的方法,通过修改它们可以让矩阵里的所有子图形合适地绘制。如果图形的尺度类型是nil,默认地,:redraw-content方法还可以在对角图里绘制变量标签和界限。

9.2.3 旋转图形

    由spin-plot函数产生的,用来旋转图形的原型就是spin-proto,该原型至少需要三个维度的数据,它使用一个固定的横宽比数据,其默认的尺度类型是variable。:content-variables方法使用3个内容变量索引,前两个以常规方式使用,第三个变量是深度提示变量的索引。:resize方法将内容的原点放置于内容矩形的中间。

    :redraw-content方法在进行标准内容重画之前调整深度提示。如果图形要显示坐标轴,坐标轴可以通过调用:redraw-content方法来重画,该方法可以通过向图形对象发送:draw-axes消息来构造。如果尺度类型为nil,:adjust-to-data方法将调整图形的范围,以确保所有的点在围绕原点所做的旋转过程中均是可见的。:depth-cuing和:showing-axes消息可以用来确定一个图形时使用深度提示还是显示坐标轴。通过发送一个值为nil或非nil的参数,这两个消息可用来图形对应的状态。

    旋转图形下边的控件可以以一个叠置层的形式实现,该叠置层由:isnew方法加入。这些按钮将旋转的类型设置为pitching、rolling或yawing,调整角度符号,然后在鼠标按下时发送:rotate消息,:do-idle方法也发送该消息。旋转图形里的当前角度可以通过使用:angle消息来设置和获取。角度以度计量。旋转类型可以通过使用:rotation-type消息设置和获取,它的值可能是符号pitching, rolling或者yawing中的一个,也可能是一个以渐进式旋转来使用的旋转矩阵。如果旋转类型是一个符号,:rotate方法使用:rotate-2方法(使用合适的变量和角度作为其参数);如果该类型是一个矩阵,:rotate使用:apply-transformation。

    除了添加控件叠置层,针对该原型:isnew方法将图形的横宽比设置为固定值,并为叠置层设置一个合适的边缘。

    针对旋转图形原型的:add-function方法是为三维图形设计的,它需要一些参数:一个两个参数(x轴限值和y轴限值)的函数,和四个实数数值。:abcplane消息有三个实数参数a,b和c,然后增加线段来显示函数f(x,y)=a+bx+cy的一段。

9.2.4 直方图

    直方图原型是histogram-proto,直方图在一个m+1维的图形里显示m维的点数据,剩余的维度用于图形的y坐标,第一个维度被扔掉,作为直方图来显示,例如,下边的表达式针对stack loss 数据构造了一个直方图:

> (setf hs (histogram (list air temp conc loss)))
该直方图变量的数量是5:
> (send hs :num-variables)
5
内容变量是:
> (send hs :content-variables)
(0 4)
那么,初始情况下该图形显示的是数据集里第一个变量——气流的直方图。如果对图形hs使用了变换,它将显示转换后的数据的第一维度数据的直方图。例如,在为们为图形的尺度类型设置为variable之后:
> (send hs :scale-type 'variable)
下边的表达式将在10步之内旋转成温度变量的直方图。

    对于一维数据的直方图,可以给予:add-points方法一个数值序列而不是一个数值序列的列表;对于一个m维数据的直方图,:add-points方法需要一个m个序列的列表。那么stack loss数据可以使用下式来安装:

> (send hs :add-points (list air temp conc loss))
    :add-lines消息相对应的方法(它也可能用于向一个直方图里添加一个密度线),它需要一个包含m+1个序列的列表。最后一个序列用来作为y坐标轴。例如,如果hp是一个一维的降水量数据的直方图(该数据在第2.2.1节里),它可以这样构造:
> (setf hp (histogram precipitation))
那么下边的表达式将一个正常浓度的图形添加到直方图里。

    因为:transfomation和:apply-transformation消息相对应的方法可以接受一个维度低于图形维度矩阵,你也可以使用相同维度的变换作为直方图里的点数据。

    针对:adjust-screen, :adjust-screen-point, :redraw-content和:adjust-points-in-rect这些消息,直方图原型提供了它自己的方法。如果尺度类型为nil,:adjust-to-data犯法增加一个x轴,该方法也会重新计算直方图里的矩形条块,与:clear-points和:resize相对应的方法也会修改以适应这些矩形条块。

    有两个消息提供了对这些矩形条块的信息访问能力。:bin-count消息返回每一个矩形条块的数量的列表;不带参数的:num-bins消息返回当前用到的矩形条块的数量,如果给它传递一个正整数来调用的话,它将矩形条块的数量给位指定的数量,当指定了一个行的矩形条块的数量的时候,同时向图形对象发送:redraw消息,除非关键字参数:draw接受了值为nil的参数。

9.2.5 名称列表

    最后一个标准绘图原型是name-list-proto,它的目的是提供一个点标签的可连接的列表,它不适用任何数值数据。

    名称列表可以使用一个维度数m=0的数字来构造。该原型的:add-points方法可以接受一个数值来指定添加的点的数量,用来替代坐标值列表的列表。例如,针对stack loss数据的名称列表可以这样构造:

> (setf n (send name-list-proto :new 0))
拥有21个标签的集合和以使用下式来添加:
> (send n :add-points 21)
    :add-lines方法被重定义,目的是不要做任何事,因为名称列表不需要显示线数据。:adjust-screen, :adjust-screen-point, :redraw-content和:adjust-points-in-rect方法也会被重定义。:isnew方法名称列表拥有一个垂直滚动条。
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!