XD blog

blog page

d3.js, javascript, visualisation


2013-09-29 Zoomer sur graphe XY avec d3.js

d3.js est un framework écrit en javascript qui permet de visualiser des données. Il est conçu pour des sites web et permet de créer des graphes animés. Le résultat est assez esthétique à en juger par les différentes galleries :

Tout d'abord, je dois dire que ça prend du temps de concevoir un graphe. Mon objectif était de créer un graphe XY et de pouvoir zoomer horizontalemen sur une partie. Quand on découvre l'outil et les spécifités de javascript, il est préférable de partir d'une exemple qui fonctionne déjà sans trop chercher à aggréger des bouts de codes venant de plusieurs graphes. L'exemple que j'ai trouvé pour le zoom vient d'ici : js fiddle. Appliqué à mes données, cela donne :

Les premiers pas sont tout de même assez longs, les erreurs ne sont pas faciles à comprendre, surtout quand on est novice. On espère que ce graphe qu'on est en train de constuire pourra resservir. Cela dit voici, quelques astuces.

Avec on sans animation

Lorsqu'on regarde un script classique écrit avec d3.js, on distingue trois parties (voir nbar):

La fonction d3.tsv est particulière : elle ne charge pas les données immédiatement. La page qui contient le graphe s'affiche d'abord, ensuite la fonction télécharge les donnéesde façon désynchronisées qui sont ensuite traitées par la fonction en accolade. Cela permet d'éviter d'attendre lorsque les données sont en grand nombre. Pour le zoom (ou une animation), on ne peut pas utiliser ce mécanisme car les données vont être constamment affichées, réaffichees, il faut donc qu'elle soient insérées sous forme de tableau dans le script javascript et non chargée. Ceci explique le code fournit qui produit le graphe ci-dessus (et que vous pouvez lire à la fin de cet article).

Le débuggeur de Firefox ou Chrome

Pour comprendre ses erreurs, les navigateurs proposent des outils d'aides au développement. On peut exécuter son script pas à pas, voir l'état des variables. Il suffit de charger sa page locale dans le navigateur et d'aller dans le menu options / outils de dévloppement . il est utile d'aller jeter un coup d'oeil pour voir les noeuds HTML que le script ajoute.

Le graphe est vide (et les données dans un fichier local)

Pour des raisons de sécurité, Chrome ne permet pas le chargement de données local par l'intermédiaire de la fonction d3.tsv ou d3.json. Le plus simple est de l'autoriser à le faire en tapant :

"C:\Program Files (x86)\Google\Chrome\Application\Chrome.exe" --allow-file-access-from-files
La navigateur acceptera de lire des données depuis un url du type file://d:/.... Néanmoins, il est préférable, avant de retourner à internet, de fermer Chrome puis de le réouvrir sans cet option. Vous pouvez aussi utiliser l'astuce suivante.

Eviter l'usage de la fonction d3.json

La plupart des scripts javascript utilisant d3.js utilise la fonction d3.json (exemple) :

