code: 9ferno

ref: b5959ac65bcd0d673e4e850f5a1dced6742536a8
dir: /appl/math/gr.b/

View raw version
implement GR;

include "sys.m";
	sys: Sys;
	print, sprint: import sys;
include "math.m";
	math: Math;
	ceil, fabs, floor, Infinity, log10, pow10, sqrt: import math;
include "draw.m";
	screen: ref Draw->Screen;
include "tk.m";
	tk: Tk;
	Toplevel: import tk;
include "tkclient.m";
	tkclient: Tkclient;
include "gr.m";

gr_cfg := array[] of {
	"frame .fc",
	"frame .fc.b",
	"label .fc.b.xy -text {0 0} -anchor e",
	"pack .fc.b.xy -fill x",
	"pack .fc.b -fill both -expand 1",
	"canvas .fc.c -relief sunken -bd 2 -width 600 -height 480 -bg white"+
		" -font /fonts/lucidasans/unicode.8.font",
	"pack .fc.c -fill both -expand 1",
	"pack .Wm_t -fill x",
	"pack .fc -fill both -expand 1",
	"pack propagate . 0",
	"bind .fc.c <ButtonPress-1> {send grcmd down1,%x,%y}",
};

TkCmd(t: ref Toplevel, arg: string): string
{
	rv := tk->cmd(t,arg);
	if(rv!=nil && rv[0]=='!')
		print("tk->cmd(%s): %s\n",arg,rv);
	return rv;
}


open(ctxt: ref Draw->Context, title: string): ref Plot
{
	if(sys==nil){
		sys = load Sys Sys->PATH;
		math = load Math Math->PATH;
		tk = load Tk Tk->PATH;
		tkclient = load Tkclient Tkclient->PATH;
		tkclient->init();
	}
	textsize := 8.;	# textsize is in points, if no user transform
	(t, tb) := tkclient->toplevel(ctxt, "", title, Tkclient->Appl);
	cc := chan of string;
	tk->namechan(t, cc, "grcmd");
	p := ref Plot(nil, Infinity,-Infinity,Infinity,-Infinity, textsize, t, tb, cc);
	for (i:=0; i<len gr_cfg; i++)
		tk->cmd(p.t,gr_cfg[i]);
	tkclient->onscreen(p.t, nil);
	tkclient->startinput(p.t, "kbd"::"ptr"::nil);
	return p;
}

Plot.bye(p: self ref Plot)
{
	cmdloop: for(;;) alt {
	s := <-p.t.ctxt.kbd =>
		tk->keyboard(p.t, s);
	s := <-p.t.ctxt.ptr =>
		tk->pointer(p.t, *s);
	s := <-p.t.ctxt.ctl or
	s = <-p.t.wreq or
	s = <-p.titlechan =>
		if(s == "exit")
			break cmdloop;
		tkclient->wmctl(p.t, s);
		case s{
		"size" =>
			canvw := int TkCmd(p.t, ".fc.c cget -width");
			canvh := int TkCmd(p.t, ".fc.c cget -height");
			TkCmd(p.t,".fc.b.xy configure -text {"+sprint("%d %d",canvw,canvh)+"}");
		}
	press := <-p.canvaschan =>
		(nil,cmds) := sys->tokenize(press,",");
		if(cmds==nil) continue;
		case hd cmds {
		"down1" =>
			xpos := real(hd tl cmds);
			ypos := real(hd tl tl cmds);
			x := (xpos-bx)/ax;
			y := -(ypos-tky+by)/ay;
			TkCmd(p.t,".fc.b.xy configure -text {"+sprint("%.3g %.3g",x,y)+"}");
		}
	}
	TkCmd(p.t,"destroy .;update");
	p.t = nil;
}

Plot.equalxy(p: self ref Plot)
{
	r := 0.;
	if( r < p.xmax - p.xmin ) r = p.xmax - p.xmin;
	if( r < p.ymax - p.ymin ) r = p.ymax - p.ymin;
	m := (p.xmax + p.xmin)/2.;
	p.xmax = m + r/2.;
	p.xmin = m - r/2.;
	m = (p.ymax + p.ymin)/2.;
	p.ymax = m + r/2.;
	p.ymin = m - r/2.;
}

Plot.graph(p: self ref Plot, x, y: array of real)
{
	n := len x;
	op := OP(GR->GRAPH, n, array[n] of real, array[n] of real, nil);
	while(n--){
		t := x[n];
		op.x[n] = t;
		if(t < p.xmin) 
			p.xmin = t;
		if(t > p.xmax) 
			p.xmax = t;
		t = y[n];
		op.y[n] = t;
		if(t < p.ymin) 
			p.ymin = t;
		if(t > p.ymax) 
			p.ymax = t;
	}
	p.op = op :: p.op;
}

Plot.text(p: self ref Plot, justify: int, s: string, x, y: real)
{
	op := OP(GR->TEXT, justify, array[1] of real, array[1] of real, s);
	op.x[0] = x;
	op.y[0] = y;
	p.op = op :: p.op;
}

Plot.pen(p: self ref Plot, nib: int)
{
	p.op = OP(GR->PEN, nib, nil, nil, nil) :: p.op;
}


#---------------------------------------------------------
# The rest of this file is concerned with sending the "display list"
# to Tk.  The only interesting parts of the problem are picking axes
# and drawing dashed lines properly.

ax, bx, ay, by: real;			# transform user to pixels
tky: con 630.;				# Tk_y = tky - y
nseg: int;				# how many segments in current stroke path
pendown: int;				# is pen currently drawing?
xoff := array[] of{"w","","e"};	# LJUST, CENTER, RJUST
yoff := array[] of{"n","","s","s"};	# HIGH, MED, BASE, LOW
linewidth: real;
toplevel: ref Toplevel;			# p.t
tkcmd: string;

mv(x, y: real)
{
	tkcmd = sprint(".fc.c create line %.1f %.1f", ax*x+bx, tky-(ay*y+by));
}

stroke()
{
	if(pendown){
		tkcmd += " -width 3";   # -capstyle round -joinstyle round
		TkCmd(toplevel,tkcmd);
		tkcmd = nil;
		pendown = 0;
		nseg = 0;
	}
}

vec(x, y: real)
{
	tkcmd += sprint(" %.1f %.1f", ax*x+bx, tky-(ay*y+by));
	pendown = 1;
	nseg++;
	if(nseg>1000){
		stroke();
		mv(x,y);
	}
}

circle(u, v, radius: real)
{
	x := ax*u+bx;
	y := tky-(ay*v+by);
	r := radius*(ax+ay)/2.;
	tkcmd = sprint(".fc.c create oval %.1f %.1f %.1f %.1f -width 3",
		x-r, y-r, x+r, y+r);
	TkCmd(toplevel,tkcmd);
	tkcmd = nil;
}

text(s: string, x, y: real, xoff, yoff: string)
{
	# rot = rotation in degrees.  90 is used for y-axis
	# x,y are in PostScript coordinate system, not user
	anchor := yoff + xoff;
	if(anchor!="")
		anchor = "-anchor " + anchor + " ";
	tkcmd = sprint(".fc.c create text %.1f %.1f %s-text '%s",
		ax*x+bx,
		tky-(ay*y+by), anchor, s);
	TkCmd(toplevel,tkcmd);
	tkcmd = nil;
}

datarange(xmin, xmax, margin: real): (real,real)
{
	r := 1.e-30;
	if( r < 0.001*fabs(xmin) ) 
		r = 0.001*fabs(xmin);
	if( r < 0.001*fabs(xmax) ) 
		r = 0.001*fabs(xmax);
	if( r < xmax-xmin ) 
		r = xmax-xmin;
	r *= 1.+2.*margin;
	x0 :=(xmin+xmax)/2. - r/2.;
	return ( x0, x0 + r);
}

dashed(ndash: int, x, y: array of real)
{
	cx, cy: real;	# current position
	d: real;	# length undone in p[i],p[i+1]
	t: real;	# length undone in current dash
	n := len x;
	if(n!=len y || n<=0)
		return;

	# choose precise dashlen
	s := 0.;
	for(i := 0; i < n - 1; i += 1){
		u := x[i+1] - x[i];
		v := y[i+1] - y[i];
		s += sqrt(u*u + v*v);
	}
	i = int floor(real ndash * s);
	if(i < 2) 
		i = 2;
	dashlen := s / real(2 * i - 1);

	t = dashlen;
	ink := 1;
	mv(x[0], y[0]);
	cx = x[0];
	cy = y[0];
	for(i = 0; i < n - 1; i += 1){
		u := x[i+1] - x[i];
		v := y[i+1] - y[i];
		d = sqrt(u * u + v * v);
		if(d > 0.){
			u /= d;
			v /= d;
			while(t <= d){
				cx += t * u;
				cy += t * v;
				if(ink){
					vec(cx, cy);
					stroke();
				}else{
					mv(cx, cy);
				}
				d -= t;
				t = dashlen;
				ink = 1 - ink;
			}
			cx = x[i+1];
			cy = y[i+1];
			if(ink){
				vec(cx, cy);
			}else{
				mv(cx, cy);
			}
			t -= d;
		}
	}
	stroke();
}

labfmt(x:real): string
{
	lab := sprint("%.6g",x);
	if(len lab>2){
		if(lab[0]=='0' && lab[1]=='.')
			lab = lab[1:];
		else if(lab[0]=='-' && len lab>3 && lab[1]=='0' && lab[2]=='.')
			lab = "-"+lab[2:];
	}
	return lab;
}

Plot.paint(p: self ref Plot, xlabel, xunit, ylabel, yunit: string)
{
	oplist: list of OP;

	# tunable parameters for dimensions of graph (fraction of box side)
	margin: con 0.075;		# separation of data from box boundary
	ticksize := 0.02;
	sep := ticksize;		# separation of text from box boundary

	# derived coordinates of various feature points...
	x0, x1, y0, y1: real;		# box corners, in original coord
	# radius := 0.2*p.textsize;	# radius for circle marker
	radius := 0.8*p.textsize;	# radius for circle marker

	Pen := SOLID;
	width := SOLID;
	linewidth = 2.;
	nseg = 0;
	pendown = 0;

	if(xunit=="") xunit = nil;
	if(yunit=="") yunit = nil;

	(x0,x1) = datarange(p.xmin,p.xmax,margin);
	ax = (400.-2.*p.textsize)/((x1-x0)*(1.+2.*sep));
	bx = 506.-ax*x1;
	(y0,y1) = datarange(p.ymin,p.ymax,margin);
	ay = (400.-2.*p.textsize)/((y1-y0)*(1.+2.*sep));
	by = 596.-ay*y1;
	# PostScript version
	# magic numbers here come from BoundingBox: 106 196 506 596
	# (x0,x1) = datarange(p.xmin,p.xmax,margin);
	# ax = (400.-2.*p.textsize)/((x1-x0)*(1.+2.*sep));
	# bx = 506.-ax*x1;
	# (y0,y1) = datarange(p.ymin,p.ymax,margin);
	# ay = (400.-2.*p.textsize)/((y1-y0)*(1.+2.*sep));
	# by = 596.-ay*y1;

	# convert from fraction of box to PostScript units
	ticksize *= ax*(x1-x0);
	sep *= ax*(x1-x0);

	# revert to original drawing order
	log := p.op;
	oplist = nil;
	while(log!=nil){
		oplist = hd log :: oplist;
		log = tl log;
	}
	p.op = oplist;

	toplevel = p.t;
	#------------send display list to Tk-----------------
	while(oplist!=nil){
		op := hd oplist;
		n := op.n;
		case op.code{
		GRAPH =>
			if(Pen == DASHED){
				dashed(17, op.x, op.y);
			}else if(Pen == DOTTED){
				dashed(85, op.x, op.y);
			}else{
				for(i:=0; i<n; i++){
					xx := op.x[i];
					yy := op.y[i];
					if(Pen == CIRCLE){
						circle(xx, yy, radius/(ax+ay));
					}else if(Pen == CROSS){
						mv(xx-radius/ax, yy);
						vec(xx+radius/ax, yy);
						stroke();
						mv(xx, yy-radius/ay);
						vec(xx, yy+radius/ay);
						stroke();
					}else if(Pen == INVIS){
					}else{
						if(i==0){
							mv(xx, yy);
						}else{
							vec(xx, yy);
						}
					}
				}
				stroke();
			}
		TEXT =>
			angle := 0.;
			if(op.n&UP) angle = 90.;
			text(op.t,op.x[0],op.y[0],xoff[n&7],yoff[(n>>3)&7]);
		PEN =>
			Pen = n;
			if( Pen==SOLID && width!=SOLID ){
				linewidth = 2.;
				width=SOLID;
			}else if( Pen==REFERENCE && width!=REFERENCE ){
				linewidth = 0.8;
				width=REFERENCE;
			}
		}
		oplist = tl oplist;
	}

	#--------------------now add axes-----------------------
	mv(x0,y0);
	vec(x1,y0);
	vec(x1,y1);
	vec(x0,y1);
	vec(x0,y0);
	stroke();

	# x ticks
	(lab1,labn,labinc,k,u,s) := mytic(x0,x1);
	for (i := lab1; i <= labn; i += labinc){
		r := real i*s*u;
		mv(r,y0);
		vec(r,y0+ticksize/ay);
		stroke();
		mv(r,y1);
		vec(r,y1-ticksize/ay);
		stroke();
		text(labfmt(real i*s),r,y0-sep/ay,"","n");
	}
	yy := y0-(2.*sep+p.textsize)/ay;
	labelstr := "";
	if(xlabel!=nil)
		labelstr = xlabel;
	if(k!=0||xunit!=nil)
		labelstr += " /";
	if(k!=0)
		labelstr += " ₁₀"+ string k;
	if(xunit!=nil)
		labelstr += " " + xunit;
	text(labelstr,(x0+x1)/2.,yy,"","n");

	# y ticks
	(lab1,labn,labinc,k,u,s) = mytic(y0,y1);
	for (i = lab1; i <= labn; i += labinc){
		r := real i*s*u;
		mv(x0,r);
		vec(x0+ticksize/ax,r);
		stroke();
		mv(x1,r);
		vec(x1-ticksize/ax,r);
		stroke();
		text(labfmt(real i*s),x0-sep/ax,r,"e","");
	}
	xx := x0-(4.*sep+p.textsize)/ax;
	labelstr = "";
	if(ylabel!=nil)
		labelstr = ylabel;
	if(k!=0||yunit!=nil)
		labelstr += " /";
	if(k!=0)
		labelstr += " ₁₀"+ string k;
	if(yunit!=nil)
		labelstr += " " + yunit;
	text(labelstr,xx,(y0+y1)/2.,"e","");

	TkCmd(p.t, "update");
}



# automatic tic choice                      Eric Grosse  9 Dec 84
# Input: low and high endpoints of expanded data range
# Output: lab1, labn, labinc, k, u, s   where the tics are
#   (lab1*s, (lab1+labinc)*s, ..., labn*s) * 10^k
# and u = 10^k.  k is metric, i.e. k=0 mod 3.

max3(a, b, c: real): real
{
	if(a<b) a=b;
	if(a<c) a=c;
	return(a);
}

my_mod(i, n: int): int
{
	while(i< 0) i+=n;
	while(i>=n) i-=n;
	return(i);
}

mytic(l, h: real): (int,int,int,int,real,real)
{
	lab1, labn, labinc, k, nlab, j, ndig, t1, tn: int;
	u, s: real;
	eps := .0001;
	k = int floor( log10((h-l)/(3.+eps)) );
	u = pow10(k);
	t1 = int ceil(l/u-eps);
	tn = int floor(h/u+eps);
	lab1 = t1;
	labn = tn;
	labinc = 1;
	nlab = labn - lab1 + 1;
	if( nlab>5 ){
		lab1 = t1 + my_mod(-t1,2);
		labn = tn - my_mod( tn,2);
		labinc = 2;
		nlab = (labn-lab1)/labinc + 1;
		if( nlab>5 ){
			lab1 = t1 + my_mod(-t1,5);
			labn = tn - my_mod( tn,5);
			labinc = 5;
			nlab = (labn-lab1)/labinc + 1;
			if( nlab>5 ){
				u *= 10.; 
				k++;
				lab1 = int ceil(l/u-eps);
				labn = int floor(h/u+eps);
				nlab = labn - lab1 + 1;
				labinc = 1;
			} else if( nlab<3 ){
				lab1 = t1 + my_mod(-t1,4);
				labn = tn - my_mod( tn,4);
				labinc = 4;
				nlab = (labn-lab1)/labinc + 1;
			}
		}
	}
	ndig = int(1.+floor(log10(max3(fabs(real lab1),fabs(real labn),1.e-30))));
	if( ((k<=0)&&(k>=-ndig))   # no zeros have to be added
	    || ((k<0)&&(k>=-3))
	    || ((k>0)&&(ndig+k<=4)) ){   # even with zeros, label is small
		s = u;
		k = 0;
		u = 1.;
	}else if(k>0){
		s = 1.;
		j = ndig;
		while(k%3!=0){ 
			k--; 
			u/=10.; 
			s*=10.; 
			j++; 
		}
		if(j-3>0){ 
			k+=3; 
			u*=1000.; 
			s/=1000.; 
		}
	}else{ # k<0
		s = 1.;
		j = ndig;
		while(k%3!=0){ 
			k++; 
			u*=10.; 
			s/=10.; 
			j--; 
		}
		if(j<0){ 
			k-=3; 
			u/=1000.; 
			s*=1000.; 
		}
	}
	return (lab1, labn, labinc, k, u, s);
}