;-*- Mode: Lisp; Package: (graph :use (common-lisp ccl g)) -*- Graph.lisp Source Code

; Source Code for Graph.lisp, a quick-graphing tool for
; for Macintosh Common Lisp.  See http://www-anw.cs.umass.edu/~rich/graph.html.

(defpackage :graph
  (:use :common-lisp :ccl :g))

(in-package :graph)

(export '(graph graph+ graph- add-to-graph subtract-from-graph grid-graph x-graph-limits y-graph-limits
          x-tick-marks y-tick-marks choose-graph print-graph graph-data))

; A graph is a window with various state vars.  The simplest way to use this is:

; (graph data) 
; and then, possibly, (graph+ data)

; The graph involved defaults to the frontmost graph or a newly created graph
; if their are no graphs yet (or if graph is t). Alternatively, you can make 
; multiple graphs, and specify the graph as a last argument to all graph routines.

; Data can be a simple list of y's (heights) or a list of list of y's.
; Or it can be a list of (x y) coordinates, e.g., ((x1 y1) (x2 y2) ...)
; Or a list of those!

; The span of the graph is initially set from the data.  Alternatively:
;   (x-graph-limits xmin xmax) does it manually (same for y-graph-limits)
;   (x-graph-limits) sets it back to auto

; Tick marks are initially just at the min and max.  Alternatively:
;   (x-tick-marks tick1 tick2 tick3 ...) sets them manually (same for y-tick-marks)
;   (x-tick-marks) sets them back to auto
; Tick marks are specified by values, e.g., (x-tick-marks 0 .5 1.0) 
; or by a list of value-label pairs, e.g., (x-tick-marks '(0 "0") '(1.0 "1"))

