How to pick color palette for a pie-chart?

AlgorithmColorsRgb

Algorithm Problem Overview


I have some code that generates image of a pie chart. It's a general purpose class, so any number of slices can be given as input. Now I have problem picking good colors for the slices. Is there some algorithm that is good at that?

Colors need to follow some rules:

  • they need to look nice
  • adjacent colors should not be similar (blue next to green is a no-go)
  • pie background color is white, so white is out of option

Some algorithm manipulating with RGB values would be a preferred solution.

Algorithm Solutions


Solution 1 - Algorithm

I solved it as follows:

  1. Choose a base color.
  2. Calculate its hue (baseHue).
  3. Create a color with the same saturation and luminosity, with its hue calculated as:
    hue = baseHue + ((240 / pieces) * piece % 240
    

In C#:

int n = 12;

Color baseColor = System.Drawing.ColorTranslator.FromHtml("#8A56E2");
double baseHue = (new HSLColor(baseColor)).Hue;

List<Color> colors = new List<Color>();
colors.Add(baseColor);

double step = (240.0 / (double)n);

for (int i = 1; i < n; ++i)
{
    HSLColor nextColor = new HSLColor(baseColor);
    nextColor.Hue = (baseHue + step * ((double)i)) % 240.0;
    colors.Add((Color)nextColor);
}

string colors = string.Join(",", colors.Select(e => e.Name.Substring(2)).ToArray());

I used the HSLColor class.

The Google Charts example that uses 12 pieces and a base color of #8A56E2:

Chart Example

Solution 2 - Algorithm

I would pre-compile a list of about 20 colors, then start repeating with the 2nd color. This way you won't break your second rule. Also, if someone makes a pie chart with more than 20 slices, they have bigger problems. :)

Solution 3 - Algorithm

Take a look at Color Brewer, a tool that helps to define a coloring scheme to convey qualitative or quantitative information: maps, charts, etc. Out of three "types" of palettes that this tool can generate - sequential, qualitative, and diverging - you probably need the latter, diverging...

You can even download Excel files with RGB definitions of all the palettes.

Solution 4 - Algorithm

Building upon this solution to solve the question's rule #2, the following algorithm swaps colors around the pie's middle-point. The two parameters:

  1. pNbColors is the number of slices in the pie
  2. pNonAdjacentSimilarColor a Boolean to indicate if you want to have Adjacent similar colors or not.

I am using ColorHSL, ColorRGB and ColorUtils (provided below).

public static function ColorArrayGenerator(
	pNbColors:int,
	pNonAdjacentSimilarColor:Boolean = false):Array
{		
	var colors:Array = new Array();
	var baseRGB:ColorRGB = new ColorRGB();
	baseRGB.setRGBFromUint(0x8A56E2);
	
	var baseHSL:ColorHSL = new ColorHSL();
	rgbToHsl(baseHSL, baseRGB);
	
	var currentHue:Number = baseHSL.Hue;
	
	colors.push(baseRGB.getUintFromRGB());
	
	var step:Number = (360.0 / pNbColors);
	var nextHSL:ColorHSL;
	var nextRGB:ColorRGB;
	var i:int;
	
	for (i = 1; i < pNbColors; i++)
	{
		currentHue += step;
		if (currentHue > 360)
		{
			currentHue -= 360;
		}
		
		nextHSL = new ColorHSL(currentHue, baseHSL.Saturation, aseHSL.Luminance);
		nextRGB = new ColorRGB();
		hslToRgb(nextRGB, nextHSL);
			
		colors.push(nextRGB.getUintFromRGB());
	}
	
	if (pNonAdjacentSimilarColor == true &&
		pNbColors > 2)
	{
		var holder:uint = 0;
		var j:int;
		
		for (i = 0, j = pNbColors / 2; i < pNbColors / 2; i += 2, j += 2)
		{
			holder = colors[i];
			colors[i] = colors[j];
			colors[j] = holder;
		}
	}
	
	return colors;
}

This produces the right-hand side output:

Comparison Image

ColorHSL class:

	final public class ColorHSL
{
	private var _hue:Number;    // 0.0 .. 359.99999
	
	private var _sat:Number;    // 0.0 .. 100.0
	
	private var _lum:Number;    // 0.0 .. 100.0
	
	public function ColorHSL(
		hue:Number = 0,
		sat:Number = 0,
		lum:Number = 0)
	{
		_hue = hue;
		_sat = sat;
		_lum = lum;
	}
	
	[Bindable]public function get Hue():Number
	{
		return _hue;
	}
	
	public function set Hue(value:Number):void
	{
		if (value > 360) 
		{
			_hue = value % 360;
		}    // remember, hue is modulo 360
		else if (value < 0)
		{
			_hue = 0;
		}
		else
		{
			_hue = value;
		}
	}
	
	[Bindable]public function get Saturation():Number
	{
		return _sat;
	}
	
	public function set Saturation(value:Number):void
	{
		if (value > 100.0)
		{
			_sat = 100.0;
		}
		else if (value < 0)
		{
			_sat = 0;
		}
		else
		{
			_sat = value;
		}
	}
	
	[Bindable]public function get Luminance():Number
	{
		return _lum;
	}
	
	public function set Luminance(value:Number):void
	{
		if (value > 100.0)
		{
			_lum = 100.0;
		}
		else if (value < 0)
		{
			_lum = 0;
		}
		else
		{
			_lum = value;
		}
	}
}

ColorRGB class:

	final public class ColorRGB
{
	private var _red:uint;
	private var _grn:uint;
	private var _blu:uint;
	private var _rgb:uint;        // composite form: 0xRRGGBB or #RRGGBB
	
	public function ColorRGB(red:uint = 0, grn:uint = 0, blu:uint = 0)
	{
		setRGB(red, grn, blu);
	}
	
	[Bindable]public function get red():uint
	{
		return _red;
	}
	
	public function set red(value:uint):void
	{
		_red = (value & 0xFF);
		updateRGB();
	}
	
	[Bindable]public function get grn():uint
	{
		return _grn;
	}
	
	public function set grn(value:uint):void
	{
		_grn = (value & 0xFF);
		updateRGB();
	}
	
	[Bindable]public function get blu():uint
	{
		return _blu;
	}
	
	public function set blu(value:uint):void
	{
		_blu = (value & 0xFF);
		updateRGB();
	}
	
	[Bindable]public function get rgb():uint
	{
		return _rgb;
	}
	
	public function set rgb(value:uint):void
	{
		_rgb = value;
		_red = (value >> 16) & 0xFF;
		_grn = (value >>  8) & 0xFF;
		_blu =  value        & 0xFF;
	}
	
	public function setRGB(red:uint, grn:uint, blu:uint):void
	{
		this.red = red;
		this.grn = grn;
		this.blu = blu;
	}
	
	public function setRGBFromUint(pValue:uint):void
	{
		setRGB((( pValue >> 16 ) & 0xFF ), ( (pValue >> 8) & 0xFF ), ( pValue & 0xFF ));
	}
	
	public function getUintFromRGB():uint
	{
		return ( ( red << 16 ) | ( grn << 8 ) | blu );
	}
	
	private function updateRGB():void
	{
		_rgb = (_red << 16) + (_grn << 8) + blu;
	}
}

ColorUtils class:

final public class ColorUtils
{
	public static function HSV2RGB(hue:Number, sat:Number, val:Number):uint
	{
		var red:Number = 0;
		var grn:Number = 0;
		var blu:Number = 0;
		var i:Number;
		var f:Number;
		var p:Number;
		var q:Number;
		var t:Number;
		hue%=360;
		sat/=100;
		val/=100;
		hue/=60;
		i = Math.floor(hue);
		f = hue-i;
		p = val*(1-sat);
		q = val*(1-(sat*f));
		t = val*(1-(sat*(1-f)));
		if (i==0)
		{
			red=val;
			grn=t;
			blu=p;
		}
		else if (i==1)
		{
			red=q;
			grn=val;
			blu=p;
		}
		else if (i==2)
		{
			red=p;
			grn=val;
			blu=t;
		}
		else if (i==3)
		{
			red=p;
			grn=q;
			blu=val;
		}
		else if (i==4)
		{
			red=t;
			grn=p;
			blu=val;
		}
		else if (i==5)
		{
			red=val;
			grn=p;
			blu=q;
		}
		red = Math.floor(red*255);
		grn = Math.floor(grn*255);
		blu = Math.floor(blu*255);

		return (red<<16) | (grn << 8) | (blu);
	}
	
	//
	public static function RGB2HSV(pColor:uint):Object
	{
		var red:uint = (pColor >> 16) & 0xff;
		var grn:uint = (pColor >> 8) & 0xff;
		var blu:uint = pColor & 0xff;

		var x:Number;
		var val:Number;
		var f:Number;
		var i:Number;
		var hue:Number;
		var sat:Number;
		red/=255;
		grn/=255;
		blu/=255;
		x = Math.min(Math.min(red, grn), blu);
		val = Math.max(Math.max(red, grn), blu);
		if (x==val){
			return({h:undefined, s:0, v:val*100});
		}
		f = (red == x) ? grn-blu : ((grn == x) ? blu-red : red-grn);
		i = (red == x) ? 3 : ((grn == x) ? 5 : 1);
		hue = Math.floor((i-f/(val-x))*60)%360;
		sat = Math.floor(((val-x)/val)*100);
		val = Math.floor(val*100);
		return({h:hue, s:sat, v:val});
	}
	
	/**
	 * Generates an array of pNbColors colors (uint) 
	 * The colors are generated to fill a pie chart (meaning that they circle back to the starting color)
	 * @param pNbColors The number of colors to generate (ex: Number of slices in the pie chart)
	 * @param pNonAdjacentSimilarColor Should the colors stay Adjacent or not ?
	 */
	public static function ColorArrayGenerator(
		pNbColors:int,
		pNonAdjacentSimilarColor:Boolean = false):Array
	{
		// Based on http://www.flexspectrum.com/?p=10
		
		var colors:Array = [];
		var baseRGB:ColorRGB = new ColorRGB();
		baseRGB.setRGBFromUint(0x8A56E2);
		
		var baseHSL:ColorHSL = new ColorHSL();
		rgbToHsl(baseHSL, baseRGB);
		
		var currentHue:Number = baseHSL.Hue;
		
		colors.push(baseRGB.getUintFromRGB());
		
		var step:Number = (360.0 / pNbColors);
		var nextHSL:ColorHSL;
		var nextRGB:ColorRGB;
		var i:int;
		
		for (i = 1; i < pNbColors; i++)
		{
			currentHue += step;

			if (currentHue > 360)
			{
				currentHue -= 360;
			}
			
			nextHSL = new ColorHSL(currentHue, baseHSL.Saturation, baseHSL.Luminance);
			nextRGB = new ColorRGB();
			hslToRgb(nextRGB, nextHSL);
			
			colors.push(nextRGB.getUintFromRGB());
		}
		
		if (pNonAdjacentSimilarColor == true &&
			pNbColors > 2)
		{
			var holder:uint = 0;
			var j:int;
			
			for (i = 0, j = pNbColors / 2; i < pNbColors / 2; i += 2, j += 2)
			{
				holder = colors[i];
				colors[i] = colors[j];
				colors[j] = holder;
			}
		}
		
		return colors;
	}
	
	static public function rgbToHsl(hsl:ColorHSL, rgb:ColorRGB):void
	{
		var h:Number = 0;
		var s:Number = 0;
		var l:Number = 0;
		
		// Normalizes incoming RGB values.
		//
		var dRed:Number = (Number)(rgb.red / 255.0);
		var dGrn:Number = (Number)(rgb.grn / 255.0);
		var dBlu:Number = (Number)(rgb.blu / 255.0);
		
		var dMax:Number = Math.max(dRed, Math.max(dGrn, dBlu));
		var dMin:Number = Math.min(dRed, Math.min(dGrn, dBlu));
		
		//-------------------------
		// hue
		//
		if (dMax == dMin)
		{
			h = 0;                 // undefined
		}
		else if (dMax == dRed && dGrn >= dBlu)
		{
			h = 60.0 * (dGrn - dBlu) / (dMax - dMin);
		}
		else if (dMax == dRed && dGrn < dBlu)
		{
			h = 60.0 * (dGrn - dBlu) / (dMax - dMin) + 360.0;
		}
		else if (dMax == dGrn)
		{
			h = 60.0 * (dBlu - dRed) / (dMax-dMin) + 120.0;
		}
		else if (dMax == dBlu)
		{
			h = 60.0 * (dRed - dGrn) / (dMax - dMin) + 240.0;
		}
		
		//-------------------------
		// luminance
		//
		l = (dMax + dMin) / 2.0;
		
		//-------------------------
		// saturation
		//
		if (l == 0 || dMax == dMin)
		{
			s = 0;
		}
		else if (0 < l && l <= 0.5)
		{
			s = (dMax - dMin) / (dMax + dMin);
		}
		else if (l>0.5)
		{
			s = (dMax - dMin) / (2 - (dMax + dMin));    //(dMax-dMin > 0)?
		}
		
		hsl.Hue = h;
		hsl.Luminance = l;
		hsl.Saturation = s;
		
	} // rgbToHsl
	
	//---------------------------------------
	// Convert the input RGB values to the corresponding HSL values.
	//
	static public function hslToRgb(rgb:ColorRGB, hsl:ColorHSL):void
	{
		if (hsl.Saturation == 0)
		{
			// Achromatic color case, luminance only.
			//
			var lumScaled:int = (int)(hsl.Luminance * 255.0); 
			rgb.setRGB(lumScaled, lumScaled, lumScaled);
			return;
		}
		
		// Chromatic case...
		//
		var dQ:Number = (hsl.Luminance < 0.5) ? (hsl.Luminance * (1.0 + hsl.Saturation)): ((hsl.Luminance + hsl.Saturation) - (hsl.Luminance * hsl.Saturation));
		var dP:Number = (2.0 * hsl.Luminance) - dQ;
		
		var dHueAng:Number = hsl.Hue / 360.0;
		
		var dFactor:Number = 1.0 / 3.0;
		
		var adT:Array = [];
		
		adT[0] = dHueAng + dFactor;                // Tr
		adT[1] = dHueAng;                        // Tg
		adT[2] = dHueAng - dFactor;                // Tb
		
		for (var i:int = 0; i < 3; i++)
		{
			if (adT[i] < 0)
			{
				adT[i] += 1.0;
			}
			
			if (adT[i] > 1)
			{
				adT[i] -= 1.0;
			}
			
			if ((adT[i] * 6) < 1)
			{
				adT[i] = dP + ((dQ - dP) * 6.0 * adT[i]);
			}
			else if ((adT[i] * 2.0) < 1)        // (1.0 / 6.0) <= adT[i] && adT[i] < 0.5
			{
				adT[i] = dQ;
			}
			else if ((adT[i] * 3.0) < 2)        // 0.5 <= adT[i] && adT[i] < (2.0 / 3.0)
			{
				adT[i] = dP + (dQ-dP) * ((2.0/3.0) - adT[i]) * 6.0;
			}
			else
			{
				adT[i] = dP;
			}
		}
		
		rgb.setRGB(adT[0] * 255.0, adT[1] * 255.0, adT[2] * 255.0);
		
	} // hslToRgb
	
	//---------------------------------------
	// Adjust the luminance value by the specified factor.
	//
	static public function adjustRgbLuminance(rgb:ColorRGB, factor:Number):void
	{
		var hsl:ColorHSL = new ColorHSL();
		
		rgbToHsl(hsl, rgb);
		
		hsl.Luminance *= factor;
		
		if (hsl.Luminance < 0.0)
		{
			hsl.Luminance = 0.0;
		}
		
		if (hsl.Luminance > 1.0)
		{
			hsl.Luminance = 1.0;
		}
		
		hslToRgb(rgb, hsl);
	}
	
	//---------------------------------------
	//
	static public function uintTo2DigitHex(value:uint):String
	{
		var str:String = value.toString(16).toUpperCase();
		
		if (1 == str.length)
		{
			str = "0" + str;
		}
		
		return str;
	}
	
	//---------------------------------------
	//
	static public function uintTo6DigitHex(value:uint):String
	{
		var str:String = value.toString(16).toUpperCase();
		
		if (1 == str.length)    {return "00000" + str;}
		if (2 == str.length)    {return "0000" + str;}
		if (3 == str.length)    {return "000" + str;}
		if (4 == str.length)    {return "00" + str;}
		if (5 == str.length)    {return "0" + str;}
		
		return str;
	}
}

Solution 5 - Algorithm

Overview

Converting from RGB to HSV and then adjusting the hue (as answered here) creates an inconsistent perceived brightness. The yellow/green are noticeably lighter than the blue/purple:

Inconsistent

A similar result without such variation is possible:

Consistent

Algorithm

The algorithm, however, is far more complex:

  1. Convert HTML hexadecimal codes to nominal RGB values (divide components by 255).
  2. Convert RGB values to XYZ colour space; use D65 reference white sRGB working space.
  3. Convert from XYZ to Lab colour space.
  4. Convert from Lab to LCH colour space.
  5. Calculate pie wedge colour hue in LCH colour space:
    (360.0 div $wedges) * $wedge
  6. Recalculate the new hue in radians.
  7. Convert back from LCH to Lab colour space using new hue.
  8. Convert from Lab to XYZ colour space.
  9. Convert from XYZ to sRGB colour space.
  10. Multiply the RGB values by 255.

Implementation

Here is an example implementation in XSLT 1.0:

<?xml version="1.0"?>
<!--
 | The MIT License
 |
 | Copyright 2014 White Magic Software, Inc.
 | 
 | Permission is hereby granted, free of charge, to any person
 | obtaining a copy of this software and associated documentation
 | files (the "Software"), to deal in the Software without
 | restriction, including without limitation the rights to use,
 | copy, modify, merge, publish, distribute, sublicense, and/or
 | sell copies of the Software, and to permit persons to whom the
 | Software is furnished to do so, subject to the following
 | conditions:
 | 
 | The above copyright notice and this permission notice shall be
 | included in all copies or substantial portions of the Software.
 | 
 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 | OTHER DEALINGS IN THE SOFTWARE.
 +-->
<xsl:stylesheet version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<!-- Reference white (X, Y, and Z components) -->
<xsl:variable name="X_r" select="0.950456"/>
<xsl:variable name="Y_r" select="1.000000"/>
<xsl:variable name="Z_r" select="1.088754"/>
<xsl:variable name="LAB_EPSILON" select="216.0 div 24389.0"/>
<xsl:variable name="LAB_K" select="24389.0 div 27.0"/>

<!-- Pie wedge colours based on this hue. -->
<xsl:variable name="base_colour" select="'46A5E5'"/>

<!-- Pie wedge stroke colour. -->
<xsl:variable name="stroke_colour" select="'white'"/>

<!--
 | Creates a colour for a particular pie wedge.
 |
 | http://en.wikipedia.org/wiki/HSL_and_HSV 
 +-->
<xsl:template name="fill">
  <!-- Current wedge number for generating a colour. -->
  <xsl:param name="wedge"/>
  <!-- Total number of wedges in the pie. -->
  <xsl:param name="wedges"/>
  <!-- RGB colour in hexadecimal. -->
  <xsl:param name="colour"/>

  <!-- Derive the colour decimal values from $colour's HEX code. -->
  <xsl:variable name="r">
    <xsl:call-template name="hex2dec">
      <xsl:with-param name="hex"
        select="substring( $colour, 1, 2 )"/>
    </xsl:call-template>
  </xsl:variable>
  <xsl:variable name="g">
    <xsl:call-template name="hex2dec">
      <xsl:with-param name="hex"
        select="substring( $colour, 3, 2 )"/>
    </xsl:call-template>
  </xsl:variable>
  <xsl:variable name="b">
    <xsl:call-template name="hex2dec">
      <xsl:with-param name="hex"
        select="substring( $colour, 5, 2 )"/>
    </xsl:call-template>
  </xsl:variable>

  <!--
   | Convert RGB to XYZ, using nominal range for RGB.
   | http://www.brucelindbloom.com/index.html?Eqn_RGB_to_XYZ.html
   +-->
  <xsl:variable name="r_n" select="$r div 255" />
  <xsl:variable name="g_n" select="$g div 255" />
  <xsl:variable name="b_n" select="$b div 255" />

  <!--
   | Assume colours are in sRGB.
   | http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
   -->
  <xsl:variable name="x"
    select=".4124564 * $r_n + .3575761 * $g_n + .1804375 * $b_n"/>
  <xsl:variable name="y"
    select=".2126729 * $r_n + .7151522 * $g_n + .0721750 * $b_n"/>
  <xsl:variable name="z"
    select=".0193339 * $r_n + .1191920 * $g_n + .9503041 * $b_n"/>

  <!--
   | Convert XYZ to L*a*b.
   | http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_Lab.html
   +-->
  <xsl:variable name="if_x">
    <xsl:call-template name="lab_f">
      <xsl:with-param name="xyz_n" select="$x div $X_r"/>
    </xsl:call-template>
  </xsl:variable>
  <xsl:variable name="if_y">
    <xsl:call-template name="lab_f">
      <xsl:with-param name="xyz_n" select="$y div $Y_r"/>
    </xsl:call-template>
  </xsl:variable>
  <xsl:variable name="if_z">
    <xsl:call-template name="lab_f">
      <xsl:with-param name="xyz_n" select="$z div $Z_r"/>
    </xsl:call-template>
  </xsl:variable>

  <xsl:variable name="lab_l" select="(116.0 * $if_y) - 16.0"/>
  <xsl:variable name="lab_a" select="500.0 * ($if_x - $if_y)"/>
  <xsl:variable name="lab_b" select="200.0 * ($if_y - $if_z)"/>
  
  <!--
   | Convert L*a*b to LCH.
   | http://www.brucelindbloom.com/index.html?Eqn_Lab_to_LCH.html
   +-->
  <xsl:variable name="lch_l" select="$lab_l"/>

  <xsl:variable name="lch_c">
    <xsl:call-template name="sqrt">
      <xsl:with-param name="n" select="($lab_a * $lab_a) + ($lab_b * $lab_b)"/>
    </xsl:call-template>
  </xsl:variable>

  <xsl:variable name="lch_h">
    <xsl:call-template name="atan2">
      <xsl:with-param name="x" select="$lab_b"/>
      <xsl:with-param name="y" select="$lab_a"/>
    </xsl:call-template>
  </xsl:variable>

  <!--
   | Prevent similar adjacent colours.
   | http://math.stackexchange.com/a/936767/7932
   +-->
  <xsl:variable name="wi" select="$wedge"/>
  <xsl:variable name="wt" select="$wedges"/>
  <xsl:variable name="w">
    <xsl:choose>
      <xsl:when test="$wt &gt; 5">
        <xsl:variable name="weven" select="(($wi+4) mod ($wt + $wt mod 2))"/>
        <xsl:value-of
          select="$weven * (1-($wi mod 2)) + ($wi mod 2 * $wi)"/>
      </xsl:when>
      <xsl:otherwise>
        <xsl:value-of select="$wedge"/>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:variable>
  <!-- lch_l, lch_c, and lch_h are now set; rotate the hue. -->
  <xsl:variable name="lch_wedge_h" select="(360.0 div $wedges) * $wedge"/>

  <!--
   | Convert wedge's hue-adjusted LCH to L*a*b.
   | http://www.brucelindbloom.com/index.html?Eqn_LCH_to_Lab.html
   +-->
  <xsl:variable name="lab_sin_h">
    <xsl:call-template name="sine">
      <xsl:with-param name="degrees" select="$lch_wedge_h"/>
    </xsl:call-template>
  </xsl:variable>
  <xsl:variable name="lab_cos_h">
    <xsl:call-template name="cosine">
      <xsl:with-param name="degrees" select="$lch_wedge_h"/>
    </xsl:call-template>
  </xsl:variable>

  <xsl:variable name="final_lab_l" select="$lch_l"/>
  <xsl:variable name="final_lab_a" select="$lch_c * $lab_cos_h"/>
  <xsl:variable name="final_lab_b" select="$lch_c * $lab_sin_h"/>

  <!--
   | Convert L*a*b to XYZ.
   | http://www.brucelindbloom.com/index.html?Eqn_Lab_to_XYZ.html
   +-->
  <xsl:variable name="of_y" select="($final_lab_l + 16.0) div 116.0"/>
  <xsl:variable name="of_x" select="($final_lab_a div 500.0) + $of_y"/>
  <xsl:variable name="of_z" select="$of_y - ($final_lab_b div 200.0)"/>

  <xsl:variable name="of_x_pow">
    <xsl:call-template name="power">
      <xsl:with-param name="base" select="$of_x"/>
      <xsl:with-param name="exponent" select="3"/>
    </xsl:call-template>
  </xsl:variable>
  <xsl:variable name="of_z_pow">
    <xsl:call-template name="power">
      <xsl:with-param name="base" select="$of_z"/>
      <xsl:with-param name="exponent" select="3"/>
    </xsl:call-template>
  </xsl:variable>

  <xsl:variable name="ox_r">
    <xsl:choose>
      <xsl:when test="$of_x_pow &gt; $LAB_EPSILON">
        <xsl:value-of select="$of_x_pow"/>
      </xsl:when>
      <xsl:otherwise>
        <xsl:value-of select="((116.0 * $of_x) - 16.0) div $LAB_K"/>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:variable>
  <xsl:variable name="oy_r">
    <xsl:choose>
      <xsl:when test="$final_lab_l &gt; ($LAB_K * $LAB_EPSILON)">
        <xsl:call-template name="power">
          <xsl:with-param name="base"
            select="($final_lab_l + 16.0) div 116.0"/>
          <xsl:with-param name="exponent"
            select="3"/>
        </xsl:call-template>
      </xsl:when>
      <xsl:otherwise>
        <xsl:value-of select="$final_lab_l div $LAB_K"/>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:variable>
  <xsl:variable name="oz_r">
    <xsl:choose>
      <xsl:when test="$of_z_pow &gt; $LAB_EPSILON">
        <xsl:value-of select="$of_z_pow"/>
      </xsl:when>
      <xsl:otherwise>
        <xsl:value-of select="((116.0 * $of_z) - 16.0) div $LAB_K"/>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:variable>

  <xsl:variable name="X" select="$ox_r * $X_r"/>
  <xsl:variable name="Y" select="$oy_r * $Y_r"/>
  <xsl:variable name="Z" select="$oz_r * $Z_r"/>

  <!--
   | Convert XYZ to sRGB.
   | http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
   +-->
  <xsl:variable name="R"
    select="3.2404542 * $X + -1.5371385 * $Y + -0.4985314 * $Z"/>
  <xsl:variable name="G"
    select="-0.9692660 * $X + 1.8760108 * $Y + 0.0415560 * $Z"/>
  <xsl:variable name="B"
    select="0.0556434 * $X + -0.2040259 * $Y + 1.0572252 * $Z"/>

  <!-- Round the result. -->
  <xsl:variable name="R_r" select="round( $R * 255 )"/>
  <xsl:variable name="G_r" select="round( $G * 255 )"/>
  <xsl:variable name="B_r" select="round( $B * 255 )"/>

  <xsl:text>rgb(</xsl:text>
  <xsl:value-of select="concat( $R_r, ',', $G_r, ',', $B_r )"/>
  <xsl:text>)</xsl:text>
</xsl:template>

<xsl:template name="lab_f">
  <xsl:param name="xyz_n"/>

  <xsl:choose>
    <xsl:when test="$xyz_n &gt; $LAB_EPSILON">
      <xsl:call-template name="nthroot">
        <xsl:with-param name="index" select="3"/>
        <xsl:with-param name="radicand" select="$xyz_n"/>
      </xsl:call-template>
    </xsl:when>
    <xsl:otherwise>
      <xsl:value-of select="($LAB_K * $xyz_n + 16.0) div 116.0" />
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

<!-- Converts a two-digit hexadecimal number to decimal. -->
<xsl:template name="hex2dec">
  <xsl:param name="hex"/>

  <xsl:variable name="digits" select="'0123456789ABCDEF'"/>
  <xsl:variable name="X" select="substring( $hex, 1, 1 )"/>
  <xsl:variable name="Y" select="substring( $hex, 2, 1 )"/>
  <xsl:variable name="Xval"
    select="string-length(substring-before($digits,$X))"/>
  <xsl:variable name="Yval"
    select="string-length(substring-before($digits,$Y))"/>
  <xsl:value-of select="16 * $Xval + $Yval"/>
</xsl:template>

</xsl:stylesheet>

The trig, root, and miscellaneous math functions are left as an exercise for the reader. Also, nobody in their right mind would want to code all this in XSLT 1.0. XSLT 2.0, on the other hand, has an implementation here.

Resources

Further reading:

Solution 6 - Algorithm

This 1985 paper by "ROSS E. ROLEY, CAPT" gives an algorithm for maximizing color separation for an arbitrary set of colors (complete with code in FORTRAN).

(Color separation appears to be an important visualization issue for military forces to prevent blue-on-blue incidents.)

However if you want to stick to a set of 20 colors, a quick and simple solution would be to pick the vertexes of a dodecahedron and convert the (x,y,z) co-ordinates (suitably scaled) to (r,g,b).

Solution 7 - Algorithm

There is a generator here. It is intended for web design, but the colours would look great on a pie chart, too.

You could either pre-compile a list of nice colours, or examine the logic behind the generator and do something similar yourself.

Solution 8 - Algorithm

I found this pseudocode formula that might help. You could start with a set to seed it.

Colour Difference Formula

The following is the formula suggested by the W3C to determine the difference between two colours.

(maximum (Red value 1, Red value 2) - minimum (Red value 1, Red value 2)) + (maximum (Green value 1, Green value 2) - minimum (Green value 1, Green value 2)) + (maximum (Blue value 1, Blue value 2) - minimum (Blue value 1, Blue value 2))

The difference between the background colour and the foreground colour should be greater than 500.

Here is the source

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestioncodeguruView Question on Stackoverflow
Solution 1 - AlgorithmNiels BosmaView Answer on Stackoverflow
Solution 2 - AlgorithmBill the LizardView Answer on Stackoverflow
Solution 3 - AlgorithmYarikView Answer on Stackoverflow
Solution 4 - AlgorithmPic MickaelView Answer on Stackoverflow
Solution 5 - AlgorithmDave JarvisView Answer on Stackoverflow
Solution 6 - AlgorithmDeclan BrennanView Answer on Stackoverflow
Solution 7 - AlgorithmKramiiView Answer on Stackoverflow
Solution 8 - AlgorithmbranchgabrielView Answer on Stackoverflow