Loading

Font & Colour Picker

A widget to allow the user to style the page with Google web fonts. Basic functionality up and working, more features to be added in the future. Still buggy in IE: it doesn't download the full font files for .woff fonts once it has downloaded a sample, although it does for .ttf fonts.

<!DOCTYPE html>
<html lang="en" >
<head>
  <meta charset="UTF-8">
  <title>Font & Colour Picker</title>
  
  
  <link rel='stylesheet prefetch' href='css/tkjcm.css'>

      <link rel="stylesheet" href="css/style.css">

  
</head>

<body>
  <style>
  /* separating out the demo styles from the widget styles in the main CSS panel */
article.sample-text, div.controls {
  width:30%;
  width:calc(33% - 1em);
  min-width:14em;
  margin:0 0.3em 0 0.1em;
  display:block;
  float:left;
  vertical-align:top;
  overflow:hidden;
  border:1px solid;
  padding:0 0.3em;
}
div.colour-picker {
  max-width:10em;
}
article.sample-text {
  max-height:100vh;
}
article.sample-text pre,
article.sample-text p {
  font-size:smaller;
  font-family:inherit;
}
br, blockquote, h1,h2,h3 {
  clear:both;
}
body {
  padding:0.5em;
  padding-bottom:2em;
}
body:after {
  content:"";display:block;clear:both;
}
li {
  list-style:inside;
  margin-bottom:0.5em;
}
body > h1 {
  font-variant:small-caps;
  margin:0 auto 0.5em ;
  text-align:center;
  text-decoration:underline;
}
main {
  max-width:50em;
  margin:0 auto;
}
code {
  color:navy;
}
blockquote {
  background:lavender;
}
</style>

<h1>Web Font Picker</h1>

<p>Allows the user to pick a font and see it applied to the page, using the Google web font developer API and the CSS object model.  <strong>If you fork, please substitute in your own <a href="https://developers.google.com/fonts/docs/developer_api#Auth">free developer key</a>.</strong> To select the font and background colours, I'm using my own CSS+Javascript colour picker which I <a href="https://codepen.io/AmeliaBR/pen/tkJCm">describe in more detail in a separate pen</a>.</p>
  
<div class="font-picker controls" data-target-selector=".sample-text">
    <label for="text-family-list">Main Text Font:</label>
  <div id="text-family-list" class="selection-list font-family">
  </div>
  <label for="heading-family-list">Heading Font:</label>
  <div id="heading-family-list"
       class="selection-list font-family"
        data-target-selector="h1, h2, h3">
  </div>
  <div class="colour-picker hideWhenBlurred"  tabIndex="0"
       data-target-selector=".sample-text"
        data-target-style="background-color, backgroundColor">
    <label>Highlight Colour:
      <input value="aliceblue"/></label>
  </div>
  <div class="colour-picker hideWhenBlurred"  tabIndex="0"
       data-target-selector=".sample-text"
        data-target-style="color">
    <label>Text Colour:
      <input value="#333"/></label>
  </div>
</div>

<article class="sample-text">
  <h1>Jabberwocky</h1>
<pre>
'Twas brillig, and the slithy toves
Did gyre and gimble in the wabe;
All mimsy were the borogoves,
And the mome raths outgrabe.

"Beware the Jabberwock, my son!
The jaws that bite, the claws that catch!
Beware the Jubjub bird, and shun
The frumious Bandersnatch!"

He took his vorpal sword in hand:
Long time the manxome foe he sought—
So rested he by the Tumtum tree,
And stood awhile in thought.

And as in uffish thought he stood,
The Jabberwock, with eyes of flame,
Came whiffling through the tulgey wood,
And burbled as it came!

One, two! One, two! and through and through
The vorpal blade went snicker-snack!
He left it dead, and with its head
He went galumphing back.

"And hast thou slain the Jabberwock?
Come to my arms, my beamish boy!
O frabjous day! Callooh! Callay!"
He chortled in his joy.

