;;;
;;; plotxy.lisp
;;;

;;; A Garnet program for plotting points in XY coordinates.  Demos appear at the
;;; end of the file.

;;; Ported by Jim Davis 4 Aug 92 from some code I wrote for the Lisp Machine long ago.

(in-package :clwin)

(use-package :kr)

(export
 '(
   ;; aggregates defined here, you might build upon them
   coordinate-transform-window		;window with coord xforms
   graphing-window			;further specialized for graphing a function
   plotting-axis			;axes for plotting
   horizontal-ticks plotting-x-axis plotting-y-axis
   graphing-aggregate			;contains axes and graphing-window

   plot-points				;standalone function for simple plotting
   line-connected-data-points

   coordinate-transform-function
   coordinate-transform-inverse

   ))

; This window allows you to work in XY coordinates where the origin is
; in the lower left corner, and X increases to the right and Y
; increases upwards, in other words the more familiar graphing
; coordinates.  It defines a pair of functions x-to-u and y-to-v which
; take X and Y (user) coordinates into U and V pixel (window)
; coordinates, applying scaling and translation.  You may specify the
; XY origin to be anywhere (in pixels, relative to upper left corner)

; There are two KLUDGES here.  Maybe someone can tell me how to fix them?
; You will notice them below.  The kludges are :TRANSFORMS-CHANGED and
; the UPDATE method for CLWIN windows.

(defun coordinate-transform-function (agg origin scale)
  "Return a function which takes w (user coordinates) into window coordinates"
  (let ((agg agg))
    (function (lambda (w) (+ (g-value agg origin)
			     (truncate (* w (g-value agg scale))))))))

(defun coordinate-transform-inverse (agg origin scale)
   (let ((agg agg))
     (function (lambda (p) (/ (- p (g-value agg origin)) (g-value agg scale))))))

