/*  curve.js:  mathematical drawing, currently 2-dimensional;
	e.g. curves, graphs, dots, regions, grid, labels;
	requires 'canvas' support & jQuery 1.4+ & jqmath.js.
	
	bds (bounds) is [low, high], xy or z is [x, y],
	h & v are horizontal & vertical (increasing downward) pixel coordinates,
	drawTgt2.crc2d/xs/ys/hR/vR & we assume there is no canvas transformation matrix,
	drawing routines may change crc2d.fillStyle/strokeStyle if needed,
	we say "dt2" for drawTgt2 and "crc" for Canvas Rendering Context
	
	Copyright 2011, Mathscribe, Inc.  Dual licensed under the MIT or GPL Version 2 licenses.
	See e.g. http://jquery.org/license for an explanation of these licenses.  */


"use strict";


(function() {
	var $ = jQuery, F = jsCurry, M = jqMath;
	
	
	M.maxA = function(a) { return Math.max.apply(null, a); };
	
	M.plusV = function(v, w) { return F.zipWith(function(x, y) { return x + y; }, v, w); };
	M.cTimesV = function(c, v) { return F.map(function(e) { return c * e; }, v); };
	
	M.evalCoefsPoly = function(a, x) /* sum(a[i]*x^i) */ {
		// use Horner's rule:
		var n = a.length, y = n ? a[--n] : 0.0;
		while (n)	// result = y*x^n + (the terms of degree < n)
			y = y*x + a[--n];
		return y;
	};
	M.derivCoefsPoly = function(a) {
		var res = [];
		for (var i = 1; i < a.length; i++)	res.push(i * a[i]);
		return res;
	};
	
	M.arithA = function(n, e, d) /* arithmetic sequence */ {
		if (e == null)	e = 0;
		if (d == null)	d = 1;
		
		var res = new Array(n);
		for (var i = 0; i < n; i++) {
			res[i] = e;
			e += d;
		}
		return res;
	};
	
	M.num = function(nod) {
		while (true) {
			if (nod.nodeType == 3 /* Text */)	return Number(nod.data);
			if (nod.tagName == 'INPUT' && nod.value)	return Number(nod.value);
			if (nod.childNodes.length == 1)	nod = nod.childNodes[0];
			else if ($(nod).children().length == 1)	nod = $(nod).children()[0];
			else	break;
		}
		return NaN;
	};
	M.vector = function(tr) { return F.map(M.num, tr.cells); };
	M.rowsVs = function(tbody) { return F.map(M.vector, tbody.rows); };
	
	M.numToS = function(x, maxNDigits, eps) {
		if (! maxNDigits)	maxNDigits = 8;
		if (eps == null)	eps = 1e-12;
		
		if (Math.abs(x) < eps)	return '0';
		return M.numS(x.toPrecision(maxNDigits), true);
	};
	M.plusS = function(x, y) /* x and y are strings; caller checks precedences */
		{ return x == '0' ? y : y == '0' ? x : /^[-\u2212]/.test(y) ? x+y : x+'+'+y; };
	M.negS = function(y) /* y is a string; caller checks precedences */
		{ return y == '0' ? '0' : /^[-\u2212]/.test(y) ? y.substring(1) : '\u2212'+y; };
	M.minusS = function(x, y) /* x and y are strings; caller checks precedences */
		{ return M.plusS(x, M.negS(y)); };
	M.timesS = function(x, y) /* x and y are strings; caller checks precedences */
		{ return x == '0' || y == '0' ? '0' : /^[-\u2212]?1$/.test(x) ? x.slice(0, -1)+y :
			y == '1' ? x : x+y; }
	M.coefsPolyS = function(a, varS, degsIncQ, maxNDigitsP, epsP) {
		if (varS == null)	varS = 'x';
		
		function termS(c, d) {
			var s = M.numToS(c, maxNDigitsP, epsP);
			if (s === '0')	return '';
			if (s.charAt(0) != '\u2212' /* &minus; */)	s = '+'+s;
			if (d == 0)	return s;
			var xd = d == 1 ? varS : varS+'^'+d;
			if (s === '+1' || s === '\u22121')	s = s.charAt(0);
			return s+xd;
		}
		var ts = F.map(termS, a);
		if (! degsIncQ)	ts = ts.reverse();
		return ts.join('').replace(/^\+/, '') || '0';
	};
	
	
	// M.dt2_ is the default drawTgt2.
	M.dt2SetYCoords = function(ys, dt2) {
		if (! dt2)	dt2 = M.dt2_;
		
		dt2.ys = ys;
		dt2.vR = parseInt(dt2.crc2d.canvas.height) / (ys[0] - ys[1]);
		return dt2;
	};
	M.newDrawTgt2 = function(canv, xs, ys) {
		var res = { crc2d: canv.getContext('2d'), xs: xs,
				hR: parseInt(canv.width) / (xs[1] - xs[0]) };
		return M.dt2SetYCoords(ys, res);
	};
	M.xyToHv = function(xy, dt2) {
		if (! dt2)	dt2 = M.dt2_;
		
		return [ dt2.hR * (xy[0] - dt2.xs[0]), dt2.vR * (xy[1] - dt2.ys[1]) ];
	};
	M.plot2 = function(xy, alphaP, dt2) {
		if (! dt2)	dt2 = M.dt2_;
		
		if (! isFinite(xy[0]) || ! isFinite(xy[1]))	return;
		var crc2d = dt2.crc2d, alpha0, hv = M.xyToHv(xy, dt2);
		if (alphaP != null) {
			alpha0 = crc2d.globalAlpha;
			crc2d.globalAlpha = alphaP;
		}
		var w = parseInt(crc2d.canvas.width), ht = parseInt(crc2d.canvas.height);
		if (hv[0] < 2)	hv[0] = 0.5*(hv[0] + 2);
		else if (hv[0] > w - 2)	hv[0] = 0.5*(hv[0] + w - 2);
		if (hv[1] < 2)	hv[1] = 0.5*(hv[1] + 2);
		else if (hv[1] > ht - 2)	hv[1] = 0.5*(hv[1] + ht - 2);
		crc2d.fillRect(hv[0] - 2, hv[1] - 2, 4, 4);
		if (alphaP != null)	crc2d.globalAlpha = alpha0;
	};
	M.moveTo2 = function(xy, beginPathQ, dt2) {
		if (! dt2)	dt2 = M.dt2_;
		
		var crc2d = dt2.crc2d, hv = M.xyToHv(xy, dt2);
		if (beginPathQ)	crc2d.beginPath();
		crc2d.moveTo(hv[0], hv[1]);
	};
	M.lineTo2 = function(xy, dt2) {
		if (! dt2)	dt2 = M.dt2_;
		
		var hv = M.xyToHv(xy, dt2);
		dt2.crc2d.lineTo(hv[0], hv[1]);
	};
	M.curve2 = function(derivs2F /* t |-> [z, z'] */, ts, beginPathQ, dt2) {
		if (! dt2)	dt2 = M.dt2_;
		
		var crc2d = dt2.crc2d, h1, v1;
		if (beginPathQ)	crc2d.beginPath();
		for (var i = 0; i < ts.length; i++) {
			var d2 = derivs2F(ts[i]), hv = M.xyToHv(d2[0], dt2), dh = d2[1][0] * dt2.hR / 3.0,
				dv = d2[1][1] * dt2.vR / 3.0, dt;
			if (i == 0)	crc2d.moveTo(hv[0], hv[1]);
			else {
				dt = ts[i] - ts[i - 1];
				crc2d.bezierCurveTo(h1, v1, hv[0] - dh*dt, hv[1] - dv*dt, hv[0], hv[1]);
			}
			if (i + 1 < ts.length) {
				dt = ts[i + 1] - ts[i];
				h1 = hv[0] + dh*dt;
				v1 = hv[1] + dv*dt;
			}
		}
	};
	M.fill = function(keepPathQ, dt2P) {
		var crc2d = (dt2P || M.dt2_).crc2d;
		crc2d.fill();
		if (! keepPathQ)	crc2d.beginPath();
	};
	M.stroke = function(keepPathQ, dt2P) {
		var crc2d = (dt2P || M.dt2_).crc2d;
		crc2d.stroke();
		if (! keepPathQ)	crc2d.beginPath();
	};
	
	M.graph2 = function(derivs2F /* x |-> [y, y'] */, ts, beginPathQ, dt2P) {
		M.curve2(function(x) { var d2 = derivs2F(x); return [[x, d2[0]], [1, d2[1]]]; }, ts,
			beginPathQ, dt2P);
	};
	M.graph2cubic = function(a, x0, beginPathQ, dt2) /* graphs sum(a[i]*(x-x0)^i) */ {
		a.length <= 4 || F.err(err_graph2cubic_);
		if (x0 == null)	x0 = 0;
		if (! dt2)	dt2 = M.dt2_;
		
		var a1 = M.derivCoefsPoly(a);
		function z2F(x)
			{ return [[x, M.evalCoefsPoly(a, x - x0)], [1, M.evalCoefsPoly(a1, x - x0)]]; }
		M.curve2(z2F, dt2.xs, beginPathQ, dt2);
	};
	M.circle = function(r, center, ts, beginPathQ, dt2P) /* circular arc */ {
		if (! ts)	ts = M.arithA(9, 0, Math.PI/4);	// err < r/1000, near 0 at critical points
		
		var a = center[0], b = center[1];
		function f2(t) {
			var x = r*Math.cos(t), y = r*Math.sin(t);
			return [[x + a, y + b], [- y, x]];
		};
		M.curve2(f2, ts, beginPathQ, dt2P);
	};
	
	M.choose_step_nDecs = function(bds) /* for e.g. clicking in a slider or graph */ {
		var d = bds[1] - bds[0], step = 0.1, nDecs = 1;
		if (isFinite(d) && d > 0.0) {
			while (d > 10.4) {
				d *= 0.1;
				step *= 10;
				--nDecs;
			}
			while (d < 1.04) {
				d *= 10;
				step *= 0.1;
				nDecs++;
			}
			var m = d > 5.2 ? 1 : d > 2.1 ? 2 : 5;	// => about 42-105 steps total
			step /= m;
			nDecs = Math.max(0, nDecs + (m > 1));
		} else	step = Infinity;
		return [step, nDecs];
	};
	M.choose_gridA_nDecs = function(bds) /* returns [vals, nDecs]; any 0 val will be exact */ {
		var t = M.choose_step_nDecs(bds), step = 10*t[0], nDecs = Math.max(t[1] - 1, 0);
		if (! isFinite(step))	return [[], 0];
		var a = Math.ceil(bds[0] / step - 1e-6), b = Math.floor(bds[1] / step + 1e-6);
		return [M.cTimesV(step, M.arithA((b - a + 1) || 0, a)), nDecs];
	};
	M.drawGrid = function(gridXs, gridYs, dt2) /* returns axisHv1Ps */ {
		if (! dt2)	dt2 = M.dt2_;
		
		var crc2d = dt2.crc2d, lineWidth0 = crc2d.lineWidth, xBds = dt2.xs, yBds = dt2.ys,
			xHv1P = null, yHv1P = null;
		crc2d.lineWidth = 0.5;
		crc2d.strokeStyle = '#00BBFF';
		F.iter(function(x) {
				if (x == 0)	return;
				var hv = M.xyToHv([x, yBds[1]], dt2);
				if (hv[0] < 1 || hv[0] > crc2d.canvas.width - 1)	return;
				M.moveTo2([x, yBds[0]], true, dt2);
				crc2d.lineTo(hv[0], hv[1]);
				crc2d.stroke();
			}, gridXs);
		F.iter(function(y) {
				var axisQ = y == 0, hv = M.xyToHv([xBds[1], y], dt2);
				if ((hv[1] < 1 || hv[1] > crc2d.canvas.height - 1) && ! axisQ)	return;
				M.moveTo2([xBds[0], y], true, dt2);
				crc2d.lineTo(hv[0], hv[1]);
				if (axisQ) {
					crc2d.moveTo(hv[0] - 6.5, hv[1] - 4);
					crc2d.lineTo(hv[0] - 0.5, hv[1]);
					crc2d.lineTo(hv[0] - 6.5, hv[1] + 4);
					crc2d.lineWidth = 0.667;
					crc2d.strokeStyle = '#001366';
					xHv1P = hv;
				}
				crc2d.stroke();
				if (axisQ) {
					crc2d.lineWidth = 0.5;
					crc2d.strokeStyle = '#00BBFF';
				}
			}, gridYs);
		if (F.elem(0, gridXs)) {
			var hv = M.xyToHv([0, yBds[1]], dt2);
			M.moveTo2([0, yBds[0]], true, dt2);
			crc2d.lineTo(hv[0], hv[1]);
			crc2d.moveTo(hv[0] - 4, 6.5);
			crc2d.lineTo(hv[0], 0.5);
			crc2d.lineTo(hv[0] + 4, 6.5);
			crc2d.lineWidth = 0.667;
			crc2d.strokeStyle = '#001366';
			crc2d.stroke();
			yHv1P = hv;
		}
		crc2d.lineWidth = lineWidth0;
		crc2d.beginPath();
		return [xHv1P, yHv1P];
	};
	
	
	// alignDs are measured from outside any border, like e.offsetWidth/offsetHeight not
	//	(box-sizing: content-box) e.style.width/height or e.style.left/top
	
	M.leftDF = F.constant(0);
	M.rightDF = F('offsetWidth');
	M.centerDF = function(g) { return g.offsetWidth / 2; };
	
	M.topDF = F.constant(0);
	M.bottomDF = F('offsetHeight');
	M.middleDF = function(g) { return g.offsetHeight / 2; };
	
	/*  A 'formatter' f applied to (e, ...) sets e's child nodes' positions within e, and e's
		width and height.  It assumes e is a positioned block-level element with default
		box-sizing, and its child nodes have position 'absolute' and no margins.  */
	
	M.formatRow = function(e, gap, alignDs, pad) /* a formatter; returns alignD - borderTop */ {
		if (! alignDs)	alignDs = M.middleDF;
		if (typeof alignDs == 'function')	alignDs = F.map(alignDs, e.childNodes);
		if (! pad)	pad = [0, 0];	// [left, top] in pixels
		e.childNodes.length == alignDs.length || F.err(err_formatRow_);
		
		if (! e.hasChildNodes()) {
			e.style.width = e.style.height = '0px';
			return 0;
		}
		var v0 = M.maxA(alignDs),
			cWHtDA = F.zipWith(function(d, c) { return [c, c.offsetWidth, c.offsetHeight, d]; },
					alignDs, e.childNodes),	// to minimize later browser reflows
			v1 = M.maxA(F.map(function(cWHtD) { return cWHtD[2] - cWHtD[3]; }, cWHtDA)),
			h = - gap;
		F.iter(function(cWHtD) {
				var c = cWHtD[0];
				h += gap;
				c.style.left = (pad[0] + h) + 'px';
				h += cWHtD[1];
				c.style.top = (pad[1] + v0 - cWHtD[3]) + 'px';
			}, cWHtDA);
		e.style.width = h + 'px';
		e.style.height = (v0 + v1) + 'px';
		return pad[1] + v0;
	};
	M.formatCol = function(e, gap, alignDs, pad) /* a formatter; returns alignD - borderLeft */
	{
		if (! alignDs)	alignDs = M.centerDF;
		if (typeof alignDs == 'function')	alignDs = F.map(alignDs, e.childNodes);
		if (! pad)	pad = [0, 0];	// [left, top] in pixels
		e.childNodes.length == alignDs.length || F.err(err_formatCol_);
		
		if (! e.hasChildNodes()) {
			e.style.width = e.style.height = '0px';
			return 0;
		}
		var h0 = M.maxA(alignDs),
			cWHtDA = F.zipWith(function(d, c) { return [c, c.offsetWidth, c.offsetHeight, d]; },
					alignDs, e.childNodes),	// to minimize later browser reflows
			h1 = M.maxA(F.map(function(cWHtD) { return cWHtD[1] - cWHtD[3]; }, cWHtDA)),
			v = - gap;
		F.iter(function(cWHtD) {
				var c = cWHtD[0];
				v += gap;
				c.style.top = (pad[1] + v) + 'px';
				v += cWHtD[2];
				c.style.left = (pad[0] + h0 - cWHtD[3]) + 'px';
			}, cWHtDA);
		e.style.height = v + 'px';
		e.style.width = (h0 + h1) + 'px';
		return pad[0] + h0;
	};
	
	M.formatRowOptsAt = function(e, hs, minGap, alignDs, hBdsP) /* a formatter; assumes e has no
			border or padding, and hs is nondecreasing; returns [startH, alignD] */ {
		if (typeof alignDs == 'function')	alignDs = F.map(alignDs, e.childNodes);
		hs.length == e.childNodes.length && hs.length == alignDs.length ||
			F.err(err_formatRowOptsAt_);
		
		var cWHtDOkA = [], startH, endH = - Infinity,
			cWHtDA = F.zipWith(function(d, c) { return [c, c.offsetWidth, c.offsetHeight, d]; },
					alignDs, e.childNodes);	// to minimize later browser reflows
		F.iter(function(cWHtD, h) {
				var c = cWHtD[0], w = cWHtD[1], h0 = h - w / 2, h1 = h0 + w;
				if (endH + minGap > h0 || hBdsP && (h0 < hBdsP[0] || hBdsP[1] < h1))
					e.removeChild(c);
				else {
					if (! cWHtDOkA.length)	startH = h0;
					endH = h1;
					cWHtDOkA.push(cWHtD);
					c.style.left = (h0 - startH) + 'px';
				}
			}, cWHtDA, hs);
		if (! e.hasChildNodes()) {
			e.style.width = e.style.height = '0px';
			return [0, 0];
		}
		e.style.width = (endH - startH) + 'px';
		
		var v0 = M.maxA(F.map(F(3), cWHtDOkA)),
			v1 = M.maxA(F.map(function(cWHtD) { return cWHtD[2] - cWHtD[3]; }, cWHtDOkA));
		F.iter(function(cWHtD) { cWHtD[0].style.top = (v0 - cWHtD[3]) + 'px'; }, cWHtDOkA);
		e.style.height = (v0 + v1) + 'px';
		return [startH, v0];
	};
	M.formatColOptsAt = function(e, vs, minGap, alignDs, vBdsP) /* a formatter; assumes e has no
			border or padding, and vs is nondecreasing; returns [startV, alignD] */ {
		if (typeof alignDs == 'function')	alignDs = F.map(alignDs, e.childNodes);
		vs.length == e.childNodes.length && vs.length == alignDs.length ||
			F.err(err_formatColOptsAt_);
		
		var cWHtDOkA = [], startV, endV = - Infinity,
			cWHtDA = F.zipWith(function(d, c) { return [c, c.offsetWidth, c.offsetHeight, d]; },
					alignDs, e.childNodes);	// to minimize later browser reflows
		F.iter(function(cWHtD, v) {
				var c = cWHtD[0], ht = cWHtD[2], v0 = v - ht / 2, v1 = v0 + ht;
				if (endV + minGap > v0 || vBdsP && (v0 < vBdsP[0] || vBdsP[1] < v1))
					e.removeChild(c);
				else {
					if (! cWHtDOkA.length)	startV = v0;
					endV = v1;
					cWHtDOkA.push(cWHtD);
					c.style.top = (v0 - startV) + 'px';
				}
			}, cWHtDA, vs);
		if (! e.hasChildNodes()) {
			e.style.width = e.style.height = '0px';
			return [0, 0];
		}
		e.style.height = (endV - startV) + 'px';
		
		var h0 = M.maxA(F.map(F(3), cWHtDOkA)),
			h1 = M.maxA(F.map(function(cWHtD) { return cWHtD[1] - cWHtD[3]; }, cWHtDOkA));
		F.iter(function(cWHtD) { cWHtD[0].style.left = (h0 - cWHtD[3]) + 'px'; }, cWHtDOkA);
		e.style.width = (h0 + h1) + 'px';
		return [startV, h0];
	};
	
	
	M.divAbs = function(innerP, classesSP, parentEltP, widthNP, docP)
			/* classesSP/parentEltP/widthNP/docP may each be omitted */ {
		if (classesSP != null && typeof classesSP != 'string') {
			if (docP)	F.err(err_divAbs_);
			docP = widthNP;
			widthNP = parentEltP;
			parentEltP = classesSP;
			classesSP = null;
		}
		if (parentEltP != null && parentEltP.nodeType != 1 /* Element */) {
			if (docP)	F.err(err_divAbs_);
			docP = widthNP;
			widthNP = parentEltP;
			parentEltP = null;
		}
		if (widthNP != null && typeof widthNP != 'number') {
			if (docP)	F.err(err_divAbs_);
			docP = widthNP;
			widthNP = null;
		}
		
		var e$ = $('<div/>', parentEltP ? parentEltP.ownerDocument : docP || document);
		if (innerP != null)	e$.append(innerP);
		e$[0].style.position = 'absolute';
		if (classesSP)	e$.addClass(classesSP);
		if (widthNP != null)	e$.width(widthNP);
		if (parentEltP != null)	e$.appendTo(parentEltP);
		return e$[0];
	};
	
	// See jqmath.css for css classes used below.
	
	function chooseNChars(bds, nDecs) /* soft bounds */ {
		function nc(x) { return x.toFixed(nDecs).length; }
		return Math.max(nc(bds[0]), nc(bds[1])) + 2;
	}
	
	M.numParam = function(resE, name, nChars, nDecs, userChangeF,
			options /* .[initVal/unitS] */) /* calls userChangeF(x, event, elt);
			defines F(resE).name/unitS/size/value functions */ {
		if (! options)	options = {};
		var initVal = options.initVal != null ? options.initVal : '',
			unitS = options.unitS || '';
		
		var doc = resE.ownerDocument,
			inputE = $('<input type=text>', doc).keyup(textF).change(textF)[0];
		$(resE).empty().addClass('ma-param-eq');
		var resOps = F(resE);
		resOps.name = F.constant(name);
		resOps.unitS = F.constant(unitS);
		resOps.size = function(nCharsP, nDecsP) {
			if (nCharsP == null)	return nChars;
			
			inputE.size = nChars = nCharsP;
			if (nDecsP != null)	nDecs = nDecsP;
		};
		resOps.value = function(valP) {
			if (valP != null)
				inputE.value = typeof valP == 'number' ? valP.toFixed(nDecs) : valP;
			return Number(inputE.value);
		};
		resOps.size(nChars, nDecs /* unnec. */);
		resOps.value(initVal);
		function textF(event) { userChangeF(Number(inputE.value), event, resE); }
		$(resE).append(M(name + '=', false, doc), inputE, unitS ? M(unitS, false, doc) : []);
		return resE;
	};
	
	M.sliderParam = function(resE, name, bds, userChangeF,
			options /* .[step/nDecs/initVal/unitS] */) /* calls userChangeF(x, event, elt);
			defines F(resE).name/unitS/bounds/value functions */ {
		if (! options)	options = {};
		var initVal = options.initVal != null ? options.initVal :
				bds[0] <= 0 && 0 <= bds[1] ? 0 : bds[0],
			unitS = options.unitS || '';
		
		var doc = resE.ownerDocument,
			slider$ = $('<div/>', doc).slider({ slide: slideF }),
			paramEqE = M.numParam($('<div/>', doc)[0], name, 1, 0, eqUserChangeF,
					{ unitS: unitS }),
			step, nDecs;
		$(resE).empty().addClass('ma-slider-param');
		var resOps = F(resE);
		resOps.name = F.constant(name);
		resOps.unitS = F.constant(unitS);
		resOps.bounds = function(bdsP, step1P, nDecs1P) {
			if (! bdsP)	return bds;
			
			bds = bdsP;
			step = step1P;
			nDecs = nDecs1P;
			if (! step || nDecs == null) {
				var t = M.choose_step_nDecs(bds);
				step = step || t[0];
				if (nDecs == null)	nDecs = t[1];
			}
			
			slider$.slider('option', { min: bds[0], max: bds[1], step: step });
			F(paramEqE).size(chooseNChars(bds, nDecs), nDecs);
		};
		resOps.bounds(bds, options.step, options.nDecs);
		resOps.value = function(valP) {
			var x = F(paramEqE).value(valP);
			if (valP != null)	slider$.slider('value', x);
			return x;
		};
		resOps.value(initVal);
		function slideF(event, info) {
			userChangeF(F(paramEqE).value(info.value), event, resE);
		}
		function eqUserChangeF(x, event) {
			slider$.slider('value', x);
			userChangeF(x, event, resE);
		}
		$(resE).append(slider$, paramEqE);
		return resE;
	};
	
	M.colors /* default dependent variable colors */ = [
		'#FF0000' /* 'red' */, '#0000FF' /* 'blue' */, '#008000' /* 'green' */, '#990099',
		'#FF9900', '#993300', '#003366', '#6600CC', '#99CC00', '#CC0033'
	];
	M.graphs = function(resE, indeps, deps, options)
			/* for visualizing a relation in R^n x R^m, e.g. a function R^n -> R^m;
			uses indeps[j].name/bounds/[step/nDecs/initVal/unitS],
			deps[i].name/bounds/f/draw/[color/nDigits/fuzz/fuzzEps/unitS],
			options.[gridWidth/gridHeight/userChangeF];
			calls f(xs, i), draw(xs, j, i, M.dt2_), userChangeF(x, j, event, elt);
			assumes resE is a positioned block-level element & in the document tree;
			defines F(resE).indepVals(xPsP, changedJP) */ {
		indeps.length && deps.length || F.err(err_labelsGrids_);
		if (! options)	options = {};
		var gridW = options.gridWidth || (indeps.length > 1 || deps.length > 2 ? 200 : 250),
			gridHt = options.gridHeight || gridW;
		
		var doc = resE.ownerDocument, left0 = resE.style.left;
		$(resE).css('left', '-10000px').empty().addClass('ma-graphs');
		var resOps = F(resE),
			ysLblsE = M.divAbs(null, resE),
			gridColsE = M.divAbs(null, resE),
			valsE = $(M.divAbs(null, 'ma-trace', resE)).append('<div>Trace at:</div>')[0],
			xValsE = $('<div/>', doc).addClass('ma-indeps-vals').appendTo(valsE)[0],
			yValsE = $('<div/>', doc).addClass('ma-deps-vals').appendTo(valsE)[0],
			xBdss = F.map(F('bounds'), indeps),
			yBdss = F.map(F('bounds'), deps),
			x_step_nDecs_A = F.map(M.choose_step_nDecs, xBdss),
			// y_step_nDecs_A = F.map(M.choose_step_nDecs, yBdss),
			gxs_nDecs_A = F.map(M.choose_gridA_nDecs, xBdss),
			gys_nDecs_A = F.map(M.choose_gridA_nDecs, yBdss),
			depAxisLabelQ = indeps.length == 1 && deps.length == 1 && F.elem(0, gxs_nDecs_A[0]);
		function gxyToE_f(nDecs)
			{ return function(x)
				{ return M.divAbs(M.numS(x.toFixed(nDecs)), 'ma-grid-label', doc); }; }
		function absVar(var1, parentEltP)
			{ return M.divAbs(M(var1.name, false, doc), 'ma-grid-var', parentEltP); }
		function buildDep(dep, i) /* returns alignDv */ {
			if (! dep.color)	dep.color = M.colors[i % M.colors.length];
			if (dep.nDigits == null)	dep.nDigits = 6;
			
			var gys_nDecs = gys_nDecs_A[i], gys = gys_nDecs[0].reverse(),
				yBds = dep.bounds, r = gridHt / (yBds[0] - yBds[1]),
				col = M.divAbs(null, ysLblsE),
				gysE = M.divAbs($(F.map(gxyToE_f(gys_nDecs[1]), gys)), col),
				vs = F.map(function(y) { return r * (y - yBds[1]); }, gys),
				startV = M.formatColOptsAt(gysE, vs, 2.9, M.rightDF)[0];
			if (! depAxisLabelQ)	absVar(dep, col);
			M.formatCol(col, 3);
			col.style.color = dep.color;
			return - startV;
		}
		var depDvs = F.map(buildDep, deps), ysLblsDv = M.formatRow(ysLblsE, 12, depDvs),
			lastYBds = deps[deps.length - 1].bounds, gridYs = gys_nDecs_A[deps.length - 1][0],
			canvEs = [], foci = [], xEqs = [];
		function buildIndep(indep, j) {
			var step_nDecs = x_step_nDecs_A[j];
			if (! indep.step)	indep.step = step_nDecs[0];
			if (indep.nDecs == null)	indep.nDecs = step_nDecs[1];
			if (indep.initVal == null) {
				var bds = indep.bounds;
				indep.initVal = bds[0] <= 0 && 0 <= bds[1] ? 0 : bds[0];
			}
			
			var gxs_nDecs = gxs_nDecs_A[j], gridXs = gxs_nDecs[0],
				xBds = indep.bounds, r = gridW / (xBds[1] - xBds[0]),
				col = M.divAbs(null, gridColsE), canvBoxE_lbls_E = M.divAbs(null, col),
				canvBoxE = $(M.divAbs(null, canvBoxE_lbls_E)).width(gridW).height(gridHt)[0],
				canvE = $('<div/>', doc).appendTo(canvBoxE)[0],
				gxsE = M.divAbs($(F.map(gxyToE_f(gxs_nDecs[1]), gridXs)), canvBoxE_lbls_E),
				hs = F.map(function(x) { return r * (x - xBds[0]); }, gridXs),
				startH = M.formatRowOptsAt(gxsE, hs, 4.9, M.bottomDF, [-15, gridW + 15])[0];
			M.formatCol(canvBoxE_lbls_E, 5, [0, - startH]);
			canvEs.push(canvE);
			foci.push($('<a href="#"></a>').addClass('ma-graph-handle').appendTo(canvBoxE).
					click(F.constant(false))[0]);
			if (F.elem(0, gridYs)) {
				var e = absVar(indep, canvBoxE);
				e.style.left = (gridW - e.offsetWidth) + 'px';
				var v = gridHt * (lastYBds[1] / (lastYBds[1] - lastYBds[0]));
				if (v + 5 + e.offsetHeight <= gridHt)	e.style.top = (v + 5) + 'px';
				else	e.style.top = (v - 7 - e.offsetHeight) + 'px';
			} else	absVar(indep, col);
			M.formatCol(col, 5);
			if (depAxisLabelQ) {
				var e = absVar(deps[0], canvBoxE);
				e.style.top = '0px';
				var h1 = gridW * (xBds[0] / (xBds[0] - xBds[1])), h = h1 - 7 - e.offsetWidth;
				e.style.left = (h >= 0 ? h : h1 + 7) + 'px';
				e.style.color = deps[0].color;
			}
			
			var eqE = M.numParam($('<div/>', doc)[0], indep.name,
					chooseNChars(xBds, indep.nDecs), indep.nDecs, eqUserChangeF,
					{ unitS: indep.unitS });
			$(xValsE).append(eqE);
			xEqs.push(eqE);
		}
		F.iter(buildIndep, indeps);
		M.formatRow(gridColsE, 12, M.topDF);
		
		resOps.indepVals = function(xPsP, changedJP) {
			var xs = F.map(function(eqE, j) { return F(eqE).value(xPsP && xPsP[j]); }, xEqs);
			if (xPsP == null)	return xs;
			
			$(yValsE).empty();
			F.iter(function(dep, i) {
					function fuzzS(fuzzP) {
						if (fuzzP == null)	return '';
						var s = M.numToS(fuzzP, 4, dep.fuzzEps || 1e-10);
						return s == '0' ? '' : '\xB1' /* ± */ + '{~' + s + '}';
					}
					var y = dep.f(xs, i);
					if (typeof y == 'number')	y = [y];
					if (Array.isArray(y))
						y = dep.name + '=' +
							F.map1(F([F._, dep.nDigits], M.numToS), y).join(',') +
							fuzzS(dep.fuzz) + (dep.unitS ? '{'+dep.unitS+'}' : '');
					if (typeof y == 'string')	y = M(y, false, doc);
					if (y != null)
						$('<div/>', doc).addClass('ma-dep-vals').append(y).appendTo(yValsE);
				}, deps);
			M.formatRow(resE, 12, [ysLblsDv, 0, 0]);
			F.iter(function(canvE, focE, j) {
					var xBds = indeps[j].bounds;
					if (j != changedJP) {
						var canv =
							$('<canvas/>', doc).attr({ width: gridW, height: gridHt }).
								css({ position: 'absolute', left: '-10000px' }).
								insertAfter(canvE)[0];	// apparently nec. for excanvas
						if (! canv.getContext)	G_vmlCanvasManager.initElement(canv);
						M.dt2_ = M.newDrawTgt2(canv, xBds, lastYBds);
						M.drawGrid(gxs_nDecs_A[j][0], gridYs);
						var crc2d = M.dt2_.crc2d;
						F.iter(function(dep, i) {
								M.dt2SetYCoords(dep.bounds);
								crc2d.strokeStyle = crc2d.fillStyle = dep.color;
								try {
									dep.draw(xs, j, i, M.dt2_);
								} catch(exc) {}
								crc2d.beginPath();
							}, deps);
						$(canv).detach().css('left', '0');
						canvE.parentNode.replaceChild(canv, canvE);
						canvEs[j] = canv;
					}
					if (j == changedJP || changedJP == null) {
						var h = gridW * ((xs[j] - xBds[0]) / (xBds[1] - xBds[0]));
						focE.style.left = (h - 0.5 * $(focE).width()) + 'px';
					}
				}, canvEs, foci);
			return xs;
		};
		resOps.indepVals(F.map(F('initVal'), indeps));
		
		function eqUserChangeF(x, event, eqE) {
			var j = F.elemIndex(eqE, xEqs);
			j != -1 || F.err(err_graphs_eqUserChangeF_);
			resOps.indepVals([], j);
			if (options.userChangeF)	options.userChangeF(x, j, event, resE);
		}
		function canvUserChangeF(x, j, event) /* caller does boundAlign() */ {
			var xPs = [];	xPs[j] = x;
			x = resOps.indepVals(xPs, j)[j];
			if (options.userChangeF)	options.userChangeF(x, j, event, resE);
		}
		F.iter(function(focE, j) {
				var indep = indeps[j], bds = indep.bounds, step = indep.step;
				function boundAlign(x) {
					step > 0 || F.err(err_boundAlign_);
					x += (x > 0 ? 0.5 : -0.5) * step;
					x -= x % step;
					return Math.max(bds[0], Math.min(bds[1], x));
				}
				$(focE).keydown(function(event) {
						var x = F(xEqs[j]).value(), d = step;
						if (event.keyCode == 37 /* Left */) {
							if (x == bds[0])	return;
							if (event.ctrlKey)	x = bds[0];
							d = - d;
						} else if (event.keyCode == 39 /* Right */) {
							if (x == bds[1])	return;
							if (event.ctrlKey)	x = bds[1];
						} else	return;
						if (event.altKey)	d *= 5;
						canvUserChangeF(boundAlign(x + d), j, event);
						return false;
					});
				var canvBoxE = focE.parentNode;
				function mouseF(event) {
					if (event.type != 'mousemove' || event.timeStamp > Date.now() - 100) {
						var r = (event.pageX - $(canvBoxE).offset().left) / gridW;
						canvUserChangeF(boundAlign(bds[0] + r*(bds[1] - bds[0])), j, event);
					}
				}
				function mouseupF(event) {
					mouseF(event);
					$(doc).unbind('mousemove', mouseF).unbind('mouseup', mouseupF);
				}
				$(canvBoxE).mousedown(function(event) {
						mouseF(event);
						$(doc).mousemove(mouseF).mouseup(mouseupF);
						focE.focus();
						return false;
					});
			}, foci);
		
		$(resE).css('left', left0);
		return resE;
	};
})();

