Migrate d3js to v4 and correct d3js visualisations i.e. bug 3.20. Breadcrumb navigation for the language treemap has been improved
authorymh <ymh.work@gmail.com>
Fri, 09 Dec 2016 11:41:15 +0100
changeset 467 762fc0eb4946
parent 466 a8effb60ccb6
child 468 8fe093d88efe
Migrate d3js to v4 and correct d3js visualisations i.e. bug 3.20. Breadcrumb navigation for the language treemap has been improved
build/build.sh
build/build_puppet.sh
cms/app-client/app/adapters/application.js
cms/app-client/app/components/discourses-component.js
cms/app-client/app/components/visu-langues.js
cms/app-client/app/helpers/annotation-content.js
cms/app-client/app/models/language.js
cms/app-client/app/models/transcript.js
cms/app-client/app/routes/tabs/langues.js
cms/app-client/app/serializers/language.js
cms/app-client/app/services/utils.js
cms/app-client/app/styles/app.scss
cms/app-client/app/styles/tabs/langues.scss
cms/app-client/app/templates/tabs/langues.hbs
cms/app-client/ember-cli-build.js
cms/app-client/mirage/config.js
cms/app-client/package.json
cms/app-client/tests/unit/models/language-test.js
cms/app-client/tests/unit/serializers/language-test.js
--- a/build/build.sh	Sun Dec 04 13:49:44 2016 +0100
+++ b/build/build.sh	Fri Dec 09 11:41:15 2016 +0100
@@ -16,34 +16,34 @@
 }
 
 function install() {
-    pushd "$DIR"
+    pushd "$DIR" > /dev/null
 
     echoblue "---> preparing bo client"
-    pushd ../server/bo_client
+    pushd ../server/bo_client > /dev/null
     /usr/local/bin/npm install
     ./node_modules/.bin/bower install
-    popd
+    popd > /dev/null
     echoblue "---> preparing bo client done"
 
     echoblue "---> preparing back"
-    pushd ../server/src
+    pushd ../server/src > /dev/null
     php composer.phar install
     /usr/local/bin/npm install
     ./node_modules/.bin/bower install
-    popd
+    popd > /dev/null
     echoblue "---> preparing back done"
 
     echoblue "---> preparing app-client"
-    pushd ../cms/app-client
+    pushd ../cms/app-client > /dev/null
     /usr/local/bin/npm install
     ./node_modules/.bin/bower install
-    popd
+    popd > /dev/null
     echoblue "---> preparing app-client done"
 
     echoblue "---> preparing module"
-    pushd ../cms
+    pushd ../cms > /dev/null
     npm install
-    popd
+    popd > /dev/null
     echoblue "---> preparing module done"
 
 
@@ -111,7 +111,7 @@
 echo "do_install: $do_install"
 [[ "$do_install" == true ]] && echoblue "DO INSTALL" && install;
 
-pushd "$DIR"
+pushd "$DIR" > /dev/null
 
 echoblue "---> cleaning build folder"
 rm -fr root
@@ -121,28 +121,28 @@
 mkdir -p root/var/www/corpusdelaparole/drupal/sites/all/modules
 
 echoblue "---> building back"
-pushd ../server/src
+pushd ../server/src > /dev/null
 version=$(sed -n "s/[[:space:]]*\'version\'[[:space:]]*=>[[:space:]]*\'\([\.0-9]*\)\'/\1/p" config/version.php)
 version=${version:-0.0.0}
 npm install
 ./node_modules/.bin/bower install
 ./node_modules/.bin/gulp copy-build ${build_option_back}
-popd
+popd > /dev/null
 echoblue "---> building back done"
 
 echoblue "---> building app-client"
-pushd ../cms/app-client
+pushd ../cms/app-client > /dev/null
 npm install
 ./node_modules/.bin/bower install
 ./node_modules/.bin/ember build ${build_option}
-popd
+popd > /dev/null
 echoblue "---> building app-client done"
 
 echoblue "---> building module"
-pushd ../cms
+pushd ../cms > /dev/null
 npm install
 ./node_modules/.bin/gulp copy-build ${build_option} --version="$version"
-popd
+popd > /dev/null
 
 echoblue "---> building package"
 vagrant ssh -c "/vagrant/build_rpm.sh"
@@ -156,5 +156,22 @@
 
 popd > /dev/null
 
+echoblue "--> archiving dist"
+
+pushd "$DIR/dist" > /dev/null
+
+rm -f corpusdelaparole-back_*_*.tar.gz
+ARCHIVE_NAME="corpusdelaparole-back_$(date +%Y-%m-%d)_${version}"
+
+mkdir "$ARCHIVE_NAME"
+
+cp bootstrap-puppet.sh corpusdelaparole-$version-*.noarch.rpm installDrupal.sh puppet-corpusdelaparole-$version-*.noarch.rpm "$ARCHIVE_NAME"
+
+tar zcvf "${ARCHIVE_NAME}.tar.gz" "$ARCHIVE_NAME"
+
+rm -fr "$ARCHIVE_NAME"
+
+popd > /dev/null
+
 echoblue "---> done"
 
--- a/build/build_puppet.sh	Sun Dec 04 13:49:44 2016 +0100
+++ b/build/build_puppet.sh	Fri Dec 09 11:41:15 2016 +0100
@@ -98,5 +98,28 @@
 
 popd > /dev/null
 
+echoblue "--> archiving dist"
+
+pushd ../server/src > /dev/null
+version=$(sed -n "s/[[:space:]]*\'version\'[[:space:]]*=>[[:space:]]*\'\([\.0-9]*\)\'/\1/p" config/version.php)
+version=${version:-0.0.0}
+popd > /dev/null
+
+pushd "$DIR/dist" > /dev/null
+
+rm -f corpusdelaparole-back_*_*.tar.gz
+ARCHIVE_NAME="corpusdelaparole-back_$(date +%Y-%m-%d)_${version}"
+
+mkdir "$ARCHIVE_NAME"
+
+cp bootstrap-puppet.sh corpusdelaparole-$version-*.noarch.rpm installDrupal.sh puppet-corpusdelaparole-$version-*.noarch.rpm "$ARCHIVE_NAME"
+
+tar zcvf "${ARCHIVE_NAME}.tar.gz" "$ARCHIVE_NAME"
+
+rm -fr "$ARCHIVE_NAME"
+
+popd > /dev/null
+
+
 echoblue "---> done"
 
--- a/cms/app-client/app/adapters/application.js	Sun Dec 04 13:49:44 2016 +0100
+++ b/cms/app-client/app/adapters/application.js	Fri Dec 09 11:41:15 2016 +0100
@@ -7,7 +7,8 @@
     datestat: 'stats/datestats',
     dateminmax: 'stats/dateminmax',
     theme: 'stats/themes',
-    discourse: 'stats/discourses'
+    discourse: 'stats/discourses',
+    language: 'stats/languages'
 };
 
 export default RESTAdapter.extend({
--- a/cms/app-client/app/components/discourses-component.js	Sun Dec 04 13:49:44 2016 +0100
+++ b/cms/app-client/app/components/discourses-component.js	Fri Dec 09 11:41:15 2016 +0100
@@ -1,13 +1,18 @@
 import Ember from 'ember';
-import d3 from 'd3';
+import * as d3 from 'd3-selection';
+import * as d3h from 'd3-hierarchy';
+import * as d3s from 'd3-scale';
 import _ from 'lodash/lodash';
 
+const MINIMUM_CIRCLE_WIDTH = 60.0;
+
 export default Ember.Component.extend({
 
     classNames: ['discourses-component'],
 
     constants: Ember.inject.service(),
     filter: Ember.inject.service(),
+    utils: Ember.inject.service(),
 
     discourseObserver: Ember.observer('filter.discourse', function() {
         Ember.$('.item').removeClass("selected");
@@ -25,22 +30,36 @@
 
         var discourses = this.get('discourses');
         var array = discourses.map(function (d) { return d.get('count'); });
-        var oldMin = Math.min(...array),
-            oldMax = Math.max(...array);
-        var sum = array.reduce(function(a, b) { return a + b; });
-        var average = sum / array.length;
-        var newMin = Math.floor((average - oldMin)),
-            newMax = Math.floor((oldMax - average));
 
         var width = self.$().parent().width();
         var height = self.$().parent().height() - self.$().siblings().outerHeight(true);
 
-        var bubble = d3.layout.pack()
-            .sort(function comparator(a, b) { return a.value + b.value; })
+        //Determine the minimum circle width
+        var longerStr = _.max(
+          [].concat(...(discourses.map(function(d) { return d.get('label').split(' ');}))),
+          function(s) { return s.length; }
+        );
+        var w = this.get('utils').getWidthOfText(longerStr, '11px');
+        // we try to take into account the fact that there is at least 2 lines
+        var minimum_circle_width = Math.max( Math.sqrt((w*w)+Math.pow(11+11/2, 2)) + 10, MINIMUM_CIRCLE_WIDTH);
+
+        // to avoid division by zero. In any case it makes no sense to consider dimensions
+        // under MINIMUM_CIRCLE_WIDTH
+        var scaleFactor = minimum_circle_width/Math.max(minimum_circle_width, Math.min(width, height));
+
+        var min = Math.min(...array),
+            max = Math.max(...array);
+
+        var scale = d3s.scaleLinear();
+        // The range is the range for font sizes
+        var fontScale = d3s.scaleQuantize().domain([min, max]).range(_.range(10, 14));
+
+        if((min/max) < scaleFactor) {
+          scale = scale.domain([min, max]).range([scaleFactor, 1]);
+        }
+
+        var bubble = d3h.pack()
             .size([width, height])
-            .value(function(d){
-                return Math.floor((((d.value - oldMin) * (newMax - newMin)) / (oldMax - oldMin)) + newMin);
-            })
             .padding(10);
 
         var element = d3.select('#' + self.get('elementId'))
@@ -49,8 +68,10 @@
           .style('position','absolute')
           .style('left', -100000);
 
-        var bubbles = bubble.nodes(self.createNodes());
+        var root = d3h.hierarchy(self.createNodes())
+          .sum(function(d) {return scale(d.value);});
 
+        var bubbles = bubble(root).descendants();
         var nodes = element
             .selectAll()
             .data(bubbles);
@@ -59,20 +80,20 @@
             .attr("class", function(d) { return ( d.children ? "category": "item" ) + ( (self.get('filter').get('discourse') !== null && _.contains(self.get('filter').get('discourse'), d.id)) ? " selected" : "" ) ; });
 
         var item = element.selectAll(".item")
-            .attr("data-id", function(d) { return d.id; })
+            .attr("data-id", function(d) { return d.data.id; })
             .style("left", function(d) { return ( d.x - d.r)  + "px"; })
             .style("top", function(d) { return ( d.y - d.r)  + "px"; })
             .style("width", function(d) { return (d.r * 2) + "px"; })
             .style("height", function(d) { return (d.r * 2) + "px"; })
-            .style("background-color", function(d) { return d.fill; })
-            .style("border-color", function(d) { return d.stroke; })
-            .style("font-size", function(d) { return Math.floor((((d.value - oldMin) * (13 - 10)) / (oldMax - oldMin)) + 10) + 'px'; })
+            .style("background-color", function(d) { return d.data.fill; })
+            .style("border-color", function(d) { return d.data.stroke; })
+            .style("font-size", function(d) { return fontScale(d.data.count) + 'px'; })
             .on('click', function(d) {
-                self.get('filter').setFilter('discourse', d.id);
+                self.get('filter').setFilter('discourse', d.data.id);
             });
 
         item.append("span")
-            .html(function(d) { return d.name + ' <span class="count">(' + d.count + ')</span>'; })
+            .html(function(d) { return d.data.name + ' <span class="count">(' + d.data.count + ')</span>'; })
             .style("margin-left", function() { return ( Ember.$(this).width() > Ember.$(this).parent().width() ? - ( Ember.$(this).width() / 2 ) + ( Ember.$(this).parent().width() / 2 ) : 0 ) + 'px'; })
             .style("margin-top", function() { return Ember.$(this).parent().height() / 2 - Ember.$(this).height() / 2 + 'px'; });
 
--- a/cms/app-client/app/components/visu-langues.js	Sun Dec 04 13:49:44 2016 +0100
+++ b/cms/app-client/app/components/visu-langues.js	Fri Dec 09 11:41:15 2016 +0100
@@ -1,221 +1,237 @@
 import Ember from 'ember';
-import d3 from 'd3';
-import ENV from 'app-client/config/environment';
+import * as d3 from  'd3-selection';
+import * as d3h from 'd3-hierarchy';
+import * as d3s from 'd3-scale';
+import * as d3t from 'd3-transition';
+import * as d3e from 'd3-ease';
 import _ from 'lodash/lodash';
-import URI from 'urijs';
 
+// inspired by http://bl.ocks.org/ganeshv/6a8e9ada3ab7f2d88022
+// and https://bost.ocks.org/mike/treemap/
 export default Ember.Component.extend({
 
-    constants: Ember.inject.service(),
-    filter: Ember.inject.service(),
-    colors: Ember.inject.service(),
+  constants: Ember.inject.service(),
+  filter: Ember.inject.service(),
+  colors: Ember.inject.service(),
+  utils: Ember.inject.service(),
 
-    scale: Ember.computed('maxCount', 'minCount', function() {
-      let maxCount = this.get('maxCount');
-      let minCount = this.get('minCount');
-      return this.get('colors').getScaleLinear(minCount, maxCount);
-    }),
-    maxCount: 0,
-    minCount: 0,
-
-    filterObserver: Ember.observer('filter.language', function() {
-        Ember.$('.node').removeClass("selected");
-        Ember.$('.node[data-id="' + this.get('filter').get('language') + '"]').addClass("selected");
-    }),
+  scale: Ember.computed('maxCount', 'minCount', function () {
+    let maxCount = this.get('maxCount');
+    let minCount = this.get('minCount');
+    return this.get('colors').getScaleLinear(minCount, maxCount);
+  }),
+  maxCount: 0,
+  minCount: 0,
 
-    didInsertElement: function(){
-        var self = this;
-        var baseurl = (ENV.APP.backRootURL || ENV.rootURL).replace(/\/$/,"")+'/api/v1';
-        var url = URI(baseurl+"/stats/languages").search(this.get('filter').get('queryParamsValuesURI'));
+  firstRender: true,
+
+  filterLanguageObserver: Ember.observer('filter.language', function () {
+    Ember.$('.node').removeClass("selected");
+    Ember.$('.node[data-id="' + this.get('filter').get('language') + '"]').addClass("selected");
+  }),
 
-        d3.json(url.href(), function(data) {
-            var margin = { top: 30, right: 0, bottom: 0, left: 0 };
-            var width = Ember.$('#' + self.get('elementId')).width();
-            var height = Ember.$('#' + self.get('elementId')).height() - margin.top - margin.bottom;
+  filterOtherModified: false,
 
-            var languages = data['languages'];
-            var array = Object.keys(languages).map(function (key) { return languages[key]; });
-            var oldMin = Math.min(...array),
-                oldMax = Math.max(...array);
-            var sum = array.reduce(function(a, b) { return a + b; });
-            var average = sum / array.length;
-            var newMin = Math.floor((average - oldMin)),
-                newMax = Math.floor((oldMax - average));
+  filterOtherObserver: Ember.observer('filter.date.[]', 'filter.discourse.[]', 'filter.theme.[]', 'filter.location', function() {
+    this.set('filterOtherModified', true);
+    this.set('firstRender', true);
+  }),
 
 
-            var x = d3.scale.linear()
-                .domain([0, width])
-                .range([0, width]),
-                y = d3.scale.linear()
-                .domain([0, height])
-                .range([0, height]);
+  didRender: function () {
 
-            var treemap = d3.layout.treemap()
-                .children(function(d, depth) { return depth ? null : d._children; })
-                .sort(function(a, b) { return a.value - b.value; })
-                .value(function(d){
-                    return Math.floor((((d.value - oldMin) * (newMax - newMin)) / (oldMax - oldMin)) + newMin);
-                })
-                .round(false);
+    if(!this.get('filterOtherModified') && !this.get('firstRender')) {
+      return;
+    }
+    this.set('firstRender', false);
+    this.set('filterOtherModified', false);
+    var self = this;
 
-            var element = d3.select('#' + self.get('elementId'))
-                .style("width", width + margin.left + margin.right + 'px')
-                .style("height", height + margin.bottom + margin.top + 'px')
-                .style("margin-left", -margin.left + "px")
-                .style("margin-right", -margin.right + "px")
-                .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
-                .style("shape-rendering", "crispEdges");
+    var margin = { top: 30, right: 0, bottom: 0, left: 0 };
+    var width = Ember.$('#' + this.get('elementId')).width();
+    var height = Ember.$('#' + this.get('elementId')).height() - margin.top - margin.bottom;
+    var ratio = (1+Math.sqrt(5))/2;
 
-            var breadcrumbs = element.insert("div", ":first-child")
-                .attr("class", "breadcrumbs")
-                .attr("y", -margin.top)
-                .style("width", width + 'px')
-                .style("height", margin.top + 'px');
+    var languages = this.get('languages');
+    var languagesMap = languages.reduce(function (res, l) { res[l.get('id')] = l.get('count'); return res; }, {});
 
-            var root = _.cloneDeep(self.constants.LANGUAGES_TREEMAP);
-            root.x = root.y = 0;
-            root.dx = width;
-            root.dy = height;
-            root.depth = 0;
-            var transitioning = false;
-
-            accumulate(root);
-            layout(root);
-            display(root);
+    var treemap = d3h.treemap()
+      .size([width/ratio, height])
+      .tile(d3h.treemapSquarify.ratio(1));
 
-            function accumulate(d) {
-                d._children = d.children;
-                if(d.children) {
-                    d.value = d.children.reduce(function(p, v) { return p + accumulate(v); }, 0);
-                } else if (d.values) {
-                    d.value = d.values.reduce(function(p, v) { return p + (languages[v] ? languages[v] : 0); }, 0);
-                } else {
-                    d.value = languages[d.id] ? languages[d.id] : 0;
-                }
-                return d.value;
-            }
+    var element = d3.select('#' + this.get('elementId'))
+      .style("width", width + margin.left + margin.right + 'px')
+      .style("height", height + margin.bottom + margin.top + 'px')
+      .style("margin-left", -margin.left + "px")
+      .style("margin-right", -margin.right + "px");
+
+    var breadcrumbs = element.select("div.breadcrumbs");
 
-            function layout(d) {
-                if (d._children) {
-                    treemap.nodes({_children: d._children});
-                    d._children.forEach(function(c) {
-                        function getCount(node, count = 0) {
-                            var c = languages[node.id];
-                            if(typeof c === 'undefined') {
-                              if(node._children) {
-                                  node._children.forEach(function(child) {
-                                      count = getCount(child, count);
-                                  });
-                              } else if(node.values) {
-                                  count = node.values.reduce(function(p, v) { return p + (languages[v] ? languages[v] : 0); }, count);
-                              }
-                              return count;
-                            } else {
-                                return count + c;
-                            }
-                        }
-                        c.count = getCount(c);
-                        c.x = d.x + c.x * d.dx;
-                        c.y = d.y + c.y * d.dy;
-                        c.dx *= d.dx;
-                        c.dy *= d.dy;
-                        c.parent = d;
-                        layout(c);
-                    });
-                }
-            }
+    if(breadcrumbs.empty()) {
+      breadcrumbs = element.insert("div", ":first-child")
+        .attr("class", "breadcrumbs")
+        .attr("y", -margin.top)
+        .style("width", width + 'px')
+        .style("height", margin.top + 'px');
+    }
+
+    var root = _.cloneDeep(this.constants.LANGUAGES_TREEMAP);
+
+    function ancestors(d) {
+      let ancestors = [];
+      let currentNode = d;
+      while (currentNode.parent) {
+        ancestors.unshift(currentNode = currentNode.parent);
+      }
+      return ancestors;
+    }
 
-            function position() {
-                return this.style("width", function(d) { return x(d.x + d.dx) - x(d.x) + 'px'; })
-                    .style("height", function(d) { return y(d.y + d.dy) - y(d.y) + 'px'; })
-                    .style("left", function(d) { return x(d.x) + 'px'; })
-                    .style("top", function(d) { return y(d.y) + 'px'; });
-            }
-
-            function display(d) {
-                breadcrumbs
-                    .datum(d.parent)
-                    .html(name(d))
-                    .on("click", transition);
-
-                var nodes = element.append("div")
-                    .attr("class", "nodes")
-                    .datum(d);
+    // This function set the count and parent attributes
+    function decorateData(d) {
+      if (d.children) {
+        d.count = d.children.reduce(function (p, v) { v.parent = d; return p + decorateData(v); }, 0);
+      } else if (d.values) {
+        d.count = d.values.reduce(function (p, v) { return p + (languagesMap[v] ? languagesMap[v] : 0); }, 0);
+      } else {
+        d.count = languagesMap[d.id] ? languagesMap[d.id] : 0;
+      }
+      return d.count;
+    }
+    decorateData(root);
 
-                var node = nodes.selectAll()
-                    .data(d._children)
-                    .enter()
-                    .append("div")
-                    .attr("data-id", function(d) { return d.id; });
-
-                var dMin = Math.min.apply(null, d._children.map(function(d){ return d.count; }));
-                var dMax = Math.max.apply(null, d._children.map(function(d){ return d.count; }));
-                self.setProperties({minCount: dMin, maxCount: dMax});
-                var scale = self.get('scale');
-                var backgroundColor = function(_d) { return scale(_d.count);};
-                var colorClass = function(_d) { return (self.get('colors').getPerceptiveLuminance(backgroundColor(_d)) >= 0.5)?'light-color':'dark-color'; };
-
-                node.attr("class", function(_d) { return "node " + colorClass(_d) + ( _d.id === self.get('filter').get('language') ? " selected" : "" ); })
-                    .call(position)
-                    .style("background-color", backgroundColor)
-                    .on("click", selectHandler);
-
-                node.filter(function(d) { return d._children; })
-                    .classed("children", true)
-                    .on("click", transition)
-                    .append("i")
-                    .attr("class", "fa fa-folder-o");
-
-                node.append("span")
-                    .html(function(d) { return d.name + ' <span class="count">(' + d.count + ')</span>'; });
+    // Clean tree with empty nodes
+    function cleanTree(n) {
+      if(n.children) {
+        n.children = n.children.filter(function(c) { return c.count !== 0;});
+        n.children.forEach(function(c) {
+          cleanTree(c);
+        });
+      }
+    }
+    cleanTree(root);
 
-                function transition(d) {
-                    if (transitioning || !d) {
-                        return;
-                    }
+    var transitioning = false;
 
-                    selectHandler(d);
-                    transitioning = true;
-
-                    var newNode = display(d),
-                    transitionNodes = node.transition().duration(750),
-                    transitionNewNodes = newNode.transition().duration(750);
+    display(root, 1);
 
-                    x.domain([d.x, d.x + d.dx]);
-                    y.domain([d.y, d.y + d.dy]);
-
-                    element.style("shape-rendering", null);
+    function transition(d, node, nodeWrapper) {
+      if (transitioning || !d) {
+        return;
+      }
 
-                    element.selectAll(".node").sort(function(a, b) { return a.depth - b.depth; });
-
-                    newNode.selectAll().style("fill-opacity", 0);
-
-                    transitionNodes.style("opacity", 0)
-                        .call(position);
+      selectHandler(d);
+      transitioning = true;
 
-                    transitionNewNodes.style("opacity", 1)
-                        .call(position);
-
-                    transitionNodes.remove().each("end", function() {
-                        element.style("shape-rendering", "crispEdges");
-                        transitioning = false;
-                    });
-                }
+      var t = d3t.transition().duration(750).ease(d3e.easeLinear);
+      var newNode = display(d, 0);
+      var transitionNodes = node.transition(t);
+      var transitionNewNodes = newNode.transition(t);
 
-                function selectHandler (d){
-                    if (d.id){
-                        self.get('filter').setFilter('language', d.id);
-                    }
-                }
+      newNode.style("fill-opacity", 0);
+      transitionNodes.style("opacity", 0).remove().on("end", function () {
+        nodeWrapper.remove();
+        transitioning = false;
+      });
+      transitionNewNodes.style("opacity", 1);
 
-                return node;
-            }
-
-            function name(d) {
-                return d.parent ? name(d.parent) + '<span class="level">' + d.name + '</span>' : '<span class="root">' + d.name + '</span>';
-            }
-
-        });
     }
 
+    function selectHandler(d) {
+      if (d.id) {
+        self.get('filter').setFilter('language', d.id);
+      }
+    }
+
+    function display(rData, opacity) {
+
+      var countArray = rData.children.map(function (d) { return d.count; });
+      var dMin = Math.min.apply(null, countArray);
+      var dMax = Math.max.apply(null, countArray);
+      var globalMin = rData.children.reduce(function minRec(m, c) {
+        if(c.count < m) {
+          m = c.count;
+        }
+        if(c.children) {
+          m = c.children.reduce(minRec, m);
+        }
+        return m;
+      }, dMax);
+      var dataScale = d3s.scaleLinear().domain([globalMin, dMax]).range([globalMin, dMax]);
+
+      var nameArray = rData.children.reduce(function (res, d) {
+        res = res.concat(d.name.split(/[\s\-]/));
+        return res;
+      }, ["(nnnnn)"]).sort(function(a,b) { return b.length - a.length; });
+
+      var w = self.get('utils').getWidthOfText(nameArray[0], '11px');
+      var p = w*w/(width*height);
+
+      if(dMin/dMax < p) {
+       dataScale = dataScale.range([p*100,100]);
+      }
+
+      var rNode = d3h.hierarchy(rData)
+        .sum(function (d) { return (d.children) ? 0 : dataScale(d.count); })
+        .sort(function (a, b) { return b.value - a.value; });
+
+      self.setProperties({ minCount: dMin, maxCount: dMax });
+      var scale = self.get('scale');
+      var backgroundColor = function (_d) { return scale(_d.data.count); };
+      var colorClass = function (_d) { return (self.get('colors').getPerceptiveLuminance(backgroundColor(_d)) >= 0.5) ? 'light-color' : 'dark-color'; };
+
+      var nodeWrapper = element.append("div")
+        .attr("class", "nodes")
+        .datum(rNode);
+
+      var descendants = treemap(rNode).descendants().filter(function (c) { return c.depth === rNode.depth + 1; });
+
+      var node = nodeWrapper.selectAll()
+        .data(descendants)
+        .enter()
+        .append("div")
+        .attr("data-id", function (d) { return d.data.id; });
+
+
+      node.attr("class", function (_d) { return "node " + colorClass(_d) + (_d.id === self.get('filter').get('language') ? " selected" : ""); })
+        .style("width", function (d) { return (Math.round(d.x1 * ratio) - Math.round(d.x0 * ratio)) + 'px'; })
+        .style("height", function (d) { return (d.y1 - d.y0) + 'px'; })
+        .style("left", function (d) { return Math.round(d.x0*ratio) + 'px'; })
+        .style("top", function (d) { return d.y0 + 'px'; })
+        .style("background-color", backgroundColor)
+        .style('opacity', opacity)
+        .attr('title', function (d) { return d.data.name + ' (' + d.data.count + ')'; })
+        .on("click", function (d) { selectHandler(d.data); });
+
+      node.filter(function (d) { return d.children; })
+        .classed("children", true)
+        .on("click", function (d) { transition(d.data, node, nodeWrapper); })
+        .append("i")
+        .attr("class", "fa fa-folder-o");
+
+      node.append("span")
+        .html(function (d) { return d.data.name + ' <span class="count">(' + d.data.count + ')</span>'; });
+
+      breadcrumbs
+        .selectAll("span")
+        .remove();
+      let i = 0;
+      ancestors(rNode.data).forEach(function (a) {
+        breadcrumbs
+          .append("span")
+          .attr('class', (i++) ? "level" : "root")
+          .html(a.name)
+          .datum(a)
+          .on("click", function (d) { transition(d, node, nodeWrapper); });
+      });
+      breadcrumbs
+        .append("span")
+        .attr('class', (i) ? "level" : "root")
+        .html(rNode.data.name)
+        .datum(rNode.data);
+
+      return node;
+    }
+
+  }
+
 });
--- a/cms/app-client/app/helpers/annotation-content.js	Sun Dec 04 13:49:44 2016 +0100
+++ b/cms/app-client/app/helpers/annotation-content.js	Fri Dec 09 11:41:15 2016 +0100
@@ -1,7 +1,7 @@
 import Ember from 'ember';
 
 export function annotationContent(params/*, hash*/) {
-  if(!params || params.length==0) {
+  if(!params || (params.length === 0)) {
     return "";
   }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cms/app-client/app/models/language.js	Fri Dec 09 11:41:15 2016 +0100
@@ -0,0 +1,5 @@
+import DS from 'ember-data';
+
+export default DS.Model.extend({
+  count: DS.attr('number')
+});
--- a/cms/app-client/app/models/transcript.js	Sun Dec 04 13:49:44 2016 +0100
+++ b/cms/app-client/app/models/transcript.js	Fri Dec 09 11:41:15 2016 +0100
@@ -2,8 +2,8 @@
 
 export default DS.Model.extend({
 
-	title: DS.attr(),
-	annotations: DS.attr(),
-	sections: DS.attr(),
-  
+  title: DS.attr(),
+  annotations: DS.attr(),
+  sections: DS.attr(),
+
 });
--- a/cms/app-client/app/routes/tabs/langues.js	Sun Dec 04 13:49:44 2016 +0100
+++ b/cms/app-client/app/routes/tabs/langues.js	Fri Dec 09 11:41:15 2016 +0100
@@ -2,10 +2,15 @@
 
 export default Ember.Route.extend({
 
-    player: Ember.inject.service(),
+  player: Ember.inject.service(),
+  filter: Ember.inject.service(),
 
-    activate: function() {
-        this.get('player').set('window', false);
-    }
+  model() {
+    return this.get('store').query('language', this.get('filter').get('queryParamsValues'));
+  },
+
+  activate() {
+    this.get('player').set('window', false);
+  }
 
 });
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cms/app-client/app/serializers/language.js	Fri Dec 09 11:41:15 2016 +0100
@@ -0,0 +1,22 @@
+import DS from 'ember-data';
+
+export default DS.JSONSerializer.extend({
+
+      normalizeResponse: function(store, primaryModelClass, payload) {
+        var data = [];
+        var languages = payload['languages'];
+        Object.keys(languages).forEach(function(key) {
+            data.push({
+                'id': key,
+                'type': 'language',
+                'attributes': {
+                    'count': languages[key]
+                }
+            });
+        });
+        return {
+            'data': data
+        };
+    }
+
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cms/app-client/app/services/utils.js	Fri Dec 09 11:41:15 2016 +0100
@@ -0,0 +1,27 @@
+
+import Ember from 'ember';
+
+export default Ember.Service.extend({
+
+  // inspired by http://stackoverflow.com/a/39089679
+  getWidthOfText(txt, fontsize){
+    // Create dummy span
+    var e = document.createElement('span');
+
+    // set the base font defined in app.scss
+    //e.className = 'base-font';
+    e.style.fontFamily = 'sans-serif';
+    // Set font-size
+    e.style.fontSize = fontsize;
+    // Set text
+    e.innerHTML = txt;
+    // Return width
+    e.style.visibility = 'hidden';
+
+    document.body.appendChild(e);
+    let w = e.offsetWidth;
+    document.body.removeChild(e);
+
+    return w;
+  }
+});
--- a/cms/app-client/app/styles/app.scss	Sun Dec 04 13:49:44 2016 +0100
+++ b/cms/app-client/app/styles/app.scss	Fri Dec 09 11:41:15 2016 +0100
@@ -36,8 +36,12 @@
     outline:0;
 }
 
+.base-font {
+  font-family: sans-serif;
+}
+
 #corpus-app {
-    font-family: sans-serif;
+    @extend .base-font;
     font-size: 12px;
     line-height: initial;
 }
--- a/cms/app-client/app/styles/tabs/langues.scss	Sun Dec 04 13:49:44 2016 +0100
+++ b/cms/app-client/app/styles/tabs/langues.scss	Fri Dec 09 11:41:15 2016 +0100
@@ -5,71 +5,70 @@
 
 #tabs-langues .breadcrumbs,
 #tabs-langues .node {
-	cursor: pointer;
+  cursor: pointer;
 }
 
 #tabs-langues .breadcrumbs:hover,
 #tabs-langues .node:hover,
 #tabs-langues .node.selected {
-	background-color: $corpus-blue!important;
-	color: $corpus-white!important;
+  background-color: $corpus-blue!important;
+  color: $corpus-white!important;
 }
 
 #tabs-langues .breadcrumbs {
-	background-color: $corpus-white;
+  background-color: $corpus-white;
     color: $corpus-black;
     position: relative;
-	line-height: 30px;
-	padding: 0 10px;
-	border-bottom: 1px solid $corpus-white;
-	box-sizing: border-box;
+  line-height: 30px;
+  padding: 0 10px;
+  border-bottom: 1px solid $corpus-white;
+  box-sizing: border-box;
 }
 
 #tabs-langues .breadcrumbs .root:only-child {
-	font-weight: bold;
+  font-weight: bold;
 }
 
 #tabs-langues .breadcrumbs .level::before {
-	content: '>';
-	margin: 0 10px;
-	font-weight: normal;
+  content: '>';
+  margin: 0 10px;
+  font-weight: normal;
 }
 
 #tabs-langues .breadcrumbs .level:last-child {
-	font-weight: bold;
+  font-weight: bold;
 }
 
 #tabs-langues .nodes {
-	width: inherit;
-	position: absolute;
+  width: inherit;
+  position: absolute;
 }
 
 #tabs-langues .node {
-	position: absolute;
-	margin: 0;
-	padding: 0;
-	border-left: 1px solid $corpus-white;
-	border-bottom: 1px solid $corpus-white;
-	padding: 10px;
-	box-sizing: border-box;
-	color: $corpus-white;
+  position: absolute;
+  margin: 0;
+  border-left: 1px solid $corpus-white;
+  border-bottom: 1px solid $corpus-white;
+  padding: 5px;
+  box-sizing: border-box;
+  color: $corpus-white;
 }
 
 #tabs-langues .node:hover {
-	border-right: none;
+  border-right: none;
 }
 
 #tabs-langues .node .fa {
