Create Colors based on # of Items

Preferably using PHP, I’d like to create X number of colors based on X number of items and using a predefined color range. For example:

ITEM – VALUE
1 – 1,234,567,890
2 – 234,567,890
3 – 34,567,890
4 – 4,567,890
5 – 567,890

Color Range
red = largest value
green = medium value
blue = smallest value

Example
Item 1 gets color red
Item 2 gets whatever color naturally comes between red and green
Item 3 gets color green
Item 4 gets whatever color naturally comes between green and blue
Item 5 gets color blue

Requirements
The code should be able to handle any number of items. 5 items was just an easy example.

Some Thoughts

  • Number of items = number of colors
  • How to divide a color range into number of items?
  • Colors can be represented in decimal format: red=255,000,000 blue=000,000,255
  • Can a formula be created, based on the color decimal format, to solve this problem?

I think it’s the math portion of the problem that I’m stuck on right now.
Thanks for your help,

Nick

Thank you to everyone who constructively contributed to the conversation. You showed me how to approach the problem from a different view - that’s exactly the kind of help I needed. Thanks again for being so helpful. :slight_smile:

Nick

I never knew that! Thanks for the tip. :slight_smile:

…now I won’t be able to show off my memorization of the first 35 digits, though. :frowning:

Shouldn’t be so hard to incorporate that in the formulas should it?
The principle with the sine functions stays the same, just transform them for different color ranges :slight_smile:

Here is my attempt:
The output:

0 –> 329
1 –> 431
2 –> 691
3 –> 790
4 –> 922
5 –> 946

Items: 5
Max Colour: #ff0000 -> 16711680 decimal
Min Colour: #0000ff -> 255 decimal

Range: max - min = 16711425 decimal
Increment: range / items = 3342285

$color_dec = $x * $increment + 255;
$color_hex = sprintf(“%06X”, $color_dec);

5 –> #FF0000 –> 946
4 –> #CC0033 –> 922
3 –> #990066 –> 790
2 –> #660099 –> 691
1 –> #3300CC –> 431
0 –> #0000FF –> 329

.

The script:


echo '<div class="w88">';
  define('qq', '<br />');
  $max = 5;
  $a = array();
  
  for($x=0; $x<=$max; $x++):
    $a[] = mt_rand(50,1000);
  endfor;
  echo qq;
  
  sort($a);
  foreach($a as $b => $c):
    echo qq, $b ,' --> ', $c;
  endforeach;
  echo qq;
  
  echo qq, $items = 'Items: ' .$max;
  
  echo qq, 'Max Colour: #ff0000 -> ' .hexdec('ff0000') . ' decimal';
  echo qq, 'Min Colour: #0000ff -> ' .hexdec('0000ff') . ' decimal';
  
  $range = hexdec('ff0000') -  hexdec('0000ff');
  echo qq, 'Range: max - min == ' .$range . ' decimal';
  
  $increment = $range /  $max;
  echo qq, 'Increment: range / items ==> ' .$increment; 
  echo qq, '$color_dec = $x * $increment + 255;';
  echo qq, '$color_hex = sprintf("%06X", $color_dec);';
  echo qq;
  
  for($x=$max; $x>=0; $x--):
    $color_dec = $x * $increment + 255;
    $color_hex = sprintf('%06X', $color_dec);
    
    echo qq, $x , " --> #", $color_hex;

    echo " --> <span style='font-weight:700; color:#";
     echo $color_hex ."'>";
      echo $a[$x];    
    echo '</span>'; 
  endfor; 
echo '</div>';
?>

.

@ ScallioXTX: that’s awesome :slight_smile:

35!? I only know nine. And looking from what you put in your code the last digit I know is actually different than it should be, because for me it’s a 4 since it’s the last number rounded up… :frowning:

Blimey Tarh, that looks good :slight_smile:

One very small comment though, you don’t need to define pi as it is already defined in the define M_PI, or you can use the function pi()

Otherwise, very nice :tup:

Here’s another implementation to throw into the mix. :wink:

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. (:

That’s a fun question, I like to do some math from time to time :slight_smile:

Took me quite a while, but I think I’ve got it figured out.

First of all, we need to take into account that there can be X items.
In order to say something about where an item i is on the scale from 1 to X we need to squish the range to the open interval (0,1)

We can simply do this by setting $x=($itemNumber-1)/($totalItems-1);

Next, we need some way to create the “inbetween” colors. Now that we have everything on the interval (0,1) we know that $x=0 is the lowest value, and should thus be blue. Following this reasoning $x=0.5 should be green and $x=1 should be red.

So, these three points are now given. We could of course create a bunch of linear functions for red, green and blue, but linear functions are kind of “hard” and won’t give a nice “flow” to the inbetween colors. The sine function is better suited for this.

Without going into details I came up with the following sine transformations:

r = sin(pi * (x-0.5))
g = sin(pi * x)
b = sin(pi * (x+0.5))

Now of course the sine function has a domain of (0,1) and we want the domain (0,255) so we need to multiply by 255. Furthermore, the sine can have negative values, but since we’re not interested in that, we take the max of 0 and the actual value, to ensure a positive value of 0 or higher. Lastly, the sine function produces numbers with decimals, which we cannot handle with CSS colors, so we’ll also apply rounding.

Thus, the final functions are:
r = max(0, round(sin(pi * (x-0.5)) * 255))
g = max(0, round(sin(pi * x) * 255))
b = max(0, round(sin(pi * (x+0.5)) * 255))

and the final PHP function is:


function getColorForItem($itemNumber,$numberOfItems) {
  $x = ($itemNumber-1)/($numberOfItems-1);

  $r = max(0,round(sin(M_PI * ($x-0.5)) * 255));
  $g = max(0,round(sin(M_PI * $x) * 255));
  $b = max(0,round(sin(M_PI * ($x+0.5)) * 255));

  return decColorToHex($r).decColorToHex($g).decColorToHex($b);
}

where decColorToHex is a simple function I wrote myself:


function decColorToHex($int) {
  return str_pad(dechex($int), 2, '0', STR_PAD_LEFT);
}

All together with some <div> output in the various colors:


$n=7;

function decColorToHex($int) {
  return str_pad(dechex($int), 2, '0', STR_PAD_LEFT);
}

function getColorForItem($itemNumber,$numberOfItems) {
  $x = ($itemNumber-1)/($numberOfItems-1);

  $r = max(0,round(sin(M_PI * ($x-0.5)) * 255));
  $g = max(0,round(sin(M_PI * $x) * 255));
  $b = max(0,round(sin(M_PI * ($x+0.5)) * 255));

  return decColorToHex($r).decColorToHex($g).decColorToHex($b);
}

for ($i=1;$i<=$n;$i++) {
  $c=getColorForItem($i,$n);
  echo '<div style="width:200px;height:30px;color:#fff;font-weight:bold;background-color:#'.$c.';">Item '.$i.': #'.$c.'</div>';
}

the $n at the beginning of the script describes the number of items X. Change it to get more or less items.

If you have any questions don’t hesitate to ask. Enjoy :slight_smile:

Disclaimer: doesn’t work if X <= 1 :stuck_out_tongue:

This looks very close to a homework problem.

@ScallioXTX, maybe you missed the part about using a “predefined colour range”?