mardi 18 juin 2013

Une jauge avec D3.js

Si vous ne connaissez pas D3.js je vous invite à jeter un oeil sur cette librairie javascript qui fait des merveille: http://d3js.org/
Pour résumer d3.js facilite le binding entre des données et le dom d'une page html (balise SVG inclu).

 Prenez le temps de parcourir les exemples, certains sont assez bluffant de par le rendu visuel mais aussi souvent de par la simplicité du code.
 En ce qui me concerne dans un cadre professionnel  j'ai déja fait quelques graph avec un peu d’interactions, et j'ai aussi utilisé le dendrogram (http://bl.ocks.org/mbostock/4063570) en lui ajoutant lui aussi de l'interaction, le gros avantage du SVG c'est qu'il s'agit de balises au meme titre qu'un bon vieux DIV, donc au final elles gèrent les mêmes événements et les mêmes CSS, contrairement a la balise canvas qui elle est une boite noir.
Dernièrement je me suis amusé à détourner une jauge pour lui donner un aspect, a mon sens, plus fun et plus visuel.
 Je suis donc parti de cet exemple :
 http://bl.ocks.org/msqr/3202712
 Pour en arriver à ceci :

<script src="http://d3js.org/d3.v2.min.js" type="text/javascript"></script>
 <style>
 body {
  font-family:  Helvetica, Arial, sans-serif;
  margin: 32px;
 }
 
 #power-gauge g.arc {
  fill: steelblue;
 }

 #power-gauge g.pointer {
  fill: #e85116;
  stroke: #b64011;
 }
 
 #power-gauge g.label text {
  text-anchor: middle;
  font-size: 14px;
  font-weight: bold;
  fill: #666;
 }
 
 #power-gauge .valueText {
  text-anchor: middle;
  font-size: 30px;
  font-weight: bold;
  fill: blue;
 }
 
 #power-gauge .titleText {
  text-anchor: middle;
  font-size: 20px;
  font-weight: bold;
  fill: #FFF;
 }
 
</style>


<div id="power-gauge">
</div>
<script>
var gauge = function(container, configuration) {
 var that = {};
 this.config = {
  size      : 200,
  clipWidth     : 200,
  clipHeight     : 110,
  ringInset     : 20,
  ringWidth     : 20,
  
  minValue     : 0,
  maxValue     : 10,
  
  minAngle     : -90,
  maxAngle     : 90,
  
  transitionMs    : 1000,
  
  majorTicks     : 5,
  labelFormat     : d3.format(',g'),
  labelInset     : 10,
  title      : 'Title',
  
  selectColor     : function(d){
          if(d>8){
           return 'red';
          }else if(d>6 && d<=8){
           return 'orange'
          }else if(d>4 && d<=6){
           return 'yellow';
          }else if(d>2 && d<=4){
           return  'blue';
          }else if(d>0 && d<=2){
           return 'green';
          }
         }
 };
 
 
 var range = undefined;
 var r = undefined;

 var value = 0;
 
 var svg = undefined;
 var arc = undefined;
 var scale = undefined;
 var ticks = undefined;
 var tickData = undefined;
 
 var value = {previous:0, value:0};

 var donut = d3.layout.pie();
 
 this.deg2rad = function (deg) {
  return deg * Math.PI / 180;
 }
 
 this.newAngle =  function(d) {
  var ratio = scale(d);
  var newAngle = this.config.minAngle + (ratio * range);
  return newAngle;
 }
 
 this.configure = function(configuration) {
  var that = this;
  var prop = undefined;
  for ( prop in configuration ) {
   this.config[prop] = configuration[prop];
  }
  
  range = this.config.maxAngle - this.config.minAngle;
  r = this.config.size / 2;

  // a linear scale that maps domain values to a percent from 0..1
  scale = d3.scale.linear()
   .range([0,1])
   .domain([this.config.minValue, this.config.maxValue]);
   
  ticks = scale.ticks(this.config.majorTicks);
  tickData=[1];
  console.log("Tickdata:"+tickData);
  this.arc = d3.svg.arc()
   .innerRadius(r - this.config.ringWidth - this.config.ringInset)
   .outerRadius(r - this.config.ringInset)
   .startAngle(function(d, i) {
    var ratio = d * i;
    var value =that.deg2rad(that.config.minAngle + (ratio * range));
    console.log('start angle:'+value);
    return value;
   })
   .endAngle(function(d, i) {
    var ratio = d * (i+1);
    console.log('minAngle='+that.config.minAngle+', ratio='+ratio+' , range='+range);
    var value =that.deg2rad(that.config.minAngle + (ratio * range));
    console.log('end angle:'+value);
    return that.deg2rad(that.config.minAngle + (ratio * range));
   });
   
  this.arcPointer = d3.svg.arc()
   .innerRadius(r - this.config.ringWidth - this.config.ringInset)
   .outerRadius(r - this.config.ringInset)
   .startAngle(function(d, i) {
    return that.deg2rad(-90);
   })
   .endAngle(function(d, i) {
    var ratio = scale(d);
    return that.deg2rad(that.config.minAngle + (ratio * range));
   });
 }
 
 this.centerTranslation = function centerTranslation() {
  return 'translate('+r +','+ r +')';
 }
 
 this.isRendered = function() {
  return (svg !== undefined);
 }

 
 this.render = function(newValue) {
  var that = this;
  svg = d3.select(container)
   .append('svg:svg')
    .attr('class', 'gauge')
    .attr('width', this.config.clipWidth)
    .attr('height', this.config.clipHeight);
  
  var centerTx = this.centerTranslation();
  
  this.arcs = svg.append('g')
    .attr('class', 'arc')
    .attr('transform', centerTx);
  
  this.arcs.selectAll('path')
    .data([1])
   .enter().append('path')
    .attr('fill', '#FFF')
    .attr('d', this.arc);
  
  var lg = svg.append('g')
    .attr('class', 'label')
    .attr('transform', centerTx);
  lg.selectAll('text')
    .data(ticks)
   .enter().append('text')
    .attr('transform', function(d) {
     var ratio = scale(d);
     var newAngle = that.config.minAngle + (ratio * range);
     return 'rotate(' +newAngle +') translate(0,' +(that.config.labelInset - r) +')';
    })
    .text(this.config.labelFormat);

   
  this.arcs2 = svg.append('g')
   .attr('class', 'arc')
   .attr('transform', centerTx);
  

  
  var textValue = svg.append('g')
    .attr('class', 'arc')
    .attr('transform', centerTx);
    
  var titleValue = svg.append('g')
    .attr('transform', 'translate('+r +','+ (r +25)+')');
    
  this.valueTextCenter = textValue.append('text').attr('class','valueText').text('0,0');
  this.titleTextCenter = titleValue.append('text').attr('class','titleText').text(this.config.title);
  this.update(newValue === undefined ? 0 : newValue);
 }

 
 this.update = function(newValue, newConfiguration) {
  var that = this;
  if ( newConfiguration  !== undefined) {
   this.configure(newConfiguration);
  }
  value.previous = value.value;
  value.value = newValue;
  this.valueTextCenter.text(newValue.toFixed(0)+"%");
  var ratio = scale(newValue);
  var newAngle = this.config.minAngle + (ratio * range);
  
  indicator = this.arcs2.selectAll('path').data([value.value]);
  indicator.enter().append("svg:path").transition().ease('linear')
  .duration(this.config.transitionMs)
  .attrTween('d', function(a){
   
     var i = d3.interpolate(value.previous, a);
     //this._current = i(0);
     return function(t) {
    return that.arcPointer(i(t));
     };

  });
  indicator.transition()
     .ease("linear")
     .duration(this.config.transitionMs)
     .attrTween("d", function(a){
   
     var i = d3.interpolate(value.previous, a);
     //this._current = i(0);
     return function(t) {
    return that.arcPointer(i(t));
     };

  }).attr('fill',function(d){
   return that.config.selectColor(d);
  });
   

 }


 this.configure(configuration);
 
};
</script>

<script>
function onDocumentReady() {

 
 var powerGauge = new gauge('#power-gauge', {
  size: 300,
  clipWidth: 300,
  clipHeight: 300,
  ringWidth: 30,
  maxValue: 100,
  transitionMs: 1000,
  selectColor: function(d){
      if(d>80){
       return 'red';
      }else if(d>60 && d<=80){
       return 'orange'
      }else if(d>40 && d<=60){
       return 'yellow';
      }else if(d>20 && d<=40){
       return  'blue';
      }else if(d>0 && d<=20){
       return 'green';
      }
     }
 });
 powerGauge.render(); 
 
 function updateReadings() {

  powerGauge.update(Math.random() * 100);
 }
 
 updateReadings();
 setInterval(function() {
  updateReadings();
 }, 3 * 1000);
}

if ( !window.isLoaded ) {
 window.addEventListener("load", function() {
  onDocumentReady();
 }, false);
} else {
 onDocumentReady();
}
</script>

Je suis pas encore un specialiste javascript, j'ai mes bonnes vieilles habitude issue du java, j'ai pas pu m'empecher d'en faire un objet afin de pouvoir l'instancier via un "new" sans vraiment savoir s'il s'agit d'une bonne pratique en javascript.
Evidemment on peut en instancier autant qu'on veut par page.
De plus le code est encore à épurer, mais il est fonctionnel !