<div class="canvas" name="production">
<canvas id="production" width="300" height="200"></canvas>
<script id="chemical_kinetics.js">
(function(){
var production = function() {
	const k = 0.5;
	var x0 = [1];
	var f = [function(x) {return k;}];
	var label = ['A'];
	return {f, x0, label, xscale: 100, tscale: 2};
}();

var delay = function() {
	const k = 2;
	var x0 = [1];
	var f = [function(x) {return -k*x[0];}];
	var label = [''];
	return {f, x0, label, xscale: 100, tscale: 2};
}();

var production_and_delay = function() {
	const k0 = 0.5, k1 = 2;
	var x0 = [1];
	var f = [function(x) {return k0 - k1*x[0];}];
	var label = ['aₛₛ'];
	return {f, x0, label, xscale: 100, tscale: 2};
}();

var irreversible_conversion = function() {
	var k = 0.5;
	var x0 = [1,0];
	var f = [function(x) {return -k*x[0];},
			 function(x) {return +k*x[0];}];
	var label = ['aₛₛ','bₛₛ'];
	return {f, x0, label, xscale: 100, tscale: 2,
			seed: 'boundary', step: 1000, size: 1, arrow: [20, 500]};
}();

var reversible_conversion = function() {
	const k1 = 0.5, k_1 = 1;
	var x0 = [1,0];
	var f = [function(x) {return -k1*x[0]+k_1*x[1];},
	         function(x) {return +k1*x[0]-k_1*x[1];}];
	var label = ['aₛₛ','bₛₛ'];
	return {f, x0, label, xscale: 100, tscale: 2};
}();

var rapid_equilibrium_assumption = function() {
	const k1 = 1, k_1 = .5, k2 = .1;
	var x0 = [1,0];
	var f = [function(x) {return -k1*x[0]+k_1*x[1];},
	         function(x) {return +k1*x[0]-k_1*x[1]-k2*x[1];}];
	var label = ['',''];
	return {f, x0, label, xscale: 100, tscale: 10};
}();

var quasi_steady_state_assumption = function() {
	const k1 = 1, k_1 = .5, k0 = .1, k2 = .1;
	var x0 = [1,0];
	var f = [function(x) {return k0-k1*x[0]+k_1*x[1];},
	         function(x) {return   +k1*x[0]-k_1*x[1]-k2*x[1];}];
	var label = ['',''];
	return {f, x0, label, xscale: 100, tscale: 10};
}();

var inhibition = function() {
	var k1 = 1, k2 = 1, k3 = .1, k4 = .1;
	var K1 = 1, K2 = 1, n1 = 2, n2 = 2;
	var x0 = [10, 5];
	var f = [function(x) {return k1/(1 + Math.pow(x[1]/K2, n1)) - k3*x[0];},
			 function(x) {return k2                             - k4*x[1];}];
	var label = ['A', 'B'];
	return {f, x0, label, xscale: 15, tscale: 10,
			seed: 'boundary', step: 1000, size: 1, arrow: [20]};
}();

var cross_inhibition = function() {
	var k1 = 1, k2 = 1, k3 = .1, k4 = .1;
	var K1 = 1, K2 = 1, n1 = 2, n2 = 2;
	var x0 = [10, 5];
	var f = [function(x) {return k1/(1 + Math.pow(x[1]/K2, n1)) - k3*x[0];},
			 function(x) {return k2/(1 + Math.pow(x[0]/K1, n2)) - k4*x[1];}];
	var label = ['A', 'B'];
	return {f, x0, label, xscale: 15, tscale: 10,
			seed: 'boundary', step: 1000, size: 1, arrow: [20]};
}();

var Collins_toggle_switch = function() {
	var α0 = 1, α1 = 1, b = 2, c = 2, δ1 = 1, δ2 = 1;
	var x0 = [10,1];
	var f = [function(x) {return α0/(1+(x[1]**b)) - δ1*x[0];},
			 function(x) {return α1/(1+(x[0]**c)) - δ1*x[1];}];
	var label = ['',''];
	return {f, x0, label, xscale: 10, tscale: 10,
			seed: 'boundary', step: 1000, size: 1, arrow: [20]};
}();

var Goodwin_oscillator = function() {
	var a = 1, b = 1, c = 1, d = 1, e = 1, _f = 1;
	var k = .1, n = 10;
	var x0 = [15, 15, 15];
	var f = [function(x) {return a/((k**n)+(x[2]**n)) - b*x[0];},
			 function(x) {return c*x[0] - d*x[1];},
			 function(x) {return e*x[1] - _f*x[2];}];
	var label = ['','',''];
	return {f, x0, label, xscale: 10, tscale: 10,
			seed: 'boundary', step: 1000, size: 1, arrow: [20]};
}();

var Elowitz_Leibler_repressilator = function() {
	var α0 = .5, α = 1, β = 1, n = 10;
	var x0 = [0,0,0,1,0,0];
	var f = [function(x) {return α0 + α/(1+(x[5]**n)) - x[0];},
			 function(x) {return α0 + α/(1+(x[3]**n)) - x[1];},
			 function(x) {return α0 + α/(1+(x[4]**n)) - x[2];},
			 function(x) {return β*(x[0]-x[3]);},
			 function(x) {return β*(x[1]-x[4]);},
			 function(x) {return β*(x[2]-x[5]);}];
	var label = ['','','','','',''];
	return {f, x0, label, xscale: 100, tscale: 10,
			seed: 'boundary', step: 1000, size: 1, arrow: [20]};
}();

var autocatalysis = function() {
	const α = .5, K = 1, δ = .1;
	var x0 = [.1];
	var f = [function(x) {return α*(x[0]/K)/(1+x[0]/K) - δ*x[0];}];
	var label = [''];
	return {f, x0, label, xscale: 100, tscale: 10};
}();

var autoinhibition = function() {
	const α = .5, K = 1, δ = .1;
	var x0 = [.1];
	var f = [function(x) {return α/(1+x[0]/K) - δ*x[0];}];
	var label = [''];
	return {f, x0, label, xscale: 100, tscale: 10};
}();

	new Plot(production, 'TimeSeries', 'production');
	new Plot(delay, 'TimeSeries', 'delay');
	new Plot(production_and_delay, 'TimeSeries', 'production_and_delay');

	new Plot(irreversible_conversion, 'TimeSeries', 'irreversible_conversion');
	new Plot(reversible_conversion, 'TimeSeries', 'reversible_conversion');
	new Plot(rapid_equilibrium_assumption, 'TimeSeries', 'rapid_equilibrium_assumption');
	new Plot(quasi_steady_state_assumption, 'TimeSeries', 'quasi_steady_state_assumption');

	new Plot(inhibition, 'Phase', 'inhibition');
	new Plot(cross_inhibition, 'Phase', 'cross_inhibition');

	new Plot(Collins_toggle_switch, 'Phase', 'Collins_toggle_switch');
	new Plot(Goodwin_oscillator, 'TimeSeries', 'Goodwin_oscillator');
	new Plot(Elowitz_Leibler_repressilator, 'TimeSeries', 'Elowitz_Leibler_repressilator');

	new Plot(autocatalysis, 'TimeSeries', 'autocatalysis');
	new Plot(autoinhibition, 'TimeSeries', 'autoinhibition');

function Plot(pde, type, canvas_name) {
	var canvas = document.getElementById(canvas_name);
	var ctx    = canvas.getContext('2d');

	var [f, x0, label, xscale, tscale, seed, step, size, arrow] = Object.values(pde);

	var draw   = null;
	if      (type === 'TimeSeries') draw = DrawTimeSeries;
	else if (type === 'Phase')      draw = DrawPhase;

	var labelX, labelY;
	if      (type === 'TimeSeries') [labelX, labelY] = ['t', ' f(t)'];
	else if (type === 'Phase')      [labelX, labelY] = label;

	const _axis  = 10;	// offset for axis 
	const _label = 20;	// offset for label
	const width  = canvas.width  - _axis - _label;	// x-axis length
	const height = canvas.height - _axis - _label;	// y-axis length

	main();

	function main() {
		ctx.save();
		ctx.translate(0, canvas.height);
		ctx.scale(1, -1);
		ctx.translate(_axis, _axis);
		DrawXY(width, height, labelX, labelY);
		draw();
		ctx.restore();
	}

	function DrawTimeSeries() {
		const n = f.length;
		const dt = 0.01;
		const colors = ["rgb(192,0,0)", "rgb(0,192,0)", "rgb(0,0,192)",
                        "rgb(0,0,0,0)", "rgb(0,0,0,0)", "rgb(0,0,0,0)"];

		for (let k=n-1; k>=0; --k) {
			var x     = x0.slice(0);
			var xnext = x0.slice(0);

			// curve
			ctx.beginPath();
			for (let w=0; w<width; w++) {
				ctx.lineTo(w, x[k] * xscale);
				for (let t=0; t<tscale; ++t) {
					for (let i=0; i<n; ++i) xnext[i] = x[i] + f[i](x) * dt;
					for (let i=0; i<n; ++i) x[i] = xnext[i];

				}
			}
			ctx.strokeStyle = colors[k];
			ctx.lineWidth   = 2.25;
			ctx.stroke();

			// label
			ctx.save();
			ctx.scale(1, -1);
			ctx.font = "12pt Tahoma";
			ctx.textBaseline = "middle";
			ctx.textAlign = "left";
			ctx.fillStyle = "rgb(127,127,127)";
			ctx.fillText(label[k], width, -(x[k] * xscale));
			ctx.restore();
		}
	}

	function DrawPhase() {
		const n = f.length;
		if (n !== 2) return;
		const dt = 0.01;

		var x0s = [];
		if (seed === 'boundary') {
			var coords = [20, 60, 120, 180];
			for (let c of coords) {
				// axis label overlapping
				if (c < width) x0s.push([c,0]);
				if (c < height) x0s.push([0,c]);
				x0s.push([canvas.width-_axis,c], [c,canvas.height-_axis]);
			}
		} else if (seed === 'line') {
			var coords = [10, 20, 30, 40, 50];
			for (let c of coords) x0s.push([c,c]);
		}

		for (let x0 of x0s) {
			var x     = [x0[0] / xscale, x0[1] / xscale];
			var xnext = [x0[0] / xscale, x0[1] / xscale];

			// curve
			ctx.beginPath();
			for (let s=0; s<step; ++s) {
				ctx.lineTo(x[0] * xscale, x[1] * xscale);

				// arraw head
				if (arrow.includes(s)) {
					var slope = Math.atan2(f[1](x), f[0](x));
					var angle = Math.PI / 8;
					var len   = .35;
					var l0 = x[0] - len * Math.cos(slope-angle);
					var l1 = x[1] - len * Math.sin(slope-angle);
					var r0 = x[0] - len * Math.cos(slope+angle);
					var r1 = x[1] - len * Math.sin(slope+angle);
					ctx.lineTo(l0   * xscale, l1   * xscale);
					ctx.lineTo(r0   * xscale, r1   * xscale);
					ctx.lineTo(x[0] * xscale, x[1] * xscale);
				}

				for (let t=0; t<tscale; ++t) {
					for (let i=0; i<n; ++i) xnext[i] = x[i] + f[i](x) * dt * size;
					for (let i=0; i<n; ++i) x[i] = xnext[i];
				}
			}
			ctx.strokeStyle = "rgb(192,0,0)";
			ctx.lineWidth   = 1.5;
			ctx.stroke();
		}
	}

	function DrawArrow(x1, y1, x2, y2, len, angle, gap) {
		var slope = Math.atan2(y2 - y1, x2 - x1);
		x1 += gap * Math.cos(slope);
		y1 += gap * Math.sin(slope);
		x2 -= gap * Math.cos(slope);
		y2 -= gap * Math.sin(slope);

		ctx.beginPath();
		ctx.moveTo(x1, y1);
		ctx.lineTo(x2, y2);
		ctx.lineTo(x2 - len * Math.cos(slope-angle), y2 - len * Math.sin(slope-angle));
		ctx.moveTo(x2, y2);
		ctx.lineTo(x2 - len * Math.cos(slope+angle), y2 - len * Math.sin(slope+angle));
		ctx.stroke();
	}

	function DrawXY(scalarX, scalarY, labelX, labelY) {
		ctx.lineWidth = 1;
		ctx.strokeStyle = "gray";
		DrawArrow(0, 0, +scalarX, 0, 8, Math.PI / 5, 0);
		DrawArrow(0, 0, 0, +scalarY, 8, Math.PI / 5, 0);

		ctx.scale(1, -1);
		ctx.font = "12pt Arial";
		ctx.textAlign = "center";
		ctx.textBaseline = "middle";
		ctx.fillStyle = "gray";
		ctx.fillText(labelX, +scalarX+10, 0);
		ctx.textBaseline = "bottom";
		ctx.fillText(labelY, 0, -scalarY-2);
		ctx.scale(1, -1);
	}
}
})();
</script>
</div>