d3.json("mes_donnees.json", function(data_json) {

    // ...
    var node = svg.selectAll(".node")
            .data(bubble.nodes(classes(data_json))
    // ...
)} ;
Mais l'exécution en locale ne fonctionne pas toujours et c'est difficile à débugger. Les données json peuvent quasiment copiées telles quels dans un fichier javascript (data_json_local.js):
var data_json = [
  // .... données json
  ] ;
Ensuite, il suffit d'inclure ce fichier dans la page HTML en insérant la ligne :
<script src="data_json_local.js"></script>
Et pour finir, on modifie le premier javascript en commentant l'appel à la fonction d3.json qui n'est plus utile. La variable data_json existe déjà :
//d3.json("mes_donnees.json", function(data_json) {


    // ...
    var node = svg.selectAll(".node")
            .data(bubble.nodes(classes(data_json))
    // ...
//)} ;

Le script qui a généré le graphe ci-dessus :

<div id="d3jsexample"></div>
<script src="http://d3js.org/d3.v3.js"></script>
<script src="td9_by_hours_data.js"></script>   <!-- the data, contains the variable data  used by the following script, it looks like the following -->
<!--
data = 
[
{'velo': 12817.0, 'last_update': '10/9/13 11:35', 'minute': 35.0, 'heure': 11.0, 'place': 25202.0, 'name': '10/9/13 11:35', 'roulant': 225.0, 'somme': 38019.0}
,{'velo': 12744.0, 'last_update': '10/9/13 11:40', 'minute': 40.0, 'heure': 11.0, 'place': 25273.0, 'name': '10/9/13 11:40', 'roulant': 227.0, 'somme': 38017.0}
...
] ;
-->
<script>

// defines the graph area (usually the same for for every graph)
margin = {
    top: 20,
    right: 20,
    bottom: 20,
    left: 45
};

width = 600 - margin.left - margin.right;
height = 300 - margin.top - margin.bottom;

// defines the range of each axis
var x = d3.time.scale()
    .domain(d3.extent(data, function (d) {
    return d.last_update;
}))
    .range([0, width]);

var y = d3.scale.linear()
    .domain(d3.extent(data, function (d) {
    return d.velo;
}))
    .range([height, 0]);

// graph type, also defines the columns to be used (last_update and velo in this case)
var line = d3.svg.line()
    .x(function (d) {
    return x(d.last_update);
})
    .y(function (d) {
    return y(d.velo);
});

// defines the function to call when zooming
var zoom = d3.behavior.zoom()
    .x(x)
    //.y(y)
    .on("zoom", zoomed);

// creates a svg section in the d3jsex section
svg = d3.select('#d3jsexample')
    .append("svg")
    .attr('width', width + margin.left + margin.right)
    .attr('height', height + margin.top + margin.bottom)
    .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
    .call(zoom);

svg.append("rect")
    .attr("width", width)
    .attr("height", height)
    .attr("class", "plot");

// two functions uses for the zoom
var make_x_axis = function () {
    return d3.svg.axis()
        .scale(x)
        .orient("bottom")
        .ticks(10);
};

var make_y_axis = function () {
    return d3.svg.axis()
        .scale(y)
        .orient("left")
        .ticks(10);
};

// defines the axis
var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom")
    .ticks(10);

svg.append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0, " + height + ")")
    .call(xAxis);

var yAxis = d3.svg.axis()
    .scale(y)
    .orient("left")
    .ticks(10);

svg.append("g")
    .attr("class", "y axis")
    .call(yAxis)
    .append("text")
      .attr("class", "label")
      .attr("transform", "rotate(-90)")
      .attr("y", 6)
      .attr("dy", ".71em")
      .style("text-anchor", "end")
      .text("velib disponibles");
      
svg.append("g")
    .attr("class", "x grid")
    .attr("transform", "translate(0," + height + ")")
    .call(make_x_axis()
    .tickSize(-height, 0, 0)
    .tickFormat(""));

svg.append("g")
    .attr("class", "y grid")
    .call(make_y_axis()
    .tickSize(-width, 0, 0)
    .tickFormat(""));

// objects for the zooming
var clip = svg.append("clipPath")
    .attr("id", "clip")
    .append("rect")
    .attr("x", 0)
    .attr("y", 0)
    .attr("width", width)
    .attr("height", height);

var chartBody = svg.append("g")
    .attr("clip-path", "url(#clip)");

chartBody.append("path")
    .datum(data)
    .attr("class", "line")
    .attr("d", line);

// zooming functions
function zoomed() {
    //console.log(d3.event.translate);     // display information in the logging console of the browser (using developping tools)
    //console.log(d3.event.scale);
    svg.select(".x.axis").call(xAxis);
    svg.select(".y.axis").call(yAxis);
    svg.select(".x.grid")
        .call(make_x_axis()
        .tickSize(-height, 0, 0)
        .tickFormat(""));
    svg.select(".y.grid")
        .call(make_y_axis()
        .tickSize(-width, 0, 0)
        .tickFormat(""));
    svg.select(".line")
        .attr("class", "line")
        .attr("d", line);
}

</script>
Et la feuille de style :
.plot {
    fill: rgba(250, 250, 255, 0.6);
}
.grid .tick {
    stroke: lightgrey;
    opacity: 0.7;
}
.grid path {
    stroke-width: 0;
}
.axis path, .axis line {
    fill: none;
    stroke: #000;
    shape-rendering: crispEdges;
}
.x.axis path {
    display: none;
}
.line {
    fill: none;
    stroke: steelblue;
    stroke-width: 1.5px;
}


<-- -->

Xavier Dupré