;;;; "grapheps.scm", Create PostScript Graphs ;;; Copyright (C) 2003 Aubrey Jaffer ; ;Permission to copy this software, to modify it, to redistribute it, ;to distribute modified versions, 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 warranty 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 'array) (require 'array-for-each) (require 'line-i/o) (require 'color) (require 'resene) (require 'saturate) (require 'filename) ;;@code{(require 'eps-graph)} ;; ;;@noindent ;;This is a graphing package creating encapsulated-PostScript files. ;;Its motivations and design choice are described in ;;@url{http://swiss.csail.mit.edu/~jaffer/Docupage/grapheps} ;; ;;@noindent ;;A dataset to be plotted is taken from a 2-dimensional array. ;;Corresponding coordinates are in rows. Coordinates from any ;;pair of columns can be plotted. ;;; String append which accepts numbers, symbols, vectors, and lists. (define (scheme->ps . args) (apply string-append (map (lambda (arg) (cond ((number? arg) (number->string arg)) ((symbol? arg) (symbol->string arg)) ((or (vector? arg) (list? arg)) (string-append "[ " (apply string-append (map (lambda (x) (scheme->ps x " ")) (if (vector? arg) (vector->list arg) arg))) "]")) (else arg))) args))) ;;; Capture for %%Title (define *plot-title* #f) ;; Remember arrays so each is output only once. (define *plot-arrays* '()) ;;@args filename.eps size elt1 ... ;;@1 should be a string naming an output file to be created. @2 ;;should be an exact integer, a list of two exact integers, or #f. ;;@3, ... are values returned by graphing primitives described here. ;; ;;@0 creates an @dfn{Encapsulated-PostScript} file named @1 containing ;;graphs as directed by the @3, ... arguments. ;; ;;The size of the graph is determined by the @2 argument. If a list ;;of two integers, they specify the width and height. If one integer, ;;then that integer is the width and the height is 3/4 of the width. ;;If #f, the graph will be 800 by 600. (define (create-postscript-graph filename size . args) (define xsize (cond ((pair? size) (car size)) ((number? size) size) (else 800))) (let ((ysize (if (and (pair? size) (pair? (cdr size))) (cadr size) (quotient (* 3 xsize) 4)))) (cond ((provided? 'inexact) (set! xsize (inexact->exact (round xsize))) (set! ysize (inexact->exact (round ysize))))) (call-with-output-file filename (lambda (oprt) (define (write-lines lines) (for-each (lambda (line) (if (list? line) (write-lines line) (write-line line oprt))) lines)) (write-line "%!PS-Adobe-3.0 EPSF-3.0" oprt) (write-line (scheme->ps "%%BoundingBox: 0 0 " xsize " " ysize) oprt) (write-line (scheme->ps "%%Title: " (or *plot-title* filename)) oprt) (write-line (scheme->ps "%%EndComments: ") oprt) (write-line (scheme->ps "0 0 " xsize " " ysize) oprt) (call-with-input-file (in-vicinity (library-vicinity) "grapheps.ps") (lambda (iprt) (do ((line (read-line iprt) (read-line iprt))) ((eof-object? line)) (write-line line oprt)))) (for-each (lambda (pair) (write-array-def (cdr pair) (car pair) oprt)) *plot-arrays*) (write-lines args) (newline oprt) (write-line "grestore" oprt) (write-line "end" oprt) (write-line "showpage" oprt))) (set! *plot-title* #f) (set! *plot-arrays* '()))) (define (write-array-def name array oprt) (define row-length (cadr (array-dimensions array))) (define idx 0) (set! idx row-length) (write-line (scheme->ps "/" name) oprt) (write-line "[" oprt) (display " [" oprt) (array-for-each (lambda (elt) (cond ((zero? idx) (write-line "]" oprt) (display " [" oprt))) (display (scheme->ps " " elt) oprt) (set! idx (modulo (+ 1 idx) row-length))) array) (write-line "]" oprt) (write-line "] def" oprt)) ;;; Arrays are named and cached in *plot-arrays*. (define (import-array array) (cond ((assq array *plot-arrays*) => cdr) (else (let ((name (gentemp))) (set! *plot-arrays* (cons (cons array name) *plot-arrays*)) name)))) ;;@noindent ;;These graphing procedures should be called as arguments to ;;@code{create-postscript-graph}. The order of these arguments is ;;significant; PostScript graphics state is affected serially from the ;;first @var{elt} argument to the last. ;;@body ;;Pushes a rectangle for the whole encapsulated page onto the ;;PostScript stack. This pushed rectangle is an implicit argument to ;;@code{partition-page} or @code{setup-plot}. (define (whole-page) 'whole-page) ;;@menu ;;* Column Ranges:: ;;* Drawing the Graph:: ;;* Graphics Context:: ;;* Rectangles:: ;;* Legending:: ;;* Legacy Plotting:: ;;* Example Graph:: ;;@end menu ;; ;;@node Column Ranges, Drawing the Graph, PostScript Graphing, PostScript Graphing ;;@subsubsection Column Ranges ;;@noindent ;;A @dfn{range} is a list of two numbers, the minimum and the maximum. ;;@cindex range ;;Ranges can be given explicity or computed in PostScript by ;;@code{column-range}. ;;@body ;;Returns the range of values in 2-dimensional @1 column @2. (define (column-range array k) (set! array (import-array array)) (scheme->ps array " " k " column-range")) ;;@body ;;Expands @1 by @2/100 on each end. (define (pad-range range p) (scheme->ps range " " p " pad-range")) ;;@body ;;Expands @1 to round number of ticks. (define (snap-range range) (scheme->ps range " snap-range")) ;;@args range1 range2 ... ;;Returns the minimal range covering all @1, @2, ... (define (combine-ranges rng1 . rngs) (define (loop rngs) (cond ((null? rngs) "") (else (scheme->ps " " (car rngs) (loop (cdr rngs)) " combine-ranges")))) (scheme->ps rng1 (loop rngs))) ;;@args x-range y-range pagerect ;;@args x-range y-range ;;@1 and @2 should each be a list of two numbers or the value returned ;;by @code{pad-range}, @code{snap-range}, or @code{combine-range}. ;;@3 is the rectangle bounding the graph to be drawn; if missing, the ;;rectangle from the top of the PostScript stack is popped and used. ;; ;;Based on the given ranges, @0 sets up scaling and margins for making ;;a graph. The margins are sized proportional to the @var{fontheight} ;;value at the time of the call to setup-plot. @0 sets two variables: ;; ;;@table @var ;;@item plotrect ;;The region where data points will be plotted. ;;@item graphrect ;;The @3 argument to @0. Includes plotrect, legends, etc. ;;@end table (define (setup-plot xrange yrange . pagerect) (if (null? pagerect) (scheme->ps xrange " " yrange " setup-plot") (scheme->ps (car pagerect) " " xrange " " yrange " setup-plot"))) ;;@node Drawing the Graph, Graphics Context, Column Ranges, PostScript Graphing ;;@subsubsection Drawing the Graph ;;@body ;;Plots points with x coordinate in @2 of @1 and y coordinate @3 of ;;@1. The symbol @4 specifies the type of glyph or drawing style for ;;presenting these coordinates. (define (plot-column array x-column y-column proc3s) (set! array (import-array array)) (scheme->ps "[ " array " " x-column " " y-column " ] " proc3s " plot-column")) ;;@noindent ;;The glyphs and drawing styles available are: ;; ;;@table @code ;;@item line ;;Draws line connecting points in order. ;;@item mountain ;;Fill area below line connecting points. ;;@item cloud ;;Fill area above line connecting points. ;;@item impulse ;;Draw line from x-axis to each point. ;;@item bargraph ;;Draw rectangle from x-axis to each point. ;;@item disc ;;Solid round dot. ;;@item point ;;Minimal point -- invisible if linewidth is 0. ;;@item square ;;Square box. ;;@item diamond ;;Square box at 45.o ;;@item plus ;;Plus sign. ;;@item cross ;;X sign. ;;@item triup ;;Triangle pointing upward ;;@item tridown ;;Triangle pointing downward ;;@item pentagon ;;Five sided polygon ;;@item circle ;;Hollow circle ;;@end table ;;@node Graphics Context, Rectangles, Drawing the Graph, PostScript Graphing ;;@subsubsection Graphics Context ;;@body ;;Saves the current graphics state, executes @1, then restores ;;to saved graphics state. (define (in-graphic-context . args) (append '("gpush") args '("gpop"))) ;;@args color ;;@1 should be a string naming a Resene color, a saturate color, or a ;;number between 0 and 100. ;; ;;@0 sets the PostScript color to the color of the given string, or a ;;grey value between black (0) and white (100). (define (set-color clrn) (define clr (cond ((color? clrn) clrn) ((number? clrn) (* 255/100 clrn)) ((or (eq? 'black clrn) (and (string? clrn) (string-ci=? "black" clrn))) 0) ((or (eq? 'white clrn) (and (string? clrn) (string-ci=? "white" clrn))) 255) (else (or (saturate clrn) (resene clrn) (string->color (if (symbol? clrn) (symbol->string clrn) clrn)))))) (define (num->str x) (define num (inexact->exact (round (+ 1000 (* x 999/255))))) (scheme->ps "." (substring (number->string num) 1 4) " ")) (cond ((number? clr) (string-append (num->str clr) " setgray")) (clr (apply scheme->ps (append (map num->str (color->sRGB clr)) '(setrgbcolor)))) (else ""))) ;;@body ;;@1 should be a (case-sensitive) string naming a PostScript font. ;;@2 should be a positive real number. ;; ;;@0 Changes the current PostScript font to @1 with height equal to ;;@2. The default font is Helvetica (12pt). (define (set-font name fontheight) (scheme->ps "/fontsize " fontheight " def /" name " fontsize selectfont")) ;;@noindent ;;The base set of PostScript fonts is: ;; ;;@multitable @columnfractions .20 .25 .25 .30 ;;@item Times @tab Times-Italic @tab Times-Bold @tab Times-BoldItalic ;;@item Helvetica @tab Helvetica-Oblique @tab Helvetica-Bold @tab Helvetica-BoldOblique ;;@item Courier @tab Courier-Oblique @tab Courier-Bold @tab Courier-BoldOblique ;;@item Symbol ;;@end multitable ;;@noindent ;;Line parameters do no affect fonts; they do effect glyphs. ;;@body ;;The default linewidth is 1. Setting it to 0 makes the lines drawn ;;as skinny as possible. Linewidth must be much smaller than ;;glyphsize for readable glyphs. (define (set-linewidth w) (scheme->ps w " setlinewidth")) ;;@args j k ;;Lines are drawn @1-on @2-off. ;;@args j ;;Lines are drawn @1-on @1-off. ;;@args ;;Turns off dashing. (define (set-linedash . args) (scheme->ps args " 0 setdash")) ;;@body ;;Sets the (PostScript) variable glyphsize to @1. The default ;;glyphsize is 6. (define (set-glyphsize w) (scheme->ps "/glyphsize " w " def")) ;;@noindent ;;The effects of @code{clip-to-rect} are also part of the graphic ;;context. ;;@node Rectangles, Legending, Graphics Context, PostScript Graphing ;;@subsubsection Rectangles ;;@noindent ;;A @dfn{rectangle} is a list of 4 numbers; the first two elements are ;;the x and y coordinates of lower left corner of the rectangle. The ;;other two elements are the width and height of the rectangle. ;;@body ;;Pushes a rectangle for the whole encapsulated page onto the ;;PostScript stack. This pushed rectangle is an implicit argument to ;;@code{partition-page} or @code{setup-plot}. (define (whole-page) 'whole-page) ;;@body ;;Pops the rectangle currently on top of the stack and pushes @1 * @2 ;;sub-rectangles onto the stack in decreasing y and increasing x order. ;;If you are drawing just one graph, then you don't need @0. (define (partition-page xparts yparts) (scheme->ps xparts " " yparts " partition-page")) ;;@body ;;The rectangle where data points should be plotted. @0 is set by ;;@code{setup-plot}. (define plotrect 'plotrect) ;;@body ;;The @var{pagerect} argument of the most recent call to ;;@code{setup-plot}. Includes plotrect, legends, etc. (define graphrect 'graphrect) ;;@body ;;fills @1 with the current color. (define (fill-rect rect) (scheme->ps rect " fill-rect")) ;;@body ;;Draws the perimiter of @1 in the current color. (define (outline-rect rect) (scheme->ps rect " outline-rect")) ;;@body ;;Modifies the current graphics-state so that nothing will be drawn ;;outside of the rectangle @1. Use @code{in-graphic-context} to limit ;;the extent of @0. (define (clip-to-rect rect) (scheme->ps rect " clip-to-rect")) ;;@node Legending, Legacy Plotting, Rectangles, PostScript Graphing ;;@subsubsection Legending ;;@args title subtitle ;;@args title ;;Puts a @1 line and an optional @2 line above the @code{graphrect}. (define (title-top title . subtitle) (set! *plot-title* title) (scheme->ps "(" title ") (" (if (null? subtitle) "" (car subtitle)) ") title-top")) ;;@args title subtitle ;;@args title ;;Puts a @1 line and an optional @2 line below the @code{graphrect}. (define (title-bottom title . subtitle) (set! *plot-title* title) (scheme->ps "(" title ") (" (if (null? subtitle) "" (car subtitle)) ") title-bottom")) ;;@body ;;These edge coordinates of @code{graphrect} are suitable for passing ;;as the first argument to @code{rule-horizontal}. (define topedge 'topedge) (define bottomedge 'bottomedge) ;;@body ;;These edge coordinates of @code{graphrect} are suitable for passing ;;as the first argument to @code{rule-vertical}. (define leftedge 'leftedge) (define rightedge 'rightedge) ;;@body ;;The margin-templates are strings whose displayed width is used to ;;reserve space for the left and right side numerical legends. ;;The default values are "-.0123456789". (define (set-margin-templates left right) (scheme->ps "/lmargin-template (" left ") def " "/rmargin-template (" right ") def")) ;;@body ;;Draws a vertical ruler with X coordinate @1 and labeled with string ;;@2. If @3 is positive, then the ticks are @3 long on the right side ;;of @1; and @2 and numeric legends are on the left. If @3 is ;;negative, then the ticks are -@3 long on the left side of @1; and @2 ;;and numeric legends are on the right. (define (rule-vertical x-coord text tick-width) (scheme->ps x-coord " (" text ") " tick-width " rule-vertical")) ;;@body ;;Draws a horizontal ruler with Y coordinate @1 and labeled with ;;string @2. If @3 is positive, then the ticks are @3 long on the top ;;side of @1; and @2 and numeric legends are on the bottom. If @3 is ;;negative, then the ticks are -@3 long on the bottom side of @1; and ;;@2 and numeric legends are on the top. (define (rule-horizontal y-coord text tick-height) (scheme->ps y-coord " (" text ") " tick-height " rule-horizontal")) ;;@body ;;Draws the y-axis. (define (y-axis) 'y-axis) ;;@body ;;Draws the x-axis. (define (x-axis) 'x-axis) ;;@body ;;Draws vertical lines through @code{graphrect} at each tick on the ;;vertical ruler. (define (grid-verticals) 'grid-verticals) ;;@body ;;Draws horizontal lines through @code{graphrect} at each tick on the ;;horizontal ruler. (define (grid-horizontals) 'grid-horizontals) ;;@node Legacy Plotting, Example Graph, Legending, PostScript Graphing ;;@subsubsection Legacy Plotting (define (graph:plot tmp data xlabel ylabel . histogram?) (set! histogram? (if (null? histogram?) #f (car histogram?))) (if (list? data) (let ((len (length data)) (nra (make-array (A:floR64b) (length data) 2))) (do ((idx 0 (+ 1 idx)) (lst data (cdr lst))) ((>= idx len) (set! data nra)) (array-set! nra (caar lst) idx 0) (array-set! nra (if (list? (cdar lst)) (cadar lst) (cdar lst)) idx 1)))) (create-postscript-graph tmp (or graph:dimensions '(600 300)) (whole-page) (setup-plot (column-range data 0) (apply combine-ranges (do ((idx (+ -1 (cadr (array-dimensions data))) (+ -1 idx)) (lst '() (cons (column-range data idx) lst))) ((< idx 1) lst)))) (outline-rect plotrect) (x-axis) (y-axis) (do ((idx (+ -1 (cadr (array-dimensions data))) (+ -1 idx)) (lst '() (cons (plot-column data 0 idx (if histogram? 'bargraph 'line)) lst))) ((< idx 1) lst)) (rule-vertical leftedge ylabel 10) (rule-horizontal bottomedge xlabel 10))) (define (functions->array vlo vhi npts . funcs) (let ((dats (make-array (A:floR32b) npts (+ 1 (length funcs))))) (define jdx 1) (array-index-map! (make-shared-array dats (lambda (idx) (list idx 0)) npts) (lambda (idx) (+ vlo (* (- vhi vlo) (/ idx (+ -1 npts)))))) (for-each (lambda (func) (array-map! (make-shared-array dats (lambda (idx) (list idx jdx)) npts) func (make-shared-array dats (lambda (idx) (list idx 0)) npts)) (set! jdx (+ 1 jdx))) funcs) dats)) (define (graph:plot-function tmp func vlo vhi . npts) (set! npts (if (null? npts) 200 (car npts))) (let ((dats (functions->array vlo vhi npts func))) (graph:plot tmp dats "" ""))) ;;@body ;;A list of the width and height of the graph to be plotted using ;;@code{plot}. (define graph:dimensions #f) ;;@args func x1 x2 npts ;;@args func x1 x2 ;;Creates and displays using @code{(system "gv tmp.eps")} an ;;encapsulated PostScript graph of the function of one argument @1 ;;over the range @2 to @3. If the optional integer argument @4 is ;;supplied, it specifies the number of points to evaluate @1 at. ;; ;;@defunx x1 x2 npts func1 func2 ... ;;Creates and displays an encapsulated PostScript graph of the ;;one-argument functions @var{func1}, @var{func2}, ... over the range ;;@var{x1} to @var{x2} at @var{npts} points. ;; ;;@defunx plot coords x-label y-label ;;@var{coords} is a list or vector of coordinates, lists of x and y ;;coordinates. @var{x-label} and @var{y-label} are strings with which ;;to label the x and y axes. (define (plot . args) (call-with-tmpnam (lambda (tmp) (cond ((procedure? (car args)) (apply graph:plot-function tmp args)) ((or (array? (car args)) (and (pair? (car args)) (pair? (caar args)))) (apply graph:plot tmp args)) (else (let ((dats (apply functions->array args))) (graph:plot tmp dats "" "")))) (system (string-append "gv '" tmp "'"))) ".eps")) ;;@node Example Graph, , Legacy Plotting, PostScript Graphing ;;@subsubsection Example Graph ;;@noindent ;;The file @file{am1.5.html}, a table of solar irradiance, is fetched ;;with @samp{wget} if it isn't already in the working directory. The ;;file is read and stored into an array, @var{irradiance}. ;; ;;@code{create-postscript-graph} is then called to create an ;;encapsulated-PostScript file, @file{solarad.eps}. The size of the ;;page is set to 600 by 300. @code{whole-page} is called and leaves ;;the rectangle on the PostScript stack. @code{setup-plot} is called ;;with a literal range for x and computes the range for column 1. ;; ;;Two calls to @code{top-title} are made so a different font can be ;;used for the lower half. @code{in-graphic-context} is used to limit ;;the scope of the font change. The graphing area is outlined and a ;;rule drawn on the left side. ;; ;;Because the X range was intentionally reduced, ;;@code{in-graphic-context} is called and @code{clip-to-rect} limits ;;drawing to the plotting area. A black line is drawn from data ;;column 1. That line is then overlayed with a mountain plot of the ;;same column colored "Bright Sun". ;; ;;After returning from the @code{in-graphic-context}, the bottom ruler ;;is drawn. Had it been drawn earlier, all its ticks would have been ;;painted over by the mountain plot. ;; ;;The color is then changed to @samp{seagreen} and the same graphrect ;;is setup again, this time with a different Y scale, 0 to 1000. The ;;graphic context is again clipped to @var{plotrect}, linedash is set, ;;and column 2 is plotted as a dashed line. Finally the rightedge is ;;ruled. Having the line and its scale both in green helps ;;disambiguate the scales. ;;@example ;;(require 'eps-graph) ;;(require 'line-i/o) ;;(require 'string-port) ;; ;;(define irradiance ;; (let ((url "http://www.pv.unsw.edu.au/am1.5.html") ;; (file "am1.5.html")) ;; (define (read->list line) ;; (define elts '()) ;; (call-with-input-string line ;; (lambda (iprt) (do ((elt (read iprt) (read iprt))) ;; ((eof-object? elt) elts) ;; (set! elts (cons elt elts)))))) ;; (if (not (file-exists? file)) ;; (system (string-append "wget -c -O" file " " url))) ;; (call-with-input-file file ;; (lambda (iprt) ;; (define lines '()) ;; (do ((line (read-line iprt) (read-line iprt))) ;; ((eof-object? line) ;; (let ((nra (make-array (A:floR64b) ;; (length lines) ;; (length (car lines))))) ;; (do ((lns lines (cdr lns)) ;; (idx (+ -1 (length lines)) (+ -1 idx))) ;; ((null? lns) nra) ;; (do ((kdx (+ -1 (length (car lines))) (+ -1 kdx)) ;; (lst (car lns) (cdr lst))) ;; ((null? lst)) ;; (array-set! nra (car lst) idx kdx))))) ;; (if (and (positive? (string-length line)) ;; (char-numeric? (string-ref line 0))) ;; (set! lines (cons (read->list line) lines)))))))) ;; ;;(let ((xrange '(.25 2.5))) ;; (create-postscript-graph ;; "solarad.eps" '(600 300) ;; (whole-page) ;; (setup-plot xrange (column-range irradiance 1)) ;; (title-top ;; "Solar Irradiance http://www.pv.unsw.edu.au/am1.5.html") ;; (in-graphic-context ;; (set-font "Helvetica-Oblique" 12) ;; (title-top ;; "" ;; "Key Centre for Photovoltaic Engineering UNSW - Air Mass 1.5 Global Spectrum")) ;; (outline-rect plotrect) ;; (rule-vertical leftedge "W/(m^2.um)" 10) ;; (in-graphic-context (clip-to-rect plotrect) ;; (plot-column irradiance 0 1 'line) ;; (set-color "Bright Sun") ;; (plot-column irradiance 0 1 'mountain) ;; ) ;; (rule-horizontal bottomedge "Wavelength in .um" 5) ;; (set-color 'seagreen) ;; ;; (setup-plot xrange '(0 1000) graphrect) ;; (in-graphic-context (clip-to-rect plotrect) ;; (set-linedash 5 2) ;; (plot-column irradiance 0 2 'line)) ;; (rule-vertical rightedge "Integrated .W/(m^2)" -10) ;; )) ;; ;;(system "gv solarad.eps") ;;@end example