'Twas brillig, and the slithy toves
Did gyre and gimble in the wabe;
All mimsy were the borogoves,
And the mome raths outgrabe.
</pre>
</article>
<article class="sample-text">
  <h1>Lemma XIV.</h1>
<p><i>
Perpendiculum quod ab umbilico Parabolæ ad tangentem ejus demittitur, medium est proportionale inter distantias umbilici a puncto contactus & a vertice principali figuræ.</i></p>
<p>
Sit enim APQ Parabola, S umbilicus ejus, A vertex principalis, P punctum contactus, PO ordinatim applicata ad diametrum principalem, PM tangens diametro principali occurrens in M, & SN linea perpendicularis ab umbilico in tangentem. Jungatur AN, & ob æquales MS & SP, MN & NP, MA & AO, parallelæ erunt rectæ AN & OP, & inde triangulum SAN rectangulum erit ad A & simile triangulis æqualibus SMN, SPN. Ergo PS est ad SN ut SN ad SA.   Q. E. D.</p>
<p>
Corol. 1. PSq. est ad SNq. ut PS ad SA.</p>
<p>
Corol. 2. Et ob datam SA, est SNq. ut PS.</p>
<p>
Corol. 3. Et concursus tangentis cujusvis PM cum recta SN quæ ab umbilico in ipsam perpendicularis est, incidit in rectam AN, quæ Parabolam tangit in vertice principali.</p>
</article>
<main>
<h3>How it works:</h3>
<p>The list of font families initially only contains the four CSS generic font family names.  A request is made to the Google web fonts developer API for the list of available web fonts, sorted by popularity.<p>

<p>For each font family, an entry is added to the selection list and its font is styled accordingly (although it will only render in that font if and when it is loaded).  Fonts files are loaded initially only as <em>samples</em>, containing just enough letters to draw the font family names.  This uses the Google Fonts option of <a href="https://developers.google.com/fonts/docs/getting_started#Optimizing_Requests">allowing you to specify a sample text in a stylesheet import rule</a>; it will return a font-face rule with a link to a source file that only contains the letters necessary to render that text.  The font samples are loaded in batches, one import stylesheet request for every 10 font sample files.  (A larger batch size would mean fewer import requests, but more and possibly larger font files, since the request would require all the letters in all the font names.)  Batches of font samples are loaded as needed as the user scrolls the font list.</p>

<p>When the user actually selects a given font, a CSS style rule is created, applying that font to the target text.  A new font-face rule is also created, loading the complete font file from a file URL contained in the original API query results.  Google fonts handles all the browser-sniffing necessary to deal with font format requirements.</p>

<p>The manipulation of the data, the DOM and the CSS OM relies on the d3 Javascript library.  I expect at least some of the CSS OM methods require IE9+.</p>

<h3>Required elements:</h3>
<p>The mark-up must contain one or more <code>&lt;div></code> elements of class "font-picker", which in turn contain one or more <code>&lt;div></code>s with classes "font-family" and "selection-list".</p>
<p>One or both of the nested divisions must contain the attribute "data-target-selector", with a valid CSS selector as the value.  If both the outer font picker and the inner selection list have target selectors specified, the two will be combined such that the inner selectors are descendents of the outer selectors.  For example, using the code from the example above:</p>
<blockquote><pre><code>&lt;div class="font-picker" data-target-selector=".sample-text">
    &lt;label for="text-family-list">Main Text Font:&lt;/label>
  &lt;div id="text-family-list" class="selection-list font-family">
  &lt;/div>
  &lt;label for="heading-family-list">Heading Font:&lt;/label>
  &lt;div id="heading-family-list"
       class="selection-list font-family"
        data-target-selector="h1, h2, h3">
  &lt;/div>
  &lt;!-- <a href="https://codepen.io/AmeliaBR/pen/tkJCm">Colour-picker code</a>-->
&lt;/div></code></pre></blockquote>
<p>The main text font list applies to all text that matches the font-picker's selector (<code>".sample-text"</code>); the heading font choice applies to any text that matches any of "<code>.sample-text h1, .sample-text h2, .sample-text h3</code>".</p> 
<h3>Optional customization:</h3>
<p>The font-picker may contain other content, such as the headings, labels or the colour-pickers.  Don't add any content within the selection list itself, as that might interfere with (not-yet-implemented) sorting functionality.</p>
<h3>Still To Do:</h3>
<ul>
  <li>Enable sorting and filtering of the font list.</li>
  <li>Provide information on (and allow selection of) the available font variants (italic, small-caps and different weights).</li>
  <li>Incorporate other font parameters, such as font-size.</li>
  <li>Find a better solution for the problem that you can't use the keyboard to switch <em>focus</em> between radio-button group options without <em>also</em> changing the selection.  Currently, I deal with it by waiting half a second after a selection changes before loading the full font file, but that (a)&nbsp;causes a similar delay when selecting an option with the pointer; and (b)&nbsp;probably will still end up with too many downloads for keyboard users.</li>
</ul>
</main>

<hr/>
Created by <a href="https://codepen.io/AmeliaBR/">Amelia Bellamy-Royds</a> in March & April 2014.
  <script src='https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.2/d3.min.js'></script>

    <script  src="js/index.js"></script>

</body>
</html>
/* Downloaded from https://ahoj.co.uk/ */
/*** The custom web font picker styles ***/
/* Created by Amelia Bellamy-Royds, March-April 2014
   Released under an MIT licence, so you're free to use 
   and adapt the code so long as you maintain credit and 
   use a compatible licence.
   See https://codepen.io/AmeliaBR/pen/BmlIx 
   for the original code and examples of use.
*/
div.font-picker div.selection-list {
  max-height:9em;
  overflow-y:scroll;
  box-sizing:border-box;  
  margin:0.5em;
  position:relative; 
  /* so that child offsets are defined relative to the list container */
}
div.font-picker div.selection-list  label  {
  display:block;
  width:95%;
  width:calc(100% - 6px - 1em);
  padding:0.25em 0.5em;
  margin:0 auto;
  position:relative;
}
div.font-picker div.selection-list input {
  display:block;
  position:absolute;
  height:1;width:1;overflow:hidden;
  margin-left:-100%;
  box-sizing:content-box;
}
div.font-picker div.selection-list input:checked + label {
  border:solid 3px darkred;
}


/* Colour Picker Styles imported as an external resource */
/* Downloaded from https://ahoj.co.uk/ */
/*** The custom web font picker code ***/
/* Created by Amelia Bellamy-Royds, March-April 2014
   Released under an MIT licence, so you're free to use 
   and adapt the code so long as you maintain credit and 
   use a compatible licence.
   See https://codepen.io/AmeliaBR/pen/BmlIx 
   for the original code and examples of use.
*/
var webFontsAPIKey = "AIzaSyBdCH1y7a3z6nN33JrDml5StONM35rCpGM",
    /* Please replace with your own API key if you fork this pen */    
    
    webFontsAPIURL = "https://www.googleapis.com/webfonts/v1/webfonts?",
    webFontsURL = "https://fonts.googleapis.com/css?";

var defaultFonts = [
  {label:"Default sans-serif", 
   family:"sans-serif", 
   kind:"default"},
  {label:"Default serif", 
   family:"serif", 
   kind:"default"},
  {label:"Default monospaced", 
   family:"monospace", 
   kind:"default"},
  {label:"Default handwriting", 
   family:"cursive", 
   kind:"default"},
  ];
var sortOrderOptions = [
  "popularity"
  ,"trending"
  ,"alpha"
  //,"style"
  //,"date"
  ];
  
var webFonts = defaultFonts,
    sortOrder = sortOrderOptions[0],
    batchSize = 10; //how many font samples to load at a time

var importStyleSheet = d3.select("head").append("style")
        .attr("class", "font-picker-fontImportRules")
      .node().sheet;
var fontStyleSheet = d3.select("head").append("style")
        .attr("class", "font-picker-fontRules")
      .node().sheet;

var lastFontID = 0;
var picker = d3.selectAll("div.font-picker")
    .datum(function(d){
        d = d || {};
      
        var thisPicker = d3.select(this);
        d.id = this.id || "font-picker-id-" + lastFontID++;
        if (!this.id) thisPicker.attr("id", d.id);
        d.targetSelector = thisPicker.attr("data-target-selector");
      
        return d;
    })
    .call(updateFamilyList) //add the default fonts while waiting
    .call(loadWebFontList); //load the webFonts and update the list
  
function updateFamilyList(fontPicker){
  //fontPicker is a d3 selection of "div.font-picker"
  //elements to update
  
  fontPicker.each(function(parent) {
    //copy data into child elements
    d3.select(this).selectAll(".font-family.selection-list")
      .datum(function(d) {
        if(d) return d;
        //else initialize:
        var selector;
        if (this.hasAttribute("data-target-selector")) {
          var parentSplit = parent.targetSelector.split(",");
          var thisSplit = this.getAttribute("data-target-selector").split(",");
          var selectorList = [];
          parentSplit.forEach(function(p){
            thisSplit.forEach(function(t) {
              selectorList.push(p + " " + t);
            });
          });
          selector = selectorList.join(", ");
        } 
        else { 
          selector = parent.targetSelector;
        }
        var id = this.id || parent.id + "-list";
        return {targetSelector: selector,
                id:id};
      });
    });

  //handle the rest as a nested selection
  var familyPick = fontPicker.selectAll(".font-family.selection-list")
      .on("scroll", function(d){
        //require a half-second pause after scrolling
        //before loading the font samples for visible fonts
        if (d.trigger) clearTimeout(d.trigger);
        var that = this;
        d.trigger = setTimeout(function(){
           var children = that.querySelectorAll("div.option"),
               visible = that.scrollTop;
          //console.log(that, visible, children.length);
          for(var i=0, max=children.length; i<max;i++){
            if (children[i].offsetTop >= visible) {
              loadWebFontTitles(i-1, batchSize);
              break;
            }
          }
          d.trigger = null;
        }, 300);
      })
      .on("change", function(d){
        //bubbled-up event from the radio buttons
        //Because using the keyboard to cycle between radio buttons
        //causes a selection change as well as a focus change,
        //require the selection to persist for a half-second pause
        //before loading the complete font
        //(the title will still be triggered by focus/scroll)
        if (d.trigger2) clearTimeout(d.trigger2);
        var that = this,
            theEvent = d3.event;
        d.trigger2 = setTimeout(function(){
           loadNewFont.call(that, d, theEvent);
          d.trigger2 = null;
        }, 500);
      });
  
  var familyOptions = familyPick.selectAll("div.option")
    .data(webFonts, //actually includes all fonts, including default 
            function(d){return d.family;} );

  var newOptions = familyOptions.enter().insert("div")
    .attr("class", "font-family option");
  newOptions.append("input")
    .attr("type","radio")
    .attr("id", familyAsID)
    .attr("name", listAsName)
    .attr("value", function(d){
        return d.family;
      })
    .on("focus", function(d,i){ //scroll into view
        this.offsetParent.scrollTop = this.offsetTop - 5;
        /*
        if (!this.importRule) {
          //load this font sample and the next batch after it
          loadWebFontTitles(i, batchSize);
        }//(now triggered by the scroll itself)*/
      });
  
  newOptions.append("label")
    .attr("for", familyAsID)
    .text(function(d){return d.label || d.family; })  
    .style("font-family", function(d){ 
        return d.family;
      }, "important")
  
  function listAsName(d){
        return this.parentNode.parentNode.id + "-font-family";
      //the "grandparent" is the list div
  }
  function familyAsID(d){
        return listAsName.call(this,d) + d.family.replace(/\s/, "");
  }
  
  
}/* End of updateFamilyList */
function loadWebFontList(fontPicker) {
  var completeURL = webFontsAPIURL + "key=" + webFontsAPIKey +
          "&sort=" + sortOrder;
  d3.json(completeURL, processFontList);
  
  function processFontList(error, fontList) {
    if(error) {
       console.log("Could not access ", webFontsAPIURL);
       return;
    }
    webFonts = webFonts.concat(fontList.items);
    
    webFonts.forEach(function(d) {
      d.familyPlus = d.family.replace(/\s/g, "+");
      d.url= encodeURIComponent(d.family);
    });
    
    //construct the import statement for the
    //first batch of font titles
    loadWebFontTitles(0, batchSize)
    updateFamilyList(fontPicker);
  }
} /* end of loadWebFontsList */

function loadWebFontTitles(startIndex, length){
  //Create a CSS Import Rule to load the sample texts
  //for the specified elements in the web fonts array
    var slice = webFonts.slice(startIndex, startIndex+length)
          .filter(function(d) {
              return ((!d.importRule)&&
                (d.kind == "webfonts#webfont"));
          });
    if (!slice.length) return;
    //console.log("loading font samples for", slice);
    var ruleNum = importStyleSheet.cssRules.length;
  
    var familyList = "",
        textList = "";
  
  slice.forEach(function(d){
      familyList += "|" + d.familyPlus;
      if (d.variants.indexOf("regular") < 0)
           familyList += ":" + d.variants[0];
      textList += d.url;
    });
  familyList = familyList.slice(1); //remove the first "|"
  
  importStyleSheet.insertRule(
                ("@import url(" + webFontsURL + 
                 "family=" + familyList +
                 "&text=" + textList +")"), 
                ruleNum);
   var rule = importStyleSheet.cssRules[ruleNum++];
    //console.log(rule);
    slice.forEach(function(d){
         d.importRule = rule; //store a reference
      });
}

function loadNewFont(familyPickerData, changeEvent) {
  //called on a change event received *by the list*
  //The first passed-in value is the data object for the
  //font picker as a whole (with info on the target).
  //The second is the event object, from which the
  //data for the selected option can be accessed:
  
  var fontData = d3.select( changeEvent.target ).datum();
  
  //console.log("this",this,familyPickerData);
  //console.log("target", d3.event.target, fontData);
  
  var ruleNum = fontStyleSheet.cssRules.length;
  
  if ((!fontData.fontFaceRule)&&
      (fontData.kind == "webfonts#webfont") ) {
     //only the title has been loaded so far;
    
    var variant = {};
    var file = fontData.files["regular"];
    if (!file) {
      variant.name = fontData.variants[0];
      variant.weight = variant.name.replace(/\D*/, "");
      variant.style = variant.name.replace(/\d*/, "");
      file = fontData.files[ variant.name ];
    }
    
    fontStyleSheet.insertRule(
        ["@font-face {",
         "font-family:", fontData.family,
         "; font-style:", (variant.style||"normal"), 
         "; font-weight:", (variant.weight||"400"),
         "; src:", "local(" + fontData.family + "),",
                   "local(" + fontData.family.replace(/\s/g, "") + "),",
                   "url(" + file + ")",
         "}"
        ].join(" "),
        ruleNum);
    
    fontData.fontFaceRule = fontStyleSheet.cssRules[ruleNum++];
  }
  
  if (!familyPickerData.targetRule) {
    fontStyleSheet.insertRule(
                (familyPickerData.targetSelector + "{}" ), 
                ruleNum);
    familyPickerData.targetRule = fontStyleSheet.cssRules[ruleNum++];
  }
  
  familyPickerData.targetRule.style["fontFamily"] = fontData.family;
}
          


/*** End of the Web Font Picker code ***/
  
/*** The custom colour picker code ***/
/* Created by Amelia Bellamy-Royds, March 2014
   Released under an MIT licence, so you're free to use 
   and adapt the code so long as you maintain credit and 
   use a compatible licence.
   See https://codepen.io/AmeliaBR/pen/tkJCm 
   for the original code and examples of use.
*/
  
  /* Initialize */
  var styleElement = d3.select("head").append("style")
        .attr("class", "colour-picker-styles");
  var styleSheet = styleElement.node().sheet;
  var lastID = 0;
  var formatPercent = d3.format("0.5%");
  var radiansPerDegree = Math.PI/180;
  
function initializeColourPickers() {
  var colourPick = d3.selectAll("div.colour-picker");
  
  colourPick.datum(function(d,i) {
      if (!d) { /* Create the data object */
        d = d || {};
        var thisPicker = d3.select(this);
        d.id = this.id || "colour-picker-id-" + lastID++;
        if (!this.id) thisPicker.attr("id", d.id);
  
        d.inputNode = thisPicker.select("input").node();
        d.hslValue = d3.hsl( d.inputNode.value || "#888" );
        d.targetSelector = thisPicker.attr("data-target-selector");
        d.targetStyle = thisPicker.attr("data-target-style");
        
        /* Create empty style rules for the target and for
           all the pseudo-elements of the colour picker itself */
        var ruleNum = styleSheet.cssRules.length;  
        styleSheet.insertRule(
                (d.targetSelector + "{}" ), 
                ruleNum);
        d.targetRule = styleSheet.cssRules[ruleNum++];

        styleSheet.insertRule(
            ("#" + d.id + "::before {}" ), 
            ruleNum);
        d.circleLightnessRule = styleSheet.cssRules[ruleNum++];

        styleSheet.insertRule(
            ("#" + d.id + " > *:last-child::before {}" ), 
            ruleNum);
        d.circleDarknessRule = styleSheet.cssRules[ruleNum++];

        styleSheet.insertRule(
            ("#" + d.id + " > *:last-child::after {}" ), 
            ruleNum);
        d.colourPointRule = styleSheet.cssRules[ruleNum++];

        styleSheet.insertRule(
                ("#" + d.id + "::after {}" ), 
                ruleNum);
        d.brightnessPointRule = styleSheet.cssRules[ruleNum++];
      } 
      //console.log(d);
      return d;
    })
    .each(updateAria)
    .each(updateStyles)
    .on("mousedown", function(d){ 
      if (d3.event.target != this)
            return;
        //don't grab events from the input element
        //or label, only from the div pseudo-elements
        //(label pseudo-elements do not catch mouse events)
      var mouse = d3.mouse(this),
          styles = getComputedStyle(this);
      
      /* store the colour picker dimensions for the duration
         of the drag event */
      d.width = parseFloat(styles["width"]);
      d.height = parseFloat(styles["height"]) + parseFloat(styles["paddingBottom"]);
      d.fontSize = parseFloat(styles["fontSize"]);
      
      if ( mouse[1] >= (d.height - d.width - d.fontSize/2) ) {
        trackColourCircle.call(this, d);
        d3.select(this).on("mousemove.colour",
                           trackColourCircle);
      } else {
        trackBrightnessSlider.call(this, d);
        d3.select(this).on("mousemove.colour",
                           trackBrightnessSlider);
      }
    })
    .on("mouseup", function(d){ 
      d3.select(this).on("mousemove.colour", null);
    })
    .on("keydown", trackArrows)
    .on("keypress", trackPlusMinus)
  .select("input")
    .on("change", function(d){
      //console.log("input changed");
      d.hslValue = d3.hsl(this.value);
      updateStyles(d);
    })
    .on("focus", function(d){
      //console.log("input focus");
      d3.select("#" + d.id).attr("inputFocused", true);
    })
    .on("blur", function(d){
      //console.log("input blur");
      d3.select("#" + d.id).attr("inputFocused", null);
    });
} /* End of Initialize function */
  
  function hueSaturationAsLeftPercent(d){
      /* hue is an angle in the circle,
         saturation is a radial distance,
         convert first to x/y coordinates relative to
         circle center, then to percent margin 
         A hue of zero (red) is 30degrees past vertical
         on my circle, and hue increases clockwise. */
      if (isNaN(d.hslValue["h"]) ) return "50%";
      var x = d.hslValue["s"] * Math.sin(
          (d.hslValue["h"] + 30)*radiansPerDegree
        );
      return formatPercent( 0.5 + x/2 );
  }
  function hueSaturationAsBottomPercent(d){
      if (isNaN(d.hslValue["h"]) ) return "50%";
      var y = d.hslValue["s"] * Math.cos(
          (d.hslValue["h"] + 30)*radiansPerDegree
        );
      return formatPercent( 0.5 + y/2 );
  }
  function hueSaturationAsPureColour(d){
    return d.hslValue["s"] ? //zero saturation value, NaN hue
      d3.hsl(d.hslValue["h"], d.hslValue["s"], 0.5).toString() :
      "#888888";
  }
  function lightnessAsLeftPadding(d){
      return formatPercent( d.hslValue["l"] );
  }
  function lightnessAsRightPadding(d){
      return formatPercent(1 - d.hslValue["l"] );
  }
  function lightnessAsProportionIntensity(d) {
    return Math.min(1, 2*(1-d.hslValue["l"]) );
  }
  function lightnessAsTranslucentBlack(d) {
    return "rgba(0,0,0," + Math.max(0, (1-2*d.hslValue["l"]) ) + ")";
  }

  function updateAria(d,i) {
    //called on each colour picker
    //only needs to be re-called if the target of the picker
    //changes -- the aria value property will be updated
    //by updateStyles
    var selectThis = d3.select(this)
      .attr("role", "slider")
      .attr("aria-controls", 
            d3.selectAll(d.targetSelector)[0]
            .map(function(element){ return element.id;})
            .join("; ")
            );
    /* The controls field may end up empty, since the 
       elements being modified may not have valid ids */
    var labelElement = selectThis.select("label"),
          labelString =  "Colour selector for ";
    if ( labelElement.empty() )
        labelString += d.targetStyle;
    else {
        labelString += labelElement.text();
    }
    selectThis.attr("aria-label", labelString);
                      
  }

  function updateStyles(d,i) {
      /* apply the colour styles and redraw the colour picker
         based on the value stored in the data object */
  
    /* Use an inline style to display the colour
       on the actual <input> element, and update its value */
    d.inputNode.value = d.hslValue.toString();
    d3.select(d.inputNode)
        .style("border-color", d.hslValue );
    
    /* Update the aria value */
    d3.select("#" + d.id)
      .attr("aria-valuenow", 
            "hue:" + d.hslValue.h + 
            " degrees; Saturation:" + formatPercent(d.hslValue.h) +
            "; Lightness:" + formatPercent(d.hslValue.l) );
    
    /* Update the CSS rules to apply the styles */  
        d.targetStyle.split(",").forEach( function(style){
            d.targetRule.style[ style.trim() ] =  
                d.hslValue.toString();
        });
    
        d.circleLightnessRule.style["opacity"] = 
          lightnessAsProportionIntensity(d);

        d.circleDarknessRule.style["backgroundColor"] =
          lightnessAsTranslucentBlack(d);

        d.colourPointRule.style["marginBottom"] =
          hueSaturationAsBottomPercent(d);
        d.colourPointRule.style["marginLeft"] =
          hueSaturationAsLeftPercent(d);

        d.brightnessPointRule.style["backgroundColor"] =   
          hueSaturationAsPureColour(d);
        d.brightnessPointRule.style["paddingLeft"] =   
          lightnessAsLeftPadding(d);
        d.brightnessPointRule.style["paddingRight"] =   
          lightnessAsRightPadding(d);
  }

