. */ /** * For more information, please contact */ namespace Goat1000\SVGGraph; /** * Class for calculating a curved best-fit line */ class BestFitCurve { protected $graph; protected $points; protected $line; protected $projection; protected $types; public function __construct(&$graph, $points, $types = null) { $this->graph =& $graph; $this->line = new PathData; $this->projection = new PathData; $this->points = $points; $this->types = $types === null || is_array($types) ? $types : [$types]; } /** * Calculates the line and projection */ public function calculate(BoundingBox $area, $limit_start, $limit_end, $project_start, $project_end) { // can't draw a line through fewer than 2 points $count = count($this->points); if($count < 2) return false; $old_scale = bcscale(50); $b = []; $y = new Matrix($count, 1); foreach($this->points as $p) $b[] = $p->y; $y->load($b); $supported_types = ['straight', 'quadratic', 'cubic', 'quartic', 'quintic']; // choose which functions to fit if($this->types !== null) { $types = []; foreach($this->types as $type) { if(!in_array($type, $supported_types)) throw new \Exception("Unknown curve type '{$type}'"); $types[] = $type; } } else { $types = ['quintic']; switch($count) { case 2 : $types = ['straight']; break; case 3 : $types = ['quadratic']; break; case 4 : $types = ['cubic']; break; case 5 : $types = ['quartic']; } } // fit the functions, measure the error $results = []; $errors = []; foreach($types as $t) { $v = $this->vandermonde($t); $result = $this->solve($v, $y); if($result !== null) { $errors[$t] = $this->error($v, $result); $results[$t] = $result; } } if(!empty($errors)) { // sort by error, best first uasort($errors, 'bccomp'); $best = null; foreach($errors as $k => $v) { $best = $k; break; } $r = $results[$best]; $c = $r->asArray(); // plot the function $fn = new Algebraic($best); $fn->setCoefficients($c); $this->buildPaths($fn, $area, $limit_start, $limit_end, $project_start, $project_end); } bcscale($old_scale); } /** * Calculates the error */ protected function error(Matrix $v, Matrix $r) { $old_scale = bcscale(50); $tr = $r->transpose(); $vr = $v->multiply($tr); $i = 0; $err = "0"; foreach($this->points as $p) { $diff = bcsub($vr($i, 0), $p->y); if(bccomp($diff, "0") == -1) $err = bcsub($err, $diff); else $err = bcadd($err, $diff); ++$i; } bcscale($old_scale); return $err; } /** * Solves the normal equation */ protected function solve(Matrix $v, Matrix $y) { $v_t = $v->transpose(); $v_t_v = $v_t->multiply($v); $v_t_y = $v_t->multiply($y); return $v_t_v->gaussian_solve($v_t_y); } /** * Returns the Vandermonde matrix for the curve type */ protected function vandermonde($type) { $old_scale = bcscale(50); // find size of matrix $a = new Algebraic($type); $test = $a->vandermonde(1); $m = count($this->points); $n = count($test) + 1; $v = new Matrix($m, $n); $i = 0; foreach($this->points as $p) { $v($i, 0, 1); $bcx = sprintf("%20.20F", $p->x); $cols = $a->vandermonde($bcx); foreach($cols as $k => $value) $v($i, $k + 1, $value); ++$i; } bcscale($old_scale); return $v; } /** * Builds the line and projection paths. * For vertical lines, $slope = null and $y_int = $x */ protected function buildPaths(&$fn, $area, $limit_start, $limit_end, $project_start, $project_end) { // initialize min and max points of line $x_min = $limit_start === null ? 0 : max($limit_start, 0); $x_max = $limit_end === null ? $area->width() : min($limit_end, $area->width()); $y_min = 0; $y_max = $area->height(); $line = new PathData; $projection = new PathData; $step = 1; if($project_start) $this->buildPath($projection, $fn, 0, $x_min, $y_min, $y_max, $step, $area); $this->buildPath($line, $fn, $x_min, $x_max, $y_min, $y_max, $step, $area); if($project_end) $this->buildPath($projection, $fn, $x_max, $area->width(), $y_min, $y_max, $step, $area); $this->projection = $projection; $this->line = $line; return true; } /** * Builds a single path section between $x1 and $x2 */ private function buildPath(&$path, &$fn, $x1, $x2, $y_min, $y_max, $step, $area) { $cmd = 'M'; for($x = $x1; $x <= $x2; $x += $step) { $y = $fn($x); if($y < $y_min || $y > $y_max) { $cmd = 'M'; continue; } $path->add($cmd, $area->x1 + $x, $area->y2 - $y); switch($cmd) { case 'M' : $cmd = 'L'; break; case 'L' : $cmd = ''; break; } } } /** * Returns the best-fit line as PathData */ public function getLine() { return $this->line; } /** * Returns the projection line(s) as PathData */ public function getProjection() { return $this->projection; } }