; This is a window so that clipping can happen
(create-instance 'coordinate-transform-window inter:interactor-window
  (:omit-title-bar-p t)
  (:visible #f (not (null (gvl :aggregate))))
  (:origin-x 0)				;in pixels, relative to upper left
  (:origin-y #f (gvl :height))		;default places origin in lower left corner

  ;;  scale factors in pixels/coordinate
  (:scale 10)
  (:xscale #f (gvl :scale))
  (:yscale #f (- (gvl :scale)))		;so that Y goes up,

  ;; Coordinate transform:  Take XY (user coordinates) to UV (window pixel coords)

  (:x-to-u #f (coordinate-transform-function (gv :self) :origin-x :xscale))
  (:y-to-v #f (coordinate-transform-function (gv :self) :origin-y :yscale))

  (:u-to-x #f (coordinate-transform-inverse (gv :self) :origin-x :xscale))
  (:v-to-y #f (coordinate-transform-inverse (gv :self) :origin-y :yscale))
  )

; these functions can be used only by objects within a
; coordinate-transform-window

(export '(x-to-u y-to-v u-to-x v-to-y))

(defun x-to-u (x)
  (funcall (gvl :window :x-to-u) x))

(defun y-to-v (y)
  (funcall (gvl :window :y-to-v) y))

(defun u-to-x (u)
  (funcall (gvl :window :u-to-x) u))

(defun v-to-y (v)
  (funcall (gvl :window :v-to-y) v))



;; Polyline drawn through datapoints.

; This must be used within a window which defines :x-to-u and :y-to-v.

(create-instance 'line-connected-data-points opal:polyline
  (:x-function #'first)			;extract X from an ITEM
  (:y-function #'second)
  (:data-points nil)
  (:point-list
   #f (mapcan (function (lambda (point)
			  (list (x-to-u (funcall (gvl :x-function) point))
				(y-to-v (funcall (gvl :y-function) point)))))
	      (gvl :data-points))))


; Unfortunately, the formula for the point-list does not record the
; dependency on x-to-u (or y-to-v), and so if x-to-u changes it does not
; automatically recompute the point list.   So instead I need to explicitly
; tell the graphic object to recompute itself.

(define-method :transforms-changed line-connected-data-points (self)
  (recompute-formula self :point-list))

; recursively recompute all transforms within self.
(define-method :transforms-changed opal:aggregate (self)
  (opal:do-components self
    #'(lambda (component)
	(kr-send component :transforms-changed component))))

;; After changing scale factor, must cause all objects to redisplay.

(define-method :set-scale coordinate-transform-window (self scale)
  (s-value self :scale scale)
  (kr-send (g-value self :aggregate) :transforms-changed (g-value self :aggregate))
  (opal:update self))

(define-method :set-scale-y coordinate-transform-window (self yscale)
  (s-value self :yscale yscale)
  (kr-send (g-value self :aggregate) :transforms-changed (g-value self :aggregate))
  (opal:update self))

(define-method :set-scale-x coordinate-transform-window (self xscale)
  (s-value self :xscale xscale)
  (kr-send (g-value self :aggregate) :transforms-changed (g-value self :aggregate))
  (opal:update self))

(define-method :set-origin coordinate-transform-window (window x0 y0)
  (s-value window :origin-x x0)
  (s-value window :origin-y y0)
  (kr-send (g-value window :aggregate) :transforms-changed (g-value window :aggregate))
  (opal:update window))



#|  ; test and example code for coordinate-transform window

(create-instance 'w1 coordinate-transform-window
  (:xscale (/ 400 20))
  (:yscale -100)
  (:left 700)
  (:origin-y #f (truncate (gvl :height) 2))
  (:top 200)
  (:width 400)
  (:height 400)
  (:aggregate (create-instance nil opal:aggregate)))

(define-method :show-points w1 (self list)
  (opal:add-component (g-value self :aggregate)
		      (create-instance nil line-connected-data-points 
			  (:data-points list))))

(defun sine-points (&key (res 36)
			 (freq 1)
			 (amplitude 1)
			 (cycles 2))
  (do* ((inc (/ (* 2 pi) res))
	(i 0 (+ i inc))
	(limit (* 2 pi cycles))
	(list nil))
      ((> i limit)
       (reverse list))
    (push (list i (* amplitude (sin (* freq i)))) list)))

(kr-send w1 :show-points w1 (sine-points))

(opal:update w1)

(create-instance 'click-xy inter:two-point-interactor
  (:window w1)
  (:operates-on w1)
  (:start-where #f `(:in ,(gvl :operates-on)))
  (:start-event :leftdown)
  (:continuous nil)
  (:final-function 
   #'(lambda (interactor pl)
       (format t "~&~D,~D -> ~,2f,~,2f~%"
	       (first pl)
	       (second pl)
	       (funcall (g-value interactor :window :u-to-x) (first pl))
	       (funcall (g-value interactor :window :v-to-y) (second pl)))))
  )

(opal:destroy click-xy)

;; add a second set of points to the display

(kr-send w1 :show-points w1 (sine-points :amplitude .5 :freq 2))

(kr-send w1 :set-scale-y w1 -50)

(kr-send w1 :set-scale-y w1 -100)

(g-value w1 :xscale)

(kr-send w1 :set-scale-x w1 40)

(kr-send w1 :set-scale-x w1 -100)


;; This code is to help me figure out why the formula's dependencies dont work

(setf p (first (g-value (g-value w1 :aggregate :components))))

; changing xscale won't cause an update
(s-value w1 :xscale 40)
(opal:update w1)

; changing DATA POINTS is sufficient to cause an update.
(s-value p :data-points (cons (car (g-value p :data-points))
			      (cdr (g-value p :data-points))))

; changing x-to-u is sufficient, also.
(s-value w1 :x-to-u (g-value w1 :x-to-u))

(opal:destroy w1)

|#

#|
;; THIS IS OUT OF DATE.  MAYBE I WILL REVIVE IT SOMEDAY

; Coordinate Transform which chooses scale so as to display all the
; items in a list.  It chooses scales for x and y to completely fill
; the graph.  If you want different ranges, then specify explicit
; x-min and x-max (etc) which will change the way the auto-scaling is
; done.

; Data points can be in ANY format, you must provide a function
; which extracts the X and Y from the format.  By default points are
; stored as a list of (X Y), so the required functions are just
; #'first and #'second but if e.g. your data is in a structure you can
; supply the required accessors.

; to do: implement autoscale-p NIL to turn off autoscaling.

(create-instance 'autoscale-coordinate-transform coordinate-transform
  (:items nil)
  (:autoscale-p t)
  (:x-function #'first)			;extract X from an ITEM
  (:y-function #'second)
  ;; extremes of X and Y
  (:x-min #f (extreme-value (gvl :items) :key (gvl :x-function) :test #'<))
  (:x-max #f (extreme-value (gvl :items) :key (gvl :x-function) :test #'>))
  (:y-min #f (extreme-value (gvl :items) :key (gvl :y-function) :test #'<))
  (:y-max #f (extreme-value (gvl :items) :key (gvl :y-function) :test #'>))
  ;; ranges of X and Y
  (:dx #f (- (gvl :x-max) (gvl :x-min)))
  (:dy #f (- (gvl :y-max) (gvl :y-min)))
  ;; scale factor chosen to show all X and Y values across full width.
  (:umax #f (gvl :width))
  (:vmax #f 0)
  (:xscale #f (/ (- (gvl :umax) (gvl :origin-x)) (gvl :dx)))
  (:yscale #f (/ (- (gvl :vmax) (gvl :origin-y)) (gvl :dy)))
  )

(defun extreme-value (sequence &key (key 'identity) (test #'>))
  (let ((val nil))
    (map nil #'(lambda (elt)
		 (when (and (funcall key elt)
			    (or (null val)
				(funcall test (funcall key elt) val)))
		   (setf val (funcall key elt))))
	 sequence)
    val))

|#


;; Plotting points in a window with axes.

; The GRAPHING-AGGREGATE holds a pair of axes and a subwindow (graphing-window).
; The axes have ticks and labels.
; Here, W means either X or Y (for horizontal axis, it is X, for vertical, Y)
; W-TO-LABEL is a function from a W to a string for a tick
; mark at that position.  The default function just calls FORMAT with
; the value of :TICK-FORMAT, which defaults to a string which displays W 
; as a floating point.

; If you want plain integer labels, change tick-format to "~D".
; If you want nominal labels instead, make w-to-label a function like
;   (lambda (x) (nth (round x) '("A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K"))))

; it would be nice if the Y axis text ran vertically.  It would be
; nice to have more flexible control over placement of axes.  For
; instance, you might want the axes drawn at 0,0 even if it's off the
; screen.

; The parent of the tick mark is a tick-list.  The parent of the
; tick-list is an axis.  The parent of the axis is the graph
; aggregate.  The graph aggregate specifies the actual position of the
; axes.

(create-instance 'TICK-MARK opal:aggregadget
  (:parent nil)				;an AXIS
  (:horizontal-p #f (gvl :parent :horizontal-p))
  (:rank 0)
  (:length #f (gvl :parent :tick-size))
  (:u1 #f (if (gvl :horizontal-p)
	    (+ (gvl :parent :x1)
	       (* (gvl :rank)
		  (gvl :parent :tick-separation)))
	    (gvl :parent :x1)))
  (:v1 #f (if (gvl :horizontal-p)
	    (gvl :parent :y1)
	    (+ (gvl :parent :y1)
	       (* (gvl :rank)
		  (gvl :parent :tick-separation)))))
  (:u2 #f (if (gvl :horizontal-p)
	    (gvl :u1)
	    (- (gvl :u1) (gvl :length))))
  (:v2 #f (if (gvl :horizontal-p)
	    (+  (gvl :v1) (gvl :length))
	    (gvl :v1)))
  (:value #f (if (gvl :horizontal-p)
	       (funcall (gvl :parent :graph :u-to-x)
			(- (gvl :u1) (gvl :parent :x1)))
	       (funcall (gvl :parent :graph :v-to-y)
			(- (gvl :v1) (gvl :parent :parent :y2)))))
  (:w-to-label #f (gvl :parent :w-to-label))
  (:tick-format #f (gvl :parent :tick-format))
  (:label-string #f (funcall (gvl :w-to-label)
			     (gvl :value)))
  (:parts
   `(
     (:line ,opal:line
	    (:x1 ,#f (gvl :parent :u1))
	    (:y1 ,#f (gvl :parent :v1))
	    (:x2 ,#f (gvl :parent :u2))
	    (:y2 ,#f (gvl :parent :v2)))
     (:label ,opal:text
	     (:string ,#f (gvl :parent :label-string))
	     (:left ,#f (if (gvl :parent :horizontal-p)
			  (gvl :parent :u1)
			  (- (gvl :parent :u2)
			     (gvl :width)
			     2)))
	     (:top ,#f (if (gvl :parent :horizontal-p)
			 (gvl :parent :v2)
			 (gvl :parent :v2))))
     )))

(create-instance 'tick-list opal:aggregate
  (:horizontal-p #f (gvl :parent :horizontal-p))
  (:x1 #f (gvl :parent :x1))
  (:y1 #f (gvl :parent :y1))
  (:x2 #f (if (gvl :horizontal-p)
	    (gvl :parent :x2)
	    (gvl :parent :x1)))
  (:y2 #f (if (gvl :horizontal-p)
	    (gvl :parent :y1)
	    (gvl :parent :y2)))
  (:delta  #f (if (gvl :horizontal-p)
		    (- (gvl :x2) (gvl :x1))
		    (- (gvl :y2) (gvl :y1))))
  (:tick-separation #f (if (zerop (gvl :tick-count))
			 0
			 (truncate (gvl :delta)
				   (gvl :tick-count))))
  (:ticks nil)
  (:tick-size #f (gvl :parent :tick-size))
  (:tick-count #f (gvl :parent :tick-count))
  (:tick-format #f (gvl :parent :tick-format))
  (:w-to-label #f (gvl :parent :w-to-label))
  (:graph #f (gvl :parent :graph))
  )

(define-method :initialize tick-list (self)
  (kr-send self :create-tick-marks self)
  (call-prototype-method self))

(define-method :create-tick-marks tick-list (self)
  (let ((list nil))
    (dotimes (i (g-value self :tick-count))
      (push (create-instance nil tick-mark
	      (:rank i))
	    list))
    (s-value self :ticks (nreverse list))
    (dolist (tick  (g-value self :ticks))
      (opal:add-component self tick))))
			       



; This axis is positioned relative to corner of the window.
(create-instance 'plotting-axis opal:aggregadget
  (:horizontal-p nil)
  (:label-string "label")
  (:tick-size 10)
  (:tick-count 0)
  (:w-to-label #f				;function from X or Y value to string
	       #'(lambda (value)
		   (format nil "~@?" (gvl :tick-format) value)))
  (:tick-format "~,2F")			;how to display values (for format)

  (:graph #f (gvl :parent :graph-subwindow))

  (:x1 #f (gvl :parent :graph-left))	;these are garnet coordinates
  (:y1 #f (gvl :parent :graph-bottom))
  ;; upper right
  (:x2 #f (gvl :parent :graph-right))
  (:y2 #f (gvl :parent :graph-top))

  (:parts
   `(
     (:line ,opal:line
	    (:x1 ,#f (gvl :parent :x1))
	    (:y1 ,#f (gvl :parent :y1))
	    ;; other endpoint is determined by specialization
	    )
     (:ticks ,tick-list)
     (:label ,opal:text
	     (:string ,#f (gvl :parent :label-string)) )
     ))
  )

;; X and Y axes are specializations to obtain tick format etc
;; from the parent aggregate.
(create-instance 'plotting-x-axis plotting-axis
  (:horizontal-p t)
  (:parts
   `(
     (:line :modify
	    (:x2 ,#f  (gvl :parent :x2))
	    (:y2 ,#f  (gvl :parent :y1)))
     :ticks
     (:label :modify
	     (:top ,#f (+ (gvl :parent :y1) 5))
	     (:left ,#f (truncate (gvl :parent :x2) 2)))
     )))

(create-instance 'plotting-y-axis plotting-axis
  (:horizontal-p nil)
  (:parts
   `(
     (:line :modify
	    (:x2 ,#f  (gvl :parent :x1))
	    (:y2 ,#f  (gvl :parent :y2)))
     :ticks
     (:label :modify
	     (:top ,#f  (truncate (gvl :parent :y1) 2))
	     (:left 5))
     )))
   


; A coordinate-transform window for use inside a larger aggregate
; which presumably draws axes around it.  Setting border-width to 0 so
; you don't notice that it is a window.  The window size and position
; are determined by the CONTAINER, which is the aggregate the window
; is inside of.  Note that the PARENT of a window must be another
; window, so I use CONTAINER to point to the aggregate.  left, top,
; width and height must ALWAYS be a positive integer, even if
; container is NIL, because opal will try to update the window, and
; create an xlib drawable for it.

(create-instance 'graphing-window coordinate-transform-window
  (:border-width 0)
  (:visible #f (and (gvl :aggregate)	;normally, having an aggregate suffices
		    (gvl :parent)	;but this window is to be a subwindow
		    (and		;this is really more of a sanity check
		     (gvl :container)	;than essential
		     (eq (gvl :container :window)
			 (gvl :parent)))
		    (gvl :parent :visible)))
  (:container nil)
  (:left #f (if (gvl :container)
	      (+ (gvl :container :graph-left) 1)
	      0))
  (:top #f (if (gvl :container)
	     (gvl :container :graph-top)
	     0))
  (:width #f (if (gvl :container)
	       (gvl :container :graph-width)
	       200))			;must always be an integer,(see below)
  (:height #f (if (gvl :container)
		(gvl :container :graph-height)
		200))
  )





;; Aggregate which draws coordinate axes and has a GRAPH-SUBWINDOW
;; where coordinates and transformed.

(create-instance 'graphing-aggregate opal:aggregadget
  (:width 400)				;needs fixed size so subwindow position
  (:height 400)				;is predictable
  (:top 0)
  (:left 0)
  (:graph-left #f (+ (gvl :left) (gvl :axis-x-margin)))
  (:graph-width #f (- (gvl :width) (* (gvl :axis-x-margin) 2)))
  (:graph-right #f (+ (gvl :graph-left) (gvl :graph-width)))
  (:graph-top #f (+ (gvl :top) (gvl :axis-y-margin)))
  (:graph-height #f (- (gvl :height) (* (gvl :axis-y-margin) 2)))
  (:graph-bottom #f (+ (gvl :graph-top) (gvl  :graph-height)))

  (:tick-size 10)			;in pixels
  (:axis-x-margin 50)
  (:axis-y-margin 50)

  ;; specification for X axis
  (:draw-x-axis? t)
  (:x-tick-count 4)
  (:x-tick-size #f (gvl :tick-size))
  (:x-tick-format "~,2F")
  (:x-label "x axis")
  (:x-to-label #'(lambda (value) (format nil "~@?" (gvl :tick-format) value)))

  ;; and for Y
  (:draw-y-axis? t)
  (:y-tick-count 4)
  (:y-tick-size #f (gvl :tick-size))
  (:y-tick-format "~,2F")
  (:y-label "y axis")
  (:y-to-label #'(lambda (value) (format nil "~@?" (gvl :tick-format) value)))

  (:graph-subwindow-prototype graphing-window)
  (:graph-subwindow-agg-prototype opal:aggregate)
  (:graph-subwindow)

  (:parts
   `(
     (:x-axis ,plotting-x-axis
	      (:label-string ,#f (gvl :parent :x-label))
	      (:tick-size ,#f (gvl :parent :x-tick-size))
	      (:tick-count ,#f (gvl :parent :x-tick-count))
	      (:tick-format ,#f (gvl :parent :x-tick-format))
	      (:w-to-label ,#f (gvl :parent :x-to-label)))
     (:y-axis ,plotting-y-axis
	      (:label-string ,#f (gvl :parent :y-label))
	      (:tick-size ,#f (gvl :parent :y-tick-size))
	      (:tick-count ,#f (gvl :parent :y-tick-count))
	      (:tick-format ,#f (gvl :parent :y-tick-format))
	      (:w-to-label ,#f (gvl :parent :y-to-label)))

     ))
  )

(define-method :initialize graphing-aggregate (self)
  (call-prototype-method self)
  (kr-send self :create-graph-subwindow self)
  (kr-send self :update-ticks self)
  )

; It is safe to create the subwindow when the aggregate is created.
; But at initialize time, the container does not yet have a window,
; so the :parent formula yields NIL.  Thus although the subwindow 
; eventually does get a parent, the parent does not know the child.

(define-method :create-graph-subwindow graphing-aggregate (self)
  (let ((container self))
    (s-value self :graph-subwindow 
	     (create-instance nil (g-value self :graph-subwindow-prototype)
	       (:aggregate (create-instance nil
			       (g-value self :graph-subwindow-agg-prototype)))
	       (:container container)
	       (:parent #f (gvl :container :window))))))

(define-method :update-ticks graphing-aggregate (self)
  (kr-send (g-value self :x-axis :ticks)
	   :create-tick-marks (g-value self :x-axis :ticks))
  (kr-send (g-value self :y-axis :ticks)
	   :create-tick-marks (g-value self :y-axis :ticks)))




;;; Kludge Dept

; The subwindow knows its parent (because the formula tells it) but
; the parent does not know the child.  I think that's because when the
; window is created, its container does not yet have a window.  So when
; the aggregate DOES get a window, that would be a good time to set
; the window for the subwindow.  But I can't find out when that happens.
; Instead, I do it when the main window is updated.  

#| ; I tried to make the window assignment happen when the aggregate
; was updates, but this does not work because the update function for window
; calls update-method-aggregate directly, thus bypassing KR's inheritance
; mechanism.

(define-method :update graphing-aggregate (agg update-info
					       line-style-gc filling-style-gc
					       drawable root-window
					       clip-mask bbox-1 bbox-2
					       &optional (total-p NIL))
  (when (g-value agg :graph-subwindow)
    (add-self-to-parent (g-value agg :graph-subwindow)))
  (call-prototype-method agg update-info line-style-gc filling-style-gc
			 drawable root-window clip-mask bbox-1 bbox-2 total-p))

|#

; I define CLWIN elsewhere, this form is here for the sake of anyone
; who might want to use PLOTXY without the rest of CLWIN.

(export 'clwin)

(when (or (not (boundp 'clwin))
	  (not (schema-p clwin)))
  (create-instance 'clwin  inter:interactor-window))

(define-method :update clwin (a-window &optional (total-p NIL))
  (when (g-value a-window :aggregate)
    (ensure-subwindows-have-parents (g-value a-window :aggregate)))
  (call-prototype-method a-window total-p))

(defun ensure-subwindows-have-parents (aggregate)
  (when (and (g-value aggregate :graph-subwindow)
	     (kr:schema-p (g-value aggregate :graph-subwindow)))
    (add-self-to-parent (g-value aggregate :graph-subwindow)))
  (opal:do-components aggregate #'ensure-subwindows-have-parents))


(defun add-self-to-parent (a-window)
  (when (and (g-value a-window :parent)
	     (kr:schema-p (g-value a-window :parent))
	     (not (find a-window (g-value a-window :parent :child))))
    (warn "~A's parent does not know its child, adding it~%" a-window)
    (push a-window (g-value a-window :parent :child))))


;; This is a simple example of how to plot.

(defun plot-points (points &key window
			   (line-style opal:default-line-style))
  (if window
    (progn
      (s-value window :data-points points)
      (opal:update window)
      )
    (let* ((agg (create-instance nil graphing-aggregate))
	   (win (create-instance nil inter:interactor-window
		  (:title "Plot")
		  (:data-points points)
		  (:left 700)
		  (:top 100)
		  (:aggregate agg)
		  (:height (g-value agg :height))
		  (:width (g-value agg :width))
		  )))
      ;; the subwindow's parent does not know the child.
      ;; so we have to fix it.
      (when (null (g-value win :child))
	(opal:update win)
	(s-value win :child (list (g-value win :aggregate :graph-subwindow))))

      (opal:add-component
       (g-value win :aggregate :graph-subwindow :aggregate)
       (create-instance nil line-connected-data-points 
	 (:line-style line-style)
	 (:data-points #f (gvl :window :parent :window :data-points))))

      (kr-send (g-value win :aggregate) :update-ticks
	       (g-value win :aggregate))

      (opal:update win)
      win)))

(defun zoom (window &optional (factor 2))
  (let ((sub (g-value  window :aggregate :graph-subwindow)))
    (kr-send sub :set-scale sub (* (g-value sub :scale)  factor))))

(defun scroll-x (window &key (pixels  100))
  (let ((sub (g-value  window :aggregate :graph-subwindow)))
    (kr-send sub :set-origin
	     sub
	     (+ (g-value sub :origin-x) pixels)
	     (+ (g-value sub :origin-y) 0))))

    
#|
(setf www (plot-points  '((1 1) (2 2) (4 5)  (8 10) (10 9))))

(setf www
      (plot-points '((1 1) (2 2) (4 5)  (8 10) (10 9) (20 18) (21 17) (22 1)
		     (25 30) (30 30) (40 1) (41 0) (42 -5) (43 -10) (50 2) (55 10)
		     (100 20)
		     )
		   :line-style (create-instance nil opal:line-style
				 (:foreground-color opal:red)
				 (:line-style :dash)
				 (:line-thickness 5)
				 (:dash-pattern '(1 1 1 1 3 1)))))

(s-value www :aggregate :x-tick-format "~d")
(opal:update www)

(zoom www .25)
(zoom www 2)

(progn
  (dotimes (i 10)
    (scroll-x www :pixels -20)
    )
  (dotimes (i 10)
    (scroll-x www :pixels 20)
    (sleep .1)
    ))

(opal:destroy www)

||#
