Here’s another implementation to throw into the mix. 
First, some math functions:
final class Math {
const PI = 3.1415926535897932384626433832795028;
static public function clamp($value, $low, $high) { return min($high, max($low, $value)); }
static public function median(array $numbers) {
$n = count($numbers);
if ($n % 2 == 1) {
return $numbers[$n/2];
} else {
return ($numbers[(int)(($n-1)/2)] + $numbers[(int)(($n-1)/2)+1]) / 2;
}
}
}
Now a simple class to represent colors. Notice the mixWith() function which mixes two colors together (the class is immutable) given an “amount.” If the amount is 0, the other color is not incorporated at all. If the amount is 1, the other color completely dominates.
class Color {
protected $_r;
protected $_g;
protected $_b;
protected $_longRepresentation;
protected $_hexRepresentation;
public function __construct($r, $g, $b) {
$this->_r = Math::clamp((int)$r, 0, 255);
$this->_g = Math::clamp((int)$g, 0, 255);
$this->_b = Math::clamp((int)$b, 0, 255);
$this->_longRepresentation = (($this->_r << 16) + ($this->_g << 8) + ($this->_b));
$this->_hexRepresentation = str_pad(dechex($this->_longRepresentation), 6, '0', STR_PAD_LEFT);
}
public function getR() { return $this->_r; }
public function getG() { return $this->_g; }
public function getB() { return $this->_b; }
public function getLong() { return $this->_longRepresentation; }
public function getHex() { return $this->_hexRepresentation; }
public function mixWith(Color $other, $amount) {
$amount = Math::clamp((float)$amount, 0.0, 1.0);
return new Color(
($other->getR() - $this->getR()) * $amount + $this->getR(),
($other->getG() - $this->getG()) * $amount + $this->getG(),
($other->getB() - $this->getB()) * $amount + $this->getB()
);
}
public function __toString() { return '#'.$this->getHex(); }
}
Here we have a multi-purpose class which simply stores a color / value pair:
class ColoredValue {
protected $_value;
protected $_color;
public function __construct($value, Color $color) {
$this->_value = (float)$value;
$this->_color = $color;
}
public function getValue() { return $this->_value; }
public function getColor() { return $this->_color; }
}
Next we have some classes to determine how to interpolate colors. You can come up with a new interpolation by extending the base class. Included is a simple linear interpolation as well as a slightly more complicated interpolation using cosine (this favors colors at the endpoints rather than colors in between two extremes).
abstract class Interpolator {
abstract public function interpolate($value, $low, $high);
}
class LinearInterpolator extends Interpolator {
public function interpolate($value, $low, $high) {
return ((float)$value - (float)$low) / ((float)$high - (float)$low);
}
}
class CosInterpolator extends LinearInterpolator {
public function interpolate($value, $low, $high) {
return (cos(Math::PI * parent::interpolate($value, $high, $low))+1)/2;
}
}
Getting close to the end now. Here’s a gradient making device. If you’ve ever used a graphics program, this is basically the implementation of those gradient maker interfaces. Basically, you give it a bunch of “anchors”, which are color/value pairs. Then, if you give it a value, it will give you the color that corresponds to that value. It does this by finding the two anchors which contain the value and then, using a previously supplied interpolation algorithm of your choice, choosing the correct position between the anchors. Obviously, you must have at least two anchors and values passed to it must be within the anchors that you have given. A basic linear search is used because the number of anchors is likely to be low.
class ValueGradientMaker {
protected $_min;
protected $_max;
protected $_anchors;
protected $_interpolator;
public function __construct(array $gradientAnchors, Interpolator $interpolator) {
$this->_anchors = $gradientAnchors;
$this->_interpolator = $interpolator;
$this->_min = null;
$this->_max = null;
if (count($gradientAnchors) < 2) throw new InvalidArgumentException();
foreach ($gradientAnchors as $anchor) {
if (!($anchor instanceof ColoredValue)) throw new InvalidArgumentException();
$value = $anchor->getValue();
if (is_null($this->_min) || $value < $this->_min) $this->_min = $value;
if (is_null($this->_max) || $value > $this->_max) $this->_max = $value;
}
}
public function getColorFor($value) {
if ($value < $this->_min || $value > $this->_max) throw new InvalidArgumentException();
$highAnchorKey = count($this->_anchors)-1;
foreach ($this->_anchors as $key => $anchor) {
if ($anchor->getValue() > $value) {
$highAnchorKey = $key;
break;
}
}
$lowAnchor = $this->_anchors[$highAnchorKey-1];
$highAnchor = $this->_anchors[$highAnchorKey];
$mixAmount = $this->_interpolator->interpolate($value, $lowAnchor->getValue(), $highAnchor->getValue());
return $lowAnchor->getColor()->mixWith($highAnchor->getColor(), $mixAmount);
}
}
Finally, we have a convenience class that ties all of this together based on your specifications (three colors using the largest and smallest number and the median):
class SimpleColoration implements IteratorAggregate {
protected $_output;
public function __construct(array $numbers, Color $lowColor, Color $middleColor, Color $highColor, Interpolator $interpolator = null) {
if (empty($numbers)) throw new InvalidArgumentException();
if (is_null($interpolator)) $interpolator = new CosInterpolator();
sort($numbers, SORT_NUMERIC);
$minValue = $numbers[0];
$midValue = Math::median($numbers);
$maxValue = $numbers[count($numbers)-1];
$gradient = new ValueGradientMaker(
array(
new ColoredValue($minValue, $lowColor),
new ColoredValue($midValue, $middleColor),
new ColoredValue($maxValue, $highColor)
),
$interpolator
);
$this->_output = array();
foreach ($numbers as $number) {
$this->_output[] = new ColoredValue($number, $gradient->getColorFor($number));
}
}
public function getIterator() { return new ArrayIterator($this->_output); }
}
Here’s an example of how you might use this:
$red = new Color(255, 0, 0);
$green = new Color(0, 255, 0);
$blue = new Color(0, 0, 255);
$numbers = array(1234567890, 234567890, 34567890, 4567890, 567890);
foreach (new SimpleColoration($numbers, $red, $green, $blue) as $entry) {
echo '<div style="color:', $entry->getColor(), '">', $entry->getValue(), '</div><br>';
}
That outputs something like this:
567890
4567890
34567890
234567890
1234567890
Here’s an example with more numbers (using a simple [fphp]range[/fphp] call):
-5
-4
-3
-2
-1
0
1
2
3
4
5
If you wanted to use a linear interpolation instead, you could simply modify the loop above to read:
foreach (new SimpleColoration($numbers, $red, $green, $blue, new LinearInterpolator()) as $entry) {
For very few numbers, the difference is hardly noticeable. Nonetheless, should you wish to change the interpolation method it is trivial to implement new algorithms. Simply extend Interpolator and pass it into the constructor. If you want to change the way that the gradient is created (e.g. using only two colors or using the exact middle rather than the median), simply modify SimpleColoration or write a similar class.
You now have three very different solutions to examine. (: