cms/app-client/app/components/visu-langues.js
changeset 467 762fc0eb4946
parent 424 feb0d3e0fef9
child 532 1190ea937f2d
--- 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;
+    }
+
+  }
+
 });