;;;; "charplot.scm", plotting on character devices for Scheme
;;; Copyright (C) 1992, 1993 Aubrey Jaffer.
;
;Permission to copy this software, to redistribute it, and to use it
;for any purpose is granted, subject to the following restrictions and
;understandings.
;
;1.  Any copy made of this software must include this copyright notice
;in full.
;
;2.  I have made no warrantee or representation that the operation of
;this software will be error-free, and I am under no obligation to
;provide any services, by way of maintenance, update, or otherwise.
;
;3.  In conjunction with products arising from the use of this
;material, there shall be no use of my name in any advertising,
;promotional, or sales literature without prior written consent in
;each case.

(require 'sort)

(define charplot:rows 24)
(define charplot:columns (output-port-width (current-output-port)))

(define charplot:xborder #\_)
(define charplot:yborder #\|)
(define charplot:xaxchar #\-)
(define charplot:yaxchar #\:)
(define charplot:curve1 #\*)
(define charplot:xtick #\.)

(define charplot:height (- charplot:rows 5))
(define charplot:width (- charplot:columns 15))

(define (charplot:printn! n char)
  (cond ((positive? n)
	 (write-char char)
	 (charplot:printn! (+ n -1) char))))

(define (charplot:center-print! str width)
  (let ((lpad (quotient (- width (string-length str)) 2)))
    (charplot:printn! lpad #\ )
    (display str)
    (charplot:printn! (- width (+ (string-length  str) lpad)) #\ )))

(define (scale-it z scale)
  (if (and (exact? z) (integer? z))
      (quotient (* z (car scale)) (cadr scale))
      (inexact->exact (round (/ (* z (car scale)) (cadr scale))))))

(define (find-scale isize delta)
  (if (inexact? delta) (set! isize (exact->inexact isize)))
  (do ((d 1 (* d 10)))
      ((<= delta isize)
       (do ((n 1 (* n 10)))
	   ((>= (* delta 10) isize)
	    (list (* n (cond ((< (* delta 8) isize) 8)
			     ((< (* delta 6) isize) 6)
			     ((< (* delta 5) isize) 5)
			     ((< (* delta 4) isize) 4)
			     ((< (* delta 3) isize) 3)
			     ((< (* delta 2) isize) 2)
			     (else 1)))
		  d))
	 (set! delta (* delta 10))))
    (set! isize (* isize 10))))

(define (charplot:iplot! data xlabel ylabel xmin xscale ymin yscale)
  (define xaxis (- (scale-it ymin yscale)))
  (define yaxis (- (scale-it xmin xscale)))
  (charplot:center-print! ylabel 11)
  (charplot:printn! (+ charplot:width 1) charplot:xborder)
  (newline)
  (set! data (sort! data (lambda (x y) (if (= (cdr x) (cdr y))
					   (< (car x) (car y))
					   (> (cdr x) (cdr y))))))
  (do ((ht (- charplot:height 1) (- ht 1)))
      ((negative? ht))
    (let ((a (make-string (+ charplot:width 1)
			  (if (= ht xaxis) charplot:xaxchar #\ )))
	  (ystep (if (= 1 (gcd (car yscale) 3)) 2 3)))
      (string-set! a charplot:width charplot:yborder)
      (if (< -1 yaxis charplot:width) (string-set! a yaxis charplot:yaxchar))
      (do ()
	  ((or (null? data) (not (>= (cdar data) ht))))
	(string-set! a (caar data) charplot:curve1)
	(set! data (cdr data)))
      (if (zero? (modulo (- ht xaxis) ystep))
	  (let* ((v (number->string (/ (* (- ht xaxis) (cadr yscale))
				       (car yscale))))
		 (l (string-length v)))
	    (if (> l 10)
		(display (substring v 0 10))
		(begin
		  (charplot:printn! (- 10 l) #\ )
		  (display v)))
	    (display charplot:yborder)
	    (display charplot:xaxchar))
	  (begin
	    (charplot:printn! 10 #\ )
	    (display charplot:yborder)
	    (display #\ )))
      (display a) (newline)))
  (let* ((xstep (if (= 1 (gcd (car xscale) 3)) 10 12))
	 (xstep/2 (quotient (- xstep 2) 2))
	 (fudge (modulo yaxis xstep)))
    (charplot:printn! 10 #\ ) (display charplot:yborder)
    (charplot:printn! (+ 1 fudge) charplot:xborder)
    (display charplot:yaxchar)
    (do ((i fudge (+ i xstep)))
	((> (+ i xstep) charplot:width)
	 (charplot:printn! (modulo (- charplot:width (+ i 1)) xstep)
			   charplot:xborder))
      (charplot:printn! xstep/2 charplot:xborder)
      (display charplot:xtick)
      (charplot:printn! xstep/2 charplot:xborder)
      (display charplot:yaxchar))
    (display charplot:yborder) (newline)
    (charplot:center-print! xlabel (+ 12 fudge (- xstep/2)))
    (do ((i fudge (+ i xstep)))
	((> (+ i xstep) charplot:width))
      (charplot:center-print! (number->string (/ (* (- i yaxis) (cadr xscale))
						 (car xscale)))
			      xstep))
    (newline)))

(define (charplot:plot! data xlabel ylabel)
  (let* ((xmax (apply max (map car data)))
	 (xmin (apply min (map car data)))
	 (xscale (find-scale charplot:width (- xmax xmin)))
	 (ymax (apply max (map cdr data)))
	 (ymin (apply min (map cdr data)))
	 (yscale (find-scale charplot:height (- ymax ymin)))
	 (ixmin (scale-it xmin xscale))
	 (iymin (scale-it ymin yscale)))
    (charplot:iplot! (map (lambda (p)
			    (cons (- (scale-it (car p) xscale) ixmin)
				  (- (scale-it (cdr p) yscale) iymin)))
			  data)
		     xlabel ylabel xmin xscale ymin yscale)))

(define plot! charplot:plot!)
