unit EZPlot; interface type realPoint = record x, y: extended; end; datray = array[1..1] of realPoint; datPtr = ^datray; dotItem = (plus, cross, points, square, circle, dot, lines, hist); dotType = set of dotItem; lineType = (solid, dotted); {, dash, dotDash} frameItem = (labels, numbers, ticks, majorGrid, minorGrid, box); frameType = set of frameItem; frame = record fRect: rect; xmin, xmax, ymin, ymax: extended; xOrderOfMag, yOrderOfMag: integer; dx, dy, delX, delY: extended; xLabel, yLabel: str255; end; procedure extremaOr (frameA, frameB: frame; var frameOut: frame); procedure chooseFrame (data: datPtr; numPts: integer; plotRect: rect; xLabel, yLabel: str255; var theFrame: frame; frameFormat: frameType; useGivenExtrema: boolean); procedure findExtremes (data: datPtr; numPts: integer; var theFrame: frame); procedure setTickIncrements (var theFrame: frame); procedure plotFrame (theFrame: frame; frameFormat: frameType; plotRect: rect); procedure plotData (data: datPtr; numPts: integer; theFrame: frame; dotFormat: dotType; lineFormat: lineType); procedure plot (data: datPtr; numPts: integer; plotRect: rect; xLabel, yLabel: str255; useGivenFrame: boolean; var theFrame: frame; frameFormat: frameType; dotFormat: dotType; lineFormat: lineType); implementation const lineSpacing = 1.5; { for text: double space = 2 } numLabelDigits = 4; { number of digits in numeric lables } majorTickDefault = 3; { default number of major ticks on x axis (number on y axis determined from aspect ratio } majTickPercentRatio = 1.0; { percent length (of longest plot side) of major tick } minTickPercentRatio = 0.5; { percent length (of longest plot side) of minor tick } crossPercentRatio = 1; { percent width of crosses/plusses plotted } dotPercentRatio = 1.3; { percent width of dots/circles/squares plotted } function frac (x: extended): extended; { returns the fractional portion of x } begin frac := x - trunc(x); end; function power (base, exponent: extended): extended; { returns base^exponent } begin power := exp(exponent * ln(base)); end; function log10 (x: extended): extended; const ln10 = 2.302585093; begin log10 := ln(x) / ln10; end; function screenX (x, xmax, xmin: extended; scrXmax, scrXmin: integer): integer; begin screenX := scrXmin + round((x - xmin) / (xmax - xmin) * (scrXmax - scrXmin)); end; function screenY (y, ymax, ymin: extended; scrYmax, scrYmin: integer): integer; begin screenY := scrYmin - round((y - ymin) / (ymax - ymin) * (scrYmin - scrYmax)); end; procedure StringDraw (theStr: str255; where: point; alignment: integer); { draws the string with the following alignment at point where: } { 781 } { 602 } { 543 } { i.e. alignment = 0 centers the string horizontally and vertically at where, while } { alignment = 1 left justifies above where and alignment = 5 right justifies below where } { NOTE: modify to allow for right angle orientation, too! } var width, fontHeight: integer; info: FontInfo; begin moveTo(where.h, where.v); { move to draw location } width := stringWidth(theStr); GetFontInfo(info); fontHeight := info.ascent; case alignment of { first, adjust vertical position } 7, 8, 1: ; 6, 0, 2: move(0, fontHeight div 2 - 1); 5, 4, 3: move(0, fontHeight); otherwise { error } ; end; case alignment of { next, adjust the horizontal position } 7, 6, 5: move(-width, 0); 8, 0, 4: move(-width div 2, 0); 1, 2, 3: ; otherwise { error } ; end; DrawString(theStr); end; procedure extremaOr (frameA, frameB: frame; var frameOut: frame); function max (a, b: extended): extended; begin if a > b then max := a else max := b; end; function min (a, b: extended): extended; begin if a < b then min := a else min := b; end; begin { extremaOr } with frameOut do begin xmin := min(frameA.xmin, frameB.xmin); xmax := max(frameA.xmax, frameB.xmax); ymin := min(frameA.ymin, frameB.ymin); ymax := max(frameA.ymax, frameB.ymax); end; end; { extremaOr } procedure findExtremes (data: datPtr; numPts: integer; var theFrame: frame); { finds the extremes of the data, then } { updates the xmin...ymax fields of theFrame accordingly } const smallReal = 1e-6; bigReal = 1e20; var i: integer; margin: extended; begin with theFrame do begin xmin := bigReal; xmax := -xmin; ymin := xmin; ymax := xmax; for i := 1 to numPts do begin { find the extrema of the data } if data^[i].x > xmax then xmax := data^[i].x; if data^[i].y > ymax then ymax := data^[i].y; if data^[i].x < xmin then xmin := data^[i].x; if data^[i].y < ymin then ymin := data^[i].y; end; { for } if abs(xmax - xmin) < smallReal then { in case there's no variation } begin xmax := xmax + 1; xmin := xmin - 1; end; if abs(ymax - ymin) < smallReal then begin ymax := ymax + 1; ymin := ymin - 1; end; end; { with } end; { findExtremes } procedure setTickIncrements (var theFrame: frame); { setTickIncrements finalizes the state of theFrame record. Values of dx..dely are decided upon } { to be the major/minor tick increments on the x and y axes. xmin..ymax are updated to fall } { on exact multiples of the minor tick increment. The frame fields xOrderOfMag and } { yOrderOfMag are also determined and set. } var numMajorTicks: integer; aspectRatio: extended; procedure chooseTickIncrement (max, min: extended; numMajorTicks: integer; var del, d: extended; var OrderOfMag: integer); var tickLength, tickMag, tempReal: extended; begin tickLength := max - min; { choose appropriate del and d: numMajorTicks major ticks } tickLength := tickLength / numMajorTicks; if tickLength > 1 then tickMag := power(10, trunc(log10(tickLength))) else tickMag := power(10, trunc(log10(tickLength)) - 1); tickLength := tickLength / tickMag; if tickLength <= 1.5 then begin del := tickMag; d := del / 5; end else if (tickLength > 1.5) and (tickLength <= 4) then begin del := 2 * tickMag; d := del / 4; end else if (tickLength > 4) and (tickLength <= 7) then begin del := 5 * tickMag; d := tickMag; end else { 7.5 < tickLength < 10 } begin tickMag := 10 * tickMag; del := tickMag; d := del / 5; end; tempReal := log10((abs(max) + abs(min)) / 2); if tempReal > 0 then OrderOfMag := trunc(tempReal) else OrderOfMag := trunc(tempReal - 1); end; begin { setTickIncrements } with theFrame do begin { first choose appropriate tick increments automatically } numMajorTicks := majorTickDefault; chooseTickIncrement(xmax, xmin, numMajorTicks, delx, dx, xOrderOfMag); aspectRatio := (fRect.right - fRect.left) / (fRect.bottom - fRect.top); { aspectRatio > 1 => landscape } numMajorTicks := round(numMajorTicks / aspectRatio); if numMajorTicks < 3 then numMajorTicks := 3; chooseTickIncrement(ymax, ymin, numMajorTicks, dely, dy, yOrderOfMag); { then adjust mins and maxs so that they fall on an even multiple of the minor tick length } xmin := dx * round(xmin / dx - 1.4); xmax := dx * round(xmax / dx + 1.4); ymin := dy * round(ymin / dy - 1.4); ymax := dy * round(ymax / dy + 1.4); end; end; procedure chooseFrame (data: datPtr; numPts: integer; plotRect: rect; xLabel, yLabel: str255; var theFrame: frame; frameFormat: frameType; useGivenExtrema: boolean); procedure chooseFrameRect (plotRect: rect; var theFrame: frame; frameFormat: frameType); { chooses a fRect based on whether the frameFormat includes labels, numeric labels, both } { or neither. The fRect's upper right corner coincides with the that of the plotRect, while } { the lower left corner of the fRect may lie within the plotRect if space is required for } { labels } var info: FontInfo; fontWidth, fontHeight: integer; xOffset, yOffset: integer; begin GetFontInfo(info); with info do fontHeight := ascent + descent + leading; fontWidth := info.widMax; xOffset := 0; yOffset := 0; if labels in frameFormat then { make space in left and lower margin for axis lables } begin xOffset := round(lineSpacing * fontHeight); yOffset := round(lineSpacing * fontHeight); end; if numbers in frameFormat then { make space for numeric lables } begin xOffset := xOffset + round((lineSpacing - 1) * fontHeight + numLabelDigits * fontWidth); yOffset := yOffset + round(lineSpacing * fontHeight); end; if (numbers in frameFormat) or (labels in frameFormat) then { add an extra Å half line } begin xOffset := xOffset + round((lineSpacing - 1) * fontHeight); yOffset := yOffset + round((lineSpacing - 1) * fontHeight); end; with plotRect do if (xOffset > right - left) or (yOffset > bottom - top) then ; { error - plotRect is too small for frameFormat, fontsize, &tc.} theFrame.fRect := plotRect; with theFrame.fRect do begin left := left + xOffset; bottom := bottom - yOffset; end; end; procedure chooseFrameLabels; begin if (labels in frameFormat) and (numbers in frameFormat) then begin if theFrame.xOrderOfMag <> 0 then theFrame.xLabel := concat(xLabel, ' [*10^', StringOf(theFrame.xOrderOfMag : 1), ']') else theFrame.xLabel := xLabel; if theFrame.yOrderOfMag <> 0 then theFrame.yLabel := concat(yLabel, ' *10^', StringOf(theFrame.yOrderOfMag : 1)) else theFrame.yLabel := yLabel; end else begin theFrame.xLabel := xLabel; theFrame.yLabel := yLabel; end; { else } end; begin { chooseFrame } chooseFrameRect(plotRect, theFrame, frameFormat); if not useGivenExtrema then findExtremes(data, numPts, theFrame); setTickIncrements(theFrame); chooseFrameLabels; end; { chooseFrame } procedure plotFrame (theFrame: frame; frameFormat: frameType; plotRect: rect); procedure plotGridNTicks; var majTickLength, minTickLength, longSide, height, width: integer; x, y, temp: extended; diagStripe: pattern; procedure initPat; { initialize the pattern needed for the major and minor grid lines } var i: integer; begin for i := 0 to 7 do begin if i mod 4 = 0 then diagStripe[i] := 136 else diagStripe[i] := diagStripe[i - 1] div 2; end; end; begin if (ticks in frameFormat) or (majorGrid in frameFormat) or (minorGrid in frameFormat) then begin if (minorGrid in frameFormat) then initPat; with theFrame.fRect do begin if (bottom - top) > (right - left) then { portrait mode } longSide := bottom - top else { landscape mode -- choose tickLength based on longer side! } longSide := right - left; end; majTickLength := round(majTickPercentRatio / 100 * longSide); minTickLength := round(minTickPercentRatio / 100 * longSide); with theFrame do begin { plot X ticks } height := fRect.bottom - fRect.top; moveTo(fRect.left, fRect.bottom); x := xmin; while x <= xmax + dx / 10 do begin temp := abs(frac(x / delx)); if (temp < 0.01) or (temp > 0.99) then { plot a major tick } begin if minorGrid in frameFormat then begin PenPat(diagStripe); line(0, -height); move(0, height); PenPat(black); end; if majorGrid in frameFormat then begin PenPat(gray); line(0, -height); move(0, height); PenPat(black); end; if ticks in frameFormat then begin line(0, -majTickLength); { draw lower tick mark } move(0, -height + majTickLength); line(0, majTickLength); { draw upper tick mark } move(0, height - majTickLength); end end { if... plot a major tick mark } else { plot a minor tick } begin if minorGrid in frameFormat then begin PenPat(diagStripe); line(0, -height); move(0, height); PenPat(black); end; if ticks in frameFormat then begin line(0, -minTickLength); { draw lower tick mark } move(0, -height + minTickLength); line(0, minTickLength); { draw upper tick mark } move(0, height - minTickLength); end; end; { plot a minor tick mark } x := x + dx; moveTo(screenX(x, xmax, xmin, fRect.right, fRect.left), fRect.bottom); end; { while x < xmax } { plot Y ticks } width := fRect.right - fRect.left; moveTo(fRect.left, fRect.bottom); y := ymin; while y <= ymax + dy / 10 do begin temp := abs(frac(y / dely)); if (temp < 0.01) or (temp > 0.99) then begin { plot a major tick } if minorGrid in frameFormat then begin PenPat(diagStripe); line(width, 0); move(-width, 0); PenPat(black); end; if majorGrid in frameFormat then begin PenPat(gray); line(width, 0); move(-width, 0); PenPat(black); end; if ticks in frameFormat then begin line(majTickLength, 0); move(width - majTickLength, 0); line(-majTickLength, 0); move(-width + majTickLength, 0); end; end else { plot a minor tick } begin if minorGrid in frameFormat then begin PenPat(diagStripe); line(width, 0); move(0, -width); PenPat(black); end; if ticks in frameFormat then begin line(minTickLength, 0); move(width - minTickLength, 0); line(-minTickLength, 0); move(-width + minTickLength, 0); end; end; y := y + dy; moveTo(fRect.left, screenY(y, ymax, ymin, fRect.top, fRect.bottom)); end; { while y < ymax } end; { with theFrame } end; { if ticks in frameFormat } end; { plotGridNTicks } procedure plotFrameBox; begin if box in frameFormat then with theFrame.fRect do FrameRect(top, left, bottom + 1, right + 1); end; procedure plotFrameNumbers; var x, y: extended; info: FontInfo; fontHeight: integer; where: point; procedure plotXnumbers; begin with theFrame do begin x := delx * round(xmin / delx); if x < xmin then x := x + delx; where.h := screenX(x, xmax, xmin, fRect.right, fRect.left); where.v := fRect.bottom + round(fontHeight * lineSpacing); while x <= xmax do begin if labels in frameFormat then StringDraw(StringOf(x / power(10, xOrderOfMag) : 3 : 2), where, 8) else if abs(xOrderOfMag) > 2 then StringDraw(StringOf(x), where, 8) else StringDraw(StringOf(x : 5 : 2), where, 8); x := x + delx; where.h := screenX(x, xmax, xmin, fRect.right, fRect.left); end; { while } end; { with theFrame } end; { plotXnumbers } procedure plotYnumbers; begin with theFrame do begin y := dely * round(ymin / dely); if y < ymin then y := y + dely; where.h := fRect.left - round((lineSpacing - 1) * fontHeight); where.v := screenY(y, ymax, ymin, fRect.top, fRect.bottom); while y <= ymax do begin if labels in frameFormat then StringDraw(StringOf(y / power(10, yOrderOfMag) : 3 : 2), where, 6) else if abs(yOrderOfMag) > 2 then StringDraw(StringOf(y), where, 6) else StringDraw(StringOf(y : 5 : 2), where, 6); y := y + dely; where.v := screenY(y, ymax, ymin, fRect.top, fRect.bottom); end; { while } end; { with theFrame } end; { plotXnumbers } begin { plotFramNumbers } if numbers in frameFormat then begin GetFontInfo(info); with info do fontHeight := ascent; plotXnumbers; plotYnumbers; end; end; { plotFramNumbers } procedure plotFrameLabels; var i, strLength, strPixLength, fontHeight, fontWidth: integer; info: FontInfo; where: point; x, y: extended; begin if labels in frameFormat then begin with theFrame do begin GetFontInfo(info); fontWidth := info.widMax; with info do fontHeight := ascent; { draw x Label } x := (xmax + xmin) / 2; where.h := screenX(x, xmax, xmin, fRect.right, fRect.left); if numbers in frameFormat then where.v := fRect.bottom + round(2 * fontHeight * lineSpacing) else where.v := fRect.bottom + round(fontHeight * lineSpacing); StringDraw(xLabel, where, 8); { draw y label } y := (ymax + ymin) / 2; strLength := length(yLabel); strPixLength := fontHeight * strLength; where.v := screenY(y, ymax, ymin, fRect.top, fRect.bottom) - strPixLength div 2; if numbers in frameFormat then where.h := fRect.left - round(lineSpacing * fontHeight + numLabelDigits * fontWidth) else where.h := fRect.left - round(lineSpacing * fontHeight); for i := 1 to strLength do begin StringDraw(yLabel[i], where, 2); where.v := where.v + fontHeight; end; end; { with theFrame... } end; { if labels in ... } end; { plotFrameLabels } begin { plotFrame } EraseRect(plotRect); plotGridNTicks; plotFrameBox; plotFrameNumbers; plotFrameLabels; end; { plotFrame } procedure plotData (data: datPtr; numPts: integer; theFrame: frame; dotFormat: dotType; lineFormat: lineType); var scrx, scry: integer; x, y: extended; saveRgn: RgnHandle; procedure plotLineNHist; var i: integer; begin case lineFormat of solid: PenPat(black); dotted: PenPat(gray); end; with theFrame do begin x := data^[1].x; y := data^[1].y; scrx := screenX(x, xmax, xmin, fRect.right, fRect.left); scry := screenY(y, ymax, ymin, fRect.top, fRect.bottom); moveTo(scrx, scry); if lines in dotFormat then for i := 2 to numPts do begin x := data^[i].x; y := data^[i].y; scrx := screenX(x, xmax, xmin, fRect.right, fRect.left); scry := screenY(y, ymax, ymin, fRect.top, fRect.bottom); LineTo(scrx, scry); end; x := data^[1].x; y := data^[1].y; scrx := screenX(x, xmax, xmin, fRect.right, fRect.left); scry := screenY(y, ymax, ymin, fRect.top, fRect.bottom); moveTo(scrx, scry); if hist in dotFormat then begin for i := 2 to numPts do begin x := data^[i].x; y := data^[i].y; scrx := screenX(x, xmax, xmin, fRect.right, fRect.left); LineTo(scrx, scry); scry := screenY(y, ymax, ymin, fRect.top, fRect.bottom); LineTo(scrx, scry); end; end; { if hist } end; { with theFrame } PenPat(black); end; { plotLineHist } procedure plotSquareNCircleNDot; var i, dotSize, longSide, offset: integer; dotRect: rect; begin with theFrame do begin with fRect do begin if (bottom - top) > (right - left) then { portrait mode } longSide := bottom - top else { landscape mode -- choose dotSize based on longer side! } longSide := right - left; end; dotSize := round(dotPercentRatio / 100 * longSide); if not odd(dotSize) then dotSize := dotSize + 1; offset := dotSize div 2; for i := 1 to numPts do begin x := data^[i].x; y := data^[i].y; scrx := screenX(x, xmax, xmin, fRect.right, fRect.left); scry := screenY(y, ymax, ymin, fRect.top, fRect.bottom); with dotRect do begin top := scry - offset; bottom := top + dotSize; right := scrx + offset + 1; left := right - dotSize; end; if dot in dotFormat then begin PenPat(Black); PaintOval(dotRect); end; if circle in dotFormat then begin EraseOval(dotRect); FrameOval(dotRect); end; if square in dotFormat then begin EraseRect(dotRect); FrameRect(dotRect); end; end; { for i... } end; { with theFrame } end; { plotCircles } procedure plotPlusNCrossNPoint; var i, longSide, offset, crossLength: integer; begin with theFrame do begin with fRect do begin if (bottom - top) > (right - left) then { portrait mode } longSide := bottom - top else { landscape mode -- choose crossLength based on longer side! } longSide := right - left; end; crossLength := round(crossPercentRatio / 100 * longSide); if odd(crossLength) then { want our crosses to be symmetrical } crossLength := crossLength + 1; offset := crossLength div 2; for i := 1 to numPts do begin x := data^[i].x; y := data^[i].y; scrx := screenX(x, xmax, xmin, fRect.right, fRect.left); scry := screenY(y, ymax, ymin, fRect.top, fRect.bottom); if plus in dotFormat then begin moveTo(scrx - offset, scry); line(crossLength, 0); moveTo(scrx, scry - offset); line(0, crosslength); end; if cross in dotFormat then begin moveTo(scrx - offset, scry - offset); line(crossLength, crossLength); moveTo(scrx + offset, scry - offset); line(-crossLength, crossLength); end; if points in dotFormat then begin PenMode(patXor); moveTo(scrx, scry); Line(0, 0); PenMode(patCopy); end; { if points... } end; { for i ... } end; { with theFrame } end; { plotPlusNCrossNPoint } begin { plotData } saveRgn := NewRgn; GetClip(saveRgn); ClipRect(theFrame.fRect); { to make sure plotting is clipped outside the fRect } if (lines in dotFormat) or (hist in dotFormat) then plotLineNHist; if (square in dotFormat) or (circle in dotFormat) or (dot in dotFormat) then plotSquareNCircleNDot; if (plus in dotFormat) or (cross in dotFormat) or (points in dotFormat) then plotPlusNCrossNPoint; SetClip(saveRgn); end; { plotData } procedure plot (data: datPtr; numPts: integer; plotRect: rect; xLabel, yLabel: str255; useGivenFrame: boolean; var theFrame: frame; frameFormat: frameType; dotFormat: dotType; lineFormat: lineType); const useGivenExtrema = false; begin if not useGivenFrame then chooseFrame(data, numPts, plotRect, xLabel, yLabel, theFrame, frameFormat, useGivenExtrema); plotFrame(theFrame, frameFormat, plotRect); plotData(data, numPts, theFrame, dotFormat, lineFormat); end; end. { unit EZPlot }