(defclass graph (g-window)
  ((data-view :accessor data-view)
   (data :initform nil)
   (auto-limits-x :initform t)          ; limiting x values from data, 
   (auto-limits-y :initform t)          ; or from user and tick-marks?
   (x-max :initform 1.0)
   (x-min :initform 0.0)
   (y-max :initform 1.0)
   (y-min :initform 0.0)
   (x-tick-marks :initform nil)         ; initial tick marks auto from limits
   (y-tick-marks :initform nil)
   (main-color)
   (character-style :initform '("Geneva" 9 :plain))
   (character-width :initform 6)
   (character-height :initform 8)
   (x-label-space)
   (y-label-space)
   (zero-space :initform 10)
   (x-end-space :initform 50)
   (y-end-space :initform 10)
   (tick-length :initform t)
   (boxy :initform nil)
   (grid-density :initform nil)
   (highlight-p :initform nil)
   (highlight-color)
   (highlight-line :initform 0)))


;; The data is a list of lists.  Each list is either a list of y-values or
;; a list of xy-pairs.

(defmethod initialize-instance ((graph graph) &key (data-view-type 'data-view))
  (without-event-processing
    (call-next-method)
    (with-slots (main-color highlight-color data-view x-label-space y-label-space character-width
                       character-height tick-length zero-space
                       x-min y-min x-max y-max) graph
      (setq data-view (make-instance data-view-type :parent graph))
      (g-set-coordinate-system data-view x-min y-min x-max y-max :lower-left)
      (setq x-label-space (* 11 character-width))
      (setq y-label-space (+ 10 character-height))
      (setq tick-length (/ zero-space 2))
      (setq main-color (g-color-on data-view))
      (setq highlight-color (g-color-pen data-view (g-color-flip data-view) nil nil 2 2))
      (g-accept-new-viewport-size graph))))

(defmethod g-accept-new-viewport-size :before ((graph graph))
  (g-set-cs-scale graph 0 0 1 1 :lower-left))

(defmethod g-accept-new-viewport-size :after ((graph graph))
  (with-slots (x-label-space zero-space y-label-space zero-space x-end-space y-end-space
                             data-view) graph
    (multiple-value-bind (x1 y1 x2 y2) (g-get-coordinate-system graph)
      (declare (ignore x1 y1))
      (g-set-viewport data-view
                      (+ x-label-space zero-space)
                      (+ y-label-space zero-space)
                      (- x2 x-end-space)
                      (- y2 y-end-space)))
    (g-clear graph)
    (g-draw-view graph)))


(defclass data-view (g-view) ())

(defun graph (new-data &optional color graph)                       ;[Doc]
  "Establishes some data for a graph, then draws it"
  (if (OR (null new-data) (loop for d in new-data never d))
    (print "No graphing data")
    (let ((original-front-window (front-window)))
      (setq new-data (loop for d in new-data for n from 0 when d collect d
                           when (null d) do (format t "~%Warning: Nth data-to-be-graphed is nil for N=~A" n)))
      (cond ((stringp color) (setq graph color) (setq color nil))
            ((keywordp color) (setq color (color-from-keyword color))))
      (setq graph (choose-graph graph))
      (with-slots (data highlight-line highlight-p) graph
        (setf data (fillin-nil-colors (regularize-data new-data color)))
        (setq highlight-line 0)
        (setq highlight-p nil)
        (compute-limits-from-data graph)
        (g-clear graph)
        (g-draw-view graph)
        (unless (eq graph (front-window)) (window-select graph))
        (window-select original-front-window)))))

(defun regularize-data (data color)
  "regular form is a list of lines, each of which is a list or list of pairs, preceded by color"
  (cond ((atom (first data))            ; simple list
         (list (cons color data)))
        ((listp (first (first data)))   ; list of lists of pairs
         (loop for d in data collect (cons color d)))
        ((= 2 (length (first data)))    ; list of pairs
         (list (cons color data)))
        (t                              ; list of lists
          (loop for d in data collect (cons color d)))))

(defun fillin-nil-colors (data)
  (loop for color-line in data
        when (null (first color-line))
        do (setf (first color-line) (first-unused-color data)))
  data)

(defun graph-data (&optional graph)
  "Returns the data plotted in graph, with color stripped away of course"
  (loop for (color . line) in (slot-value (choose-graph graph) 'data)
        collect line))

(defun first-x (data)
  "returns the x-value of the first point in data"
  (let* ((line (loop for line in data until line finally return line))
         (first-point (second line)))
    (if (not (consp first-point))
      1
      (first first-point))))
               
(defun first-y (data)
  "returns the y-value of the first point in data"
  (let* ((line (loop for line in data until line finally return line))
         (first-point (second line)))
    (if (not (consp first-point))
      first-point
      (second first-point))))

(defun graph+ (new-data &optional color graph)            ;[Doc]
  (add-to-graph new-data color graph))
               
(defun add-to-graph (new-data &optional color graph)                  ;[Doc]
  "Adds additional data to a graph"
  (if (OR (null new-data) (loop for d in new-data never d))
    (print "No graphing data added")
    (let ((original-front-window (front-window)))
      (setq new-data (loop for d in new-data for n from 0 when d collect d
                           when (null d) do (format t "~%Warning: Nth data-to-be-graphed is nil for N=~A" n)))
      (cond ((stringp color) (setq graph color) (setq color nil))
            ((keywordp color) (setq color (color-from-keyword color))))
      (setq graph (choose-graph graph))
      (with-slots (data) graph
        (setf data (fillin-nil-colors (append data (regularize-data new-data color))))
        (compute-limits-from-data graph)
        (g-clear graph)
        (g-draw-view graph)
        (unless (eq graph (front-window)) (window-select graph))
        (window-select original-front-window)))))

(defun graph- (&optional color-keyword graph)            ;[Doc]
  "Remove a line of data points from the graph.  Defaults to last line"
  (if (not color-keyword)
    (subtract-from-graph nil graph)
    (with-slots (data) (setq graph (choose-graph graph))
      (loop with remove-color = (color-from-keyword color-keyword)
            for (color . list) in data
            for line-num from 0
            when (eq color remove-color)
            do (subtract-from-graph line-num graph)
               (return-from graph-)
            finally (print "No such color used in this graph")))))
               
(defun subtract-from-graph (&optional line-num graph)            ;[Doc]
  "Remove a line of data points from the graph. Line-num is from zero or defaults to last line"
  (let ((original-front-window (front-window)))
    (setq graph (choose-graph graph))
    (with-slots (data) graph
      (unless line-num (setq line-num (- (length data) 1)))
      (setf data (loop for line in data
                       for num from 0
                       unless (eq num line-num) collect line))
      (compute-limits-from-data graph)
      (g-clear graph)
      (g-draw-view graph)
      (unless (eq graph (front-window)) (window-select graph))
      (window-select original-front-window))))

(defun x-graph-limits (&optional xmin xmax (graph (choose-graph)))  ;[Doc]
  (setq graph (choose-graph graph))
  (with-slots (auto-limits-x data-view x-min y-min x-max y-max) graph
    (if (or xmin xmax)
      (progn (setf auto-limits-x nil)
             (if xmin (setf x-min xmin))
             (if xmax (setf x-max xmax))
             (g-set-coordinate-system data-view x-min y-min x-max y-max :lower-left))
      (progn (setf auto-limits-x t)
             (compute-limits-from-data graph)))
    (g-clear graph)
    (g-draw-view graph)))

(defun y-graph-limits (&optional ymin ymax (graph (choose-graph)))  ;[Doc]
  (setq graph (choose-graph graph))
  (with-slots (auto-limits-y data-view x-min y-min x-max y-max) graph
    (if (or ymin ymax)
      (progn (setf auto-limits-y nil)
             (if ymin (setf y-min ymin))
             (if ymax (setf y-max ymax))
             (g-set-coordinate-system data-view x-min y-min x-max y-max :lower-left))
      (progn (setf auto-limits-y t)
             (compute-limits-from-data graph)))
    (g-clear graph)
    (g-draw-view graph)))

(defun x-tick-marks (&rest x-ticks)                                ;[Doc]
  "Sets the ticks marks and possibly resets limits."
  (let ((graph (choose-graph)))
    (with-slots (data-view x-tick-marks x-min y-min x-max y-max) graph
      (if x-ticks 
        (progn (setf x-tick-marks (regularize-tick-marks x-ticks))
               (setq x-min (min x-min (min-tick-mark x-tick-marks)))
               (setq x-max (max x-max (max-tick-mark x-tick-marks)))
               (g-set-coordinate-system data-view x-min y-min x-max y-max :lower-left))
        (progn (setq x-tick-marks nil)
               (compute-limits-from-data graph)))
      (g-clear graph)
      (g-draw-view graph))))

(defun y-tick-marks (&rest y-ticks)                                ;[Doc]
  "Sets the ticks marks and possibly resets limits."
  (let ((graph (choose-graph)))
    (with-slots (y-tick-marks) graph
      (if y-ticks 
        (progn (setf y-tick-marks (regularize-tick-marks y-ticks))
               (compute-limits-from-data graph))
        (progn (setq y-tick-marks nil)
               (compute-limits-from-data graph)))
      (g-clear graph)
      (g-draw-view graph))))

(defun regularize-tick-marks (ticks &optional (format-string "~A"))
  (loop for tick in ticks
        when (atom tick) collect (list tick (format nil format-string tick))
        else collect tick))

(defun min-tick-mark (ticks)
  (let ((first (first ticks)))
    (if (atom first)
      first
      (first first))))

(defun max-tick-mark (ticks)
  (let ((last (first (last ticks))))
    (if (atom last)
      last
      (first last))))

(defmethod g-draw-view ((graph graph))
  "Draws the graph"
  (with-slots (data grid-density highlight-p) graph
    (draw-axes graph)
    (loop for (color . list) in data
          for line-num from 0
          do (draw-line graph list color))
    (when highlight-p (draw-highlight graph))
    (if grid-density (grid-graph nil graph))))

(defvar colors (list (g-color-red t)
                     (g-color-green t)
                     (g-color-blue t)
                     (g-color-black t)
                     (g-color-yellow t)
                     (g-color-pink t)
                     (g-color-cyan t)
                     (g-color-purple t)
                     (g-color-magenta t)
                     (g-color-orange t)
                     (g-color-brown t)
                     (g-color-light-blue t)
                     (g-color-gray t)
                     (g-color-dark-green t)
                     (g-color-tan t)))

(defun nth-color (n)                                      ;[Doc]
  (nth (mod n (length colors)) colors))

(defun color-from-keyword (color-keyword)
  (case color-keyword
    (:blue g::blue)
    (:red g::red)
    (:green g::green)
    (:black g::black)
    (:yellow g::yellow)
    (:pink g::pink)
    (:cyan g::cyan)
    (:purple g::purple)
    (:magenta g::magenta)
    (:orange g::orange)
    (:brown g::brown)
    (:light-blue g::light-blue)
    (:gray g::gray)
    (:dark-green g::dark-green)
    (:tan g::tan)
    (:white g::white)
    (:light-gray g::light-gray)
    (:dark-gray g::dark-gray)
    (t (error "Unrecognized color keyword: ~A" color-keyword))))

(defun first-unused-color (data)
  "Returns first color in the list of colors that is least used in data"
  (loop for permitted-times-used from 0 do
        (loop for color in colors
              when (<= (times-color-used color data) permitted-times-used)
              do (return-from first-unused-color color))))

(defun times-color-used (color data)
  (loop for (c . list) in data count (eq c color)))

(defun choose-graph (&optional graph)                           ;[Doc]
  "Select a graph based on input 'graph'"
  (cond ((typep graph 'graph)
         graph)
        ((typep graph 'string)
         (or (find-window graph 'graph) 
             (make-instance 'graph :window-title graph)))
        ((null graph)
         (or (front-window :class 'graph)
              (make-instance 'graph :window-title "Graph")))
        ((eq graph t)
         (make-instance 'graph :window-title "Graph"))
        (t (error "Can't chose graph" graph))))
  
(defun draw-axes (&optional graph)
  (setq graph (choose-graph graph))
  (with-slots (x-label-space y-label-space data-view x-max y-max main-color) graph
    (g-draw-line graph
                 x-label-space y-label-space
                 (g-convert-x data-view graph x-max) y-label-space
                 main-color)
    (g-draw-line graph
                 x-label-space y-label-space
                 x-label-space (g-convert-y data-view graph y-max)
                 main-color)
    (draw-tick-marks graph)))

(defun draw-tick-marks (graph)
  (with-slots (main-color x-tick-marks y-tick-marks x-label-space y-label-space 
                     data-view tick-length character-style x-min y-min x-max y-max
                     character-width character-height) graph
    (loop for (x label) in (or x-tick-marks (regularize-tick-marks (list x-min x-max)))
	  for gx = (g-convert-x data-view graph x)
          when (<= x-min x x-max)
	  do
          (g-draw-line-r graph gx y-label-space 0 tick-length main-color)
          (g-draw-text graph label character-style
                       (- gx (/ (* character-width (length label)) 2))
                       (- y-label-space 5 character-height) main-color))
    (loop for (y label) in (or y-tick-marks (regularize-tick-marks (list y-min y-max) "~7F"))
	  for gy = (g-convert-y data-view graph y)
          when (<= y-min y y-max)
	  do
          (g-draw-line-r graph x-label-space gy tick-length 0 main-color)
          (g-draw-text graph label character-style
                       (- x-label-space 5 (* character-width (length label)))
                       (- gy (/ character-height 2)) main-color))))

(defun draw-segment (graph x1 y1 x2 y2 color)
  (with-slots (data-view boxy) graph
    (if boxy
      (progn (g-draw-line data-view x1 y1 x2 y1 color)
             (g-draw-line data-view x2 y1 x2 y2 color))
      (g-draw-line data-view x1 y1 x2 y2 color))))

(defun draw (graph y-list color)
  (loop for x1 from 1 below (length y-list)
	for x2 from 2 upto (length y-list)
	for y1 in y-list
	for y2 in (cdr y-list)
	do
        (draw-segment graph x1 y1 x2 y2 color)))

(defun draw-xy (graph xylist color)
  (loop for (x1 y1) in xylist
	for (x2 y2) in (cdr xylist)
	do
    (draw-segment graph x1 y1 x2 y2 color)))

(defun compute-limits-from-data (graph)
  (with-slots (auto-limits-x auto-limits-y data-view data x-min x-max y-min y-max
                             x-tick-marks y-tick-marks) graph
    (when auto-limits-x
      (setq x-min (or (min-tick-mark x-tick-marks) (first-x data)))
      (setq x-max (or (max-tick-mark x-tick-marks) (first-x data))))
    (when auto-limits-y
      (setq y-min (or (min-tick-mark y-tick-marks) (first-y data)))
      (setq y-max (or (max-tick-mark y-tick-marks) (first-y data))))
    (when (or auto-limits-x auto-limits-y)
      (loop for list in data do
            (setq list (rest list))
            (cond ((atom (first list))
                   (when auto-limits-y
                     (loop for y in list do
                           (if (< y y-min) (setq y-min y))
                           (if (> y y-max) (setq y-max y))))
                   (when auto-limits-x
                     (if (< 1 x-min) (setq x-min 1))
                     (if (> (length list) x-max) (setq x-max (length list)))))
                  (t (loop for (x y) in list do 
                           (when auto-limits-y
                             (if (< y y-min) (setq y-min y))
                             (if (> y y-max) (setq y-max y)))
                           (when auto-limits-x
                             (if (< x x-min) (setq x-min x))
                             (if (> x x-max) (setq x-max x)))))))
      (when (= y-min y-max)
        (format t "~%Warning: all lines are flat at ~A" y-min)
        (if (> y-max 0)
          (setq y-min 0)
          (setq y-min (- y-max 1))))
      (g-set-coordinate-system data-view x-min y-min x-max y-max :lower-left))))

(defun grid-graph (&optional grid-densit graph)               ;[Doc]
  (setq graph (choose-graph graph))
  (with-slots (data-view grid-density x-tick-marks y-tick-marks 
                         x-max x-min y-max y-min main-color) graph
    (when grid-densit (setq grid-density grid-densit))
    (setq grid-density (or grid-density 5))
    (loop for (x label) in x-tick-marks
	  for dx = (gd-coord-x data-view x)
          when (<= x-min x x-max)
	  do label                             ; to prevent warning that label is ignored
          (loop for dy from (gd-coord-y data-view y-min)
		downto (gd-coord-y data-view y-max) by grid-density do
                (gd-draw-point data-view dx dy main-color)))
    (loop for (y label) in y-tick-marks
	  for dy = (gd-coord-y data-view y)
          when (<= y-min y y-max)
	  do label                             ; to prevent warning that label is ignored
          (loop for dx from (gd-coord-x data-view x-min)
		to (gd-coord-x data-view x-max) by grid-density do
                (gd-draw-point data-view dx dy main-color)))))

(defmethod g-cursor ((v data-view) x y)
  (declare (ignore x y))
  *cross-hair-cursor*)

(defmethod g-click-event-handler ((v data-view) x y)
    (print (list  x y)))

(defmethod view-key-event-handler ((graph graph) char)
  (with-slots (data highlight-p highlight-line) graph
    (case char
      ((#\h #\Space)
       (setq highlight-p (not highlight-p))
       (draw-highlight graph))
      (#\                              ; back arrow
       (when highlight-p
         (draw-highlight graph)
         (setf highlight-line (mod (- highlight-line 1) (length data)))
         (draw-highlight graph)))
      (#\                              ; space or forward arrow
       (when highlight-p
         (draw-highlight graph)
         (setf highlight-line (mod (+ highlight-line 1) (length data)))
         (draw-highlight graph))))))

(defun draw-highlight (graph)
  (with-slots (data highlight-line highlight-color) graph
    (draw-line graph (rest (nth highlight-line data)) highlight-color)))

(defun draw-line (graph line color)
  (if (atom (first line))
    (draw graph line color)
    (draw-xy graph line color)))

(defmethod window-hardcopy ((graph graph) &optional (show-dialog? t))
  (let (picture)
    (start-picture graph)
    (g-draw-view graph)
    (setq picture (get-picture graph))
    (sleep .5)
    (picture-hardcopy picture show-dialog?)
    (kill-picture picture)))


(export '(histogram histogram+))

; A histogram is a graph, created in a particular way

; (histogram data num-bins min max graph)

(defun histogram (data &key num-bins min max-excl color graph)                                   ;[Doc]
  "plots histogram of data, min <= data < max-excl, in a color on a graph named graph"
  (unless data (error "No graphing data"))
  (when (= (length data) 1) (error "Can't histogram a single datum"))
  (unless min (setq min (loop for d in data minimize d)))
  (let ((max (loop for d in data maximize d)))
    (when (= max min) (error "Data min=max; no histogram possible"))
    (when (and (integerp max) (integerp min))
             (unless max-excl (setq max-excl (+ 1 max)))
             (unless (or num-bins (> (- max min) 200))
               (setq num-bins (- max-excl min))))
    (unless num-bins (setq num-bins 30))
    (unless max-excl (setq max-excl (+ max (* .00001 (/ (- max min) num-bins))))))
  (setq graph (choose-graph graph))
  (when (string= "Graph" (window-title graph)) (set-window-title graph "Histogram"))
  (setf (slot-value graph 'boxy) t)
  (loop with bins = (make-array num-bins :initial-element 0)
        with num-too-small = 0 and num-too-big = 0
        with scale-factor = (/ num-bins (- max-excl min))
        for d in data
        for bin = (truncate (* (- d min) scale-factor))
        do (cond ((< bin 0) (incf num-too-small))
                 ((>= bin num-bins) (incf num-too-big))
                 (t (incf (aref bins bin))))
        finally (progn (graph (loop for i below num-bins
                                    collect (list (+ min (/ i scale-factor))
                                                  (aref bins i))
                                    when (= i (- num-bins 1)) 
                                    collect (list max-excl (aref bins i)))
                              color graph)
                       (unless (= 0 num-too-big) 
                         (format t "~%~A data points were above the range" num-too-big))
                       (unless (= 0 num-too-small) 
                         (format t "~%~A data points were below the range" num-too-small)))))

(defun histogram+ (data &key num-bins min max-excl color graph)                              ;[Doc]
  "adds histogram of data, min <= data < max-excl, in a color to a graph named graph"
  (unless data (error "No graphing data"))
  (when (= (length data) 1) (error "Can't histogram a single datum"))
  (unless min (setq min (loop for d in data minimize d)))
  (let ((max (loop for d in data maximize d)))
    (when (= max min) (error "Data min=max; no histogram possible"))
    (when (and (integerp max) (integerp min))
             (unless max-excl (setq max-excl (+ 1 max)))
             (unless (or num-bins (> (- max min) 200))
               (setq num-bins (- max-excl min))))
    (unless num-bins (setq num-bins 30))
    (unless max-excl (setq max-excl (+ max (* .00001 (/ (- max min) num-bins))))))
  (setq graph (choose-graph graph))
  (when (string= "Graph" (window-title graph)) (set-window-title graph "Histogram"))
  (setf (slot-value graph 'boxy) t)
  (loop with bins = (make-array num-bins :initial-element 0)
        with num-too-small = 0 and num-too-big = 0
        with scale-factor = (/ num-bins (- max-excl min))
        for d in data
        for bin = (truncate (* (- d min) scale-factor))
        do (cond ((< bin 0) (incf num-too-small))
                 ((>= bin num-bins) (incf num-too-big))
                 (t (incf (aref bins bin))))
        finally (progn (graph+ (loop for i below num-bins
                                     collect (list (+ min (/ i scale-factor))
                                                   (aref bins i))
                                     when (= i (- num-bins 1)) 
                                     collect (list max-excl (aref bins i)))
                               color graph)
                       (unless (= 0 num-too-big) 
                         (format t "~%~A data points were above the range" num-too-big))
                       (unless (= 0 num-too-small) 
                         (format t "~%~A data points were below the range" num-too-small)))))



;