-    margin-right: 5px;
+  margin-right: 5px;
 }
 
 #tabs-langues .node .fa::before {
-	font-size: 14px;
-	line-height: 12px;
+  font-size: 14px;
+  line-height: 12px;
 }
 
 #tabs-langues .node .count {
-	font-weight: bold;
+  font-weight: bold;
 }
 
 #tabs-langues .light-color {
--- a/cms/app-client/app/templates/tabs/langues.hbs	Sun Dec 04 13:49:44 2016 +0100
+++ b/cms/app-client/app/templates/tabs/langues.hbs	Fri Dec 09 11:41:15 2016 +0100
@@ -1,3 +1,3 @@
 <div id="tabs-langues">
-    {{visu-langues}}
+    {{visu-langues languages=model}}
 </div>
\ No newline at end of file
--- a/cms/app-client/ember-cli-build.js	Sun Dec 04 13:49:44 2016 +0100
+++ b/cms/app-client/ember-cli-build.js	Fri Dec 09 11:41:15 2016 +0100
@@ -2,6 +2,7 @@
 /* global require, module */
 var EmberApp = require('ember-cli/lib/broccoli/ember-app');
 
+
 module.exports = function(defaults) {
     var app = new EmberApp(defaults, {
         // Add options here
--- a/cms/app-client/mirage/config.js	Sun Dec 04 13:49:44 2016 +0100
+++ b/cms/app-client/mirage/config.js	Fri Dec 09 11:41:15 2016 +0100
@@ -31,34 +31,44 @@
         return transcripts.find(id).transcript;
     });
 
-    this.get('/stats/languages', 'languages');
+    this.get('/stats/languages', ({languages}, request) => {
+      let qParams = request.queryParams['discourse'];
+      if(qParams) {
+        var res = [];
+        let allLanguages = languages.all().models;
+        let i=0;
+        while(i<allLanguages.length && res.length < (allLanguages.length/Math.pow(2,qParams.length))) {
+          let d = allLanguages[i++];
+          d.count = Math.max(Math.floor(d.count / 2), 1);
+          res.push(d);
+        }
+
+        return new Collection('language', res);
+      } else {
+        return languages.all();
+      }
+
+    });
 
     this.get('/stats/geostats', 'geostats');
 
     this.get('/stats/themes', 'themes');
 
     this.get('/stats/discourses', ({discourses}, request) => {
-      console.log("DISCOURSES", request.queryParams, discourses.all(), discourses.all().models);
 
-      if(request.queryParams['discourse']) {
+      let qParams = request.queryParams['discourse'];
+      if(qParams) {
         var res = [];
-        request.queryParams.discourse.forEach( did => {
-          let d = discourses.find(did);
-          if(d) {
-            res.push(d);
-          }
-        });
         let allDiscourses = discourses.all().models;
         let i=0;
-        while(i<allDiscourses.length && res.length < (allDiscourses.length/2)) {
+        while(i<allDiscourses.length && res.length < (allDiscourses.length/Math.pow(2,qParams.length))) {
           let d = allDiscourses[i++];
           if(!_.contains(request.queryParams.discourse, d.id)) {
-            d.count = Math.floor(d.count / 2);
+            d.count = Math.max(Math.floor(d.count / 2), 1);
             res.push(d);
           }
         }
-        console.log("DISCOURSES", request.queryParams, { modelName: "discourse", models: res });
-        //return discourses.all();
+
         return new Collection('discourse', res);
       } else {
         return discourses.all();
--- a/cms/app-client/package.json	Sun Dec 04 13:49:44 2016 +0100
+++ b/cms/app-client/package.json	Fri Dec 09 11:41:15 2016 +0100
@@ -31,7 +31,6 @@
     "ember-cli": "2.10.0",
     "ember-cli-app-version": "^2.0.0",
     "ember-cli-babel": "^5.1.7",
-    "ember-cli-d3": "1.1.6",
     "ember-cli-dependency-checker": "^1.3.0",
     "ember-cli-htmlbars": "^1.0.10",
     "ember-cli-htmlbars-inline-precompile": "^0.3.3",
@@ -44,6 +43,7 @@
     "ember-cli-sri": "^2.1.0",
     "ember-cli-test-loader": "^1.1.0",
     "ember-cli-uglify": "^1.2.0",
+    "ember-d3": "0.3.0",
     "ember-data": "^2.10.0",
     "ember-data-fixture-adapter": "1.13.0",
     "ember-disable-proxy-controllers": "^1.0.1",
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cms/app-client/tests/unit/models/language-test.js	Fri Dec 09 11:41:15 2016 +0100
@@ -0,0 +1,12 @@
+import { moduleForModel, test } from 'ember-qunit';
+
+moduleForModel('language', 'Unit | Model | language', {
+  // Specify the other units that are required for this test.
+  needs: []
+});
+
+test('it exists', function(assert) {
+  let model = this.subject();
+  // let store = this.store();
+  assert.ok(!!model);
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cms/app-client/tests/unit/serializers/language-test.js	Fri Dec 09 11:41:15 2016 +0100
@@ -0,0 +1,15 @@
+import { moduleForModel, test } from 'ember-qunit';
+
+moduleForModel('language', 'Unit | Serializer | language', {
+  // Specify the other units that are required for this test.
+  needs: ['serializer:language']
+});
+
+// Replace this with your real tests.
+test('it serializes records', function(assert) {
+  let record = this.subject();
+
+  let serializedRecord = record.serialize();
+
+  assert.ok(serializedRecord);
+});