function trackColourCircle(d) {
  /* Called on mouse drag that started within the circle */
  var mouse = d3.mouse(this),
      radius = d.width/2,
      relX = radius - mouse[0],
      relY = radius - (d.height - mouse[1]),
      r = Math.sqrt( relX*relX + relY*relY),
      a = Math.atan2( -relX, -relY );
  
  d.hslValue = d3.hsl((360 + a/radiansPerDegree - 30)%360,
                      Math.min(1, r/radius),
                     d.hslValue["l"] /* maintain lightness */
                     );
  updateStyles(d);
}
function trackBrightnessSlider(d) {
  /* Called on mouse drag that started within the slider */
  var mouse = d3.mouse(this);
  d.hslValue["l"] =  mouse[0]/d.width;
  updateStyles(d);
}
function trackPlusMinus(d) { 
 /* called on a keyPRESS event */ 
  if (d3.event.target != this)
            return;
    //don't grab events from the input element
  switch(d3.event.charCode) {
    case 43: //'+' 
      d.hslValue["l"] = Math.min(1,d.hslValue["l"] + 0.01);
      break;
    case 45: //'-'
      d.hslValue["l"] = Math.max(0,d.hslValue["l"] - 0.01);
      break;      
    default:
      return; //all other keystrokes
  }
  updateStyles(d); 
              
}
function trackArrows(d) {
  /* called on a keyDOWN event */ 
  if (d3.event.target != this)
            return;
  var arrows = { left: 37,
                 up: 38,
                 right: 39,
                 down: 40,
                numLeft:100, //4 on the NumPad
                numUp:104, //8 on the NumPad
                numRight:102,  //6 on the NumPad
                numDown:98, //2 on the NumPad
                leftUp:36, //"home" (top left of NumPad)
                numLeftUp:103, //7 on the NumPad, same key as "home"
                rightUp: 33, //etc...
                numRightUp: 105,
                rightDown: 34,
                numRightDown: 99,
                leftDown: 35,
                numLeftDown: 97,
                center: 12, //middle button on NumPad
                numCenter: 101 //with NumLock
                 };
  
  //get x,y coordinates
  if(isNaN(d.hslValue["h"])) d.hslValue["h"] = 0;
  var x = d.hslValue["s"] * Math.sin(
          (d.hslValue["h"] + 30)*radiansPerDegree
        ),
      y = d.hslValue["s"] * Math.cos(
          (d.hslValue["h"] + 30)*radiansPerDegree
        );
  
  //adjust
  switch(d3.event.keyCode) {
    case arrows.left: case arrows.numLeft: 
    case arrows.leftUp: case arrows.numLeftUp: 
    case arrows.leftDown: case arrows.numLeftDown:
      x = Math.max(-1, x - 0.05);
      break;
    case arrows.right: case arrows.numRight: 
    case arrows.rightUp: case arrows.numRightUp:
    case arrows.rightDown: case arrows.numRightDown:
      x = Math.min(1, x + 0.05);
      break;
    case arrows.up: case arrows.numUp:
    case arrows.down: case arrows.numDown:
    case arrows.center: case arrows.numCenter:
      break;  //skip to next block
    default:
      return; //all other keystrokes
  }
  switch(d3.event.keyCode) {
    case arrows.up: case arrows.numUp:
    case arrows.leftUp: case arrows.numLeftUp: 
    case arrows.rightUp: case arrows.numRightUp:
      y = Math.max(-1, y + 0.05);
      break;
    case arrows.down: case arrows.numDown:
    case arrows.leftDown: case arrows.numLeftDown:
    case arrows.rightDown: case arrows.numRightDown:
      y = Math.min(1, y - 0.05);      
      break;
    case arrows.center: case arrows.numCenter:
      x = 0; y = 0;
  }
  d3.event.preventDefault();
  
  //convert back to polar coordinates
  var r = Math.sqrt(x*x + y*y),
      a = Math.atan2( x, y );
  
  d.hslValue = d3.hsl((360 + a/radiansPerDegree - 30)%360,
                      Math.min(1, r),
                     d.hslValue["l"] /* maintain lightness */
                     );
   updateStyles(d);
}
initializeColourPickers();

/*** End of the custom colour picker code ***/

This awesome code is write by Amelia Bellamy-Royds, you can se more from this user in the personal repository