cms/app-client/app/components/visu-langues.js
changeset 467 762fc0eb4946
parent 424 feb0d3e0fef9
child 532 1190ea937f2d
equal deleted inserted replaced
466:a8effb60ccb6 467:762fc0eb4946
     1 import Ember from 'ember';
     1 import Ember from 'ember';
     2 import d3 from 'd3';
     2 import * as d3 from  'd3-selection';
     3 import ENV from 'app-client/config/environment';
     3 import * as d3h from 'd3-hierarchy';
       
     4 import * as d3s from 'd3-scale';
       
     5 import * as d3t from 'd3-transition';
       
     6 import * as d3e from 'd3-ease';
     4 import _ from 'lodash/lodash';
     7 import _ from 'lodash/lodash';
     5 import URI from 'urijs';
     8 
     6 
     9 // inspired by http://bl.ocks.org/ganeshv/6a8e9ada3ab7f2d88022
       
    10 // and https://bost.ocks.org/mike/treemap/
     7 export default Ember.Component.extend({
    11 export default Ember.Component.extend({
     8 
    12 
     9     constants: Ember.inject.service(),
    13   constants: Ember.inject.service(),
    10     filter: Ember.inject.service(),
    14   filter: Ember.inject.service(),
    11     colors: Ember.inject.service(),
    15   colors: Ember.inject.service(),
    12 
    16   utils: Ember.inject.service(),
    13     scale: Ember.computed('maxCount', 'minCount', function() {
    17 
    14       let maxCount = this.get('maxCount');
    18   scale: Ember.computed('maxCount', 'minCount', function () {
    15       let minCount = this.get('minCount');
    19     let maxCount = this.get('maxCount');
    16       return this.get('colors').getScaleLinear(minCount, maxCount);
    20     let minCount = this.get('minCount');
    17     }),
    21     return this.get('colors').getScaleLinear(minCount, maxCount);
    18     maxCount: 0,
    22   }),
    19     minCount: 0,
    23   maxCount: 0,
    20 
    24   minCount: 0,
    21     filterObserver: Ember.observer('filter.language', function() {
    25 
    22         Ember.$('.node').removeClass("selected");
    26   firstRender: true,
    23         Ember.$('.node[data-id="' + this.get('filter').get('language') + '"]').addClass("selected");
    27 
    24     }),
    28   filterLanguageObserver: Ember.observer('filter.language', function () {
    25 
    29     Ember.$('.node').removeClass("selected");
    26     didInsertElement: function(){
    30     Ember.$('.node[data-id="' + this.get('filter').get('language') + '"]').addClass("selected");
    27         var self = this;
    31   }),
    28         var baseurl = (ENV.APP.backRootURL || ENV.rootURL).replace(/\/$/,"")+'/api/v1';
    32 
    29         var url = URI(baseurl+"/stats/languages").search(this.get('filter').get('queryParamsValuesURI'));
    33   filterOtherModified: false,
    30 
    34 
    31         d3.json(url.href(), function(data) {
    35   filterOtherObserver: Ember.observer('filter.date.[]', 'filter.discourse.[]', 'filter.theme.[]', 'filter.location', function() {
    32             var margin = { top: 30, right: 0, bottom: 0, left: 0 };
    36     this.set('filterOtherModified', true);
    33             var width = Ember.$('#' + self.get('elementId')).width();
    37     this.set('firstRender', true);
    34             var height = Ember.$('#' + self.get('elementId')).height() - margin.top - margin.bottom;
    38   }),
    35 
    39 
    36             var languages = data['languages'];
    40 
    37             var array = Object.keys(languages).map(function (key) { return languages[key]; });
    41   didRender: function () {
    38             var oldMin = Math.min(...array),
    42 
    39                 oldMax = Math.max(...array);
    43     if(!this.get('filterOtherModified') && !this.get('firstRender')) {
    40             var sum = array.reduce(function(a, b) { return a + b; });
    44       return;
    41             var average = sum / array.length;
    45     }
    42             var newMin = Math.floor((average - oldMin)),
    46     this.set('firstRender', false);
    43                 newMax = Math.floor((oldMax - average));
    47     this.set('filterOtherModified', false);
    44 
    48     var self = this;
    45 
    49 
    46             var x = d3.scale.linear()
    50     var margin = { top: 30, right: 0, bottom: 0, left: 0 };
    47                 .domain([0, width])
    51     var width = Ember.$('#' + this.get('elementId')).width();
    48                 .range([0, width]),
    52     var height = Ember.$('#' + this.get('elementId')).height() - margin.top - margin.bottom;
    49                 y = d3.scale.linear()
    53     var ratio = (1+Math.sqrt(5))/2;
    50                 .domain([0, height])
    54 
    51                 .range([0, height]);
    55     var languages = this.get('languages');
    52 
    56     var languagesMap = languages.reduce(function (res, l) { res[l.get('id')] = l.get('count'); return res; }, {});
    53             var treemap = d3.layout.treemap()
    57 
    54                 .children(function(d, depth) { return depth ? null : d._children; })
    58     var treemap = d3h.treemap()
    55                 .sort(function(a, b) { return a.value - b.value; })
    59       .size([width/ratio, height])
    56                 .value(function(d){
    60       .tile(d3h.treemapSquarify.ratio(1));
    57                     return Math.floor((((d.value - oldMin) * (newMax - newMin)) / (oldMax - oldMin)) + newMin);
    61 
    58                 })
    62     var element = d3.select('#' + this.get('elementId'))
    59                 .round(false);
    63       .style("width", width + margin.left + margin.right + 'px')
    60 
    64       .style("height", height + margin.bottom + margin.top + 'px')
    61             var element = d3.select('#' + self.get('elementId'))
    65       .style("margin-left", -margin.left + "px")
    62                 .style("width", width + margin.left + margin.right + 'px')
    66       .style("margin-right", -margin.right + "px");
    63                 .style("height", height + margin.bottom + margin.top + 'px')
    67 
    64                 .style("margin-left", -margin.left + "px")
    68     var breadcrumbs = element.select("div.breadcrumbs");
    65                 .style("margin-right", -margin.right + "px")
    69 
    66                 .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
    70     if(breadcrumbs.empty()) {
    67                 .style("shape-rendering", "crispEdges");
    71       breadcrumbs = element.insert("div", ":first-child")
    68 
    72         .attr("class", "breadcrumbs")
    69             var breadcrumbs = element.insert("div", ":first-child")
    73         .attr("y", -margin.top)
    70                 .attr("class", "breadcrumbs")
    74         .style("width", width + 'px')
    71                 .attr("y", -margin.top)
    75         .style("height", margin.top + 'px');
    72                 .style("width", width + 'px')
    76     }
    73                 .style("height", margin.top + 'px');
    77 
    74 
    78     var root = _.cloneDeep(this.constants.LANGUAGES_TREEMAP);
    75             var root = _.cloneDeep(self.constants.LANGUAGES_TREEMAP);
    79 
    76             root.x = root.y = 0;
    80     function ancestors(d) {
    77             root.dx = width;
    81       let ancestors = [];
    78             root.dy = height;
    82       let currentNode = d;
    79             root.depth = 0;
    83       while (currentNode.parent) {
    80             var transitioning = false;
    84         ancestors.unshift(currentNode = currentNode.parent);
    81 
    85       }
    82             accumulate(root);
    86       return ancestors;
    83             layout(root);
    87     }
    84             display(root);
    88 
    85 
    89     // This function set the count and parent attributes
    86             function accumulate(d) {
    90     function decorateData(d) {
    87                 d._children = d.children;
    91       if (d.children) {
    88                 if(d.children) {
    92         d.count = d.children.reduce(function (p, v) { v.parent = d; return p + decorateData(v); }, 0);
    89                     d.value = d.children.reduce(function(p, v) { return p + accumulate(v); }, 0);
    93       } else if (d.values) {
    90                 } else if (d.values) {
    94         d.count = d.values.reduce(function (p, v) { return p + (languagesMap[v] ? languagesMap[v] : 0); }, 0);
    91                     d.value = d.values.reduce(function(p, v) { return p + (languages[v] ? languages[v] : 0); }, 0);
    95       } else {
    92                 } else {
    96         d.count = languagesMap[d.id] ? languagesMap[d.id] : 0;
    93                     d.value = languages[d.id] ? languages[d.id] : 0;
    97       }
    94                 }
    98       return d.count;
    95                 return d.value;
    99     }
    96             }
   100     decorateData(root);
    97 
   101 
    98             function layout(d) {
   102     // Clean tree with empty nodes
    99                 if (d._children) {
   103     function cleanTree(n) {
   100                     treemap.nodes({_children: d._children});
   104       if(n.children) {
   101                     d._children.forEach(function(c) {
   105         n.children = n.children.filter(function(c) { return c.count !== 0;});
   102                         function getCount(node, count = 0) {
   106         n.children.forEach(function(c) {
   103                             var c = languages[node.id];
   107           cleanTree(c);
   104                             if(typeof c === 'undefined') {
       
   105                               if(node._children) {
       
   106                                   node._children.forEach(function(child) {
       
   107                                       count = getCount(child, count);
       
   108                                   });
       
   109                               } else if(node.values) {
       
   110                                   count = node.values.reduce(function(p, v) { return p + (languages[v] ? languages[v] : 0); }, count);
       
   111                               }
       
   112                               return count;
       
   113                             } else {
       
   114                                 return count + c;
       
   115                             }
       
   116                         }
       
   117                         c.count = getCount(c);
       
   118                         c.x = d.x + c.x * d.dx;
       
   119                         c.y = d.y + c.y * d.dy;
       
   120                         c.dx *= d.dx;
       
   121                         c.dy *= d.dy;
       
   122                         c.parent = d;
       
   123                         layout(c);
       
   124                     });
       
   125                 }
       
   126             }
       
   127 
       
   128             function position() {
       
   129                 return this.style("width", function(d) { return x(d.x + d.dx) - x(d.x) + 'px'; })
       
   130                     .style("height", function(d) { return y(d.y + d.dy) - y(d.y) + 'px'; })
       
   131                     .style("left", function(d) { return x(d.x) + 'px'; })
       
   132                     .style("top", function(d) { return y(d.y) + 'px'; });
       
   133             }
       
   134 
       
   135             function display(d) {
       
   136                 breadcrumbs
       
   137                     .datum(d.parent)
       
   138                     .html(name(d))
       
   139                     .on("click", transition);
       
   140 
       
   141                 var nodes = element.append("div")
       
   142                     .attr("class", "nodes")
       
   143                     .datum(d);
       
   144 
       
   145                 var node = nodes.selectAll()
       
   146                     .data(d._children)
       
   147                     .enter()
       
   148                     .append("div")
       
   149                     .attr("data-id", function(d) { return d.id; });
       
   150 
       
   151                 var dMin = Math.min.apply(null, d._children.map(function(d){ return d.count; }));
       
   152                 var dMax = Math.max.apply(null, d._children.map(function(d){ return d.count; }));
       
   153                 self.setProperties({minCount: dMin, maxCount: dMax});
       
   154                 var scale = self.get('scale');
       
   155                 var backgroundColor = function(_d) { return scale(_d.count);};
       
   156                 var colorClass = function(_d) { return (self.get('colors').getPerceptiveLuminance(backgroundColor(_d)) >= 0.5)?'light-color':'dark-color'; };
       
   157 
       
   158                 node.attr("class", function(_d) { return "node " + colorClass(_d) + ( _d.id === self.get('filter').get('language') ? " selected" : "" ); })
       
   159                     .call(position)
       
   160                     .style("background-color", backgroundColor)
       
   161                     .on("click", selectHandler);
       
   162 
       
   163                 node.filter(function(d) { return d._children; })
       
   164                     .classed("children", true)
       
   165                     .on("click", transition)
       
   166                     .append("i")
       
   167                     .attr("class", "fa fa-folder-o");
       
   168 
       
   169                 node.append("span")
       
   170                     .html(function(d) { return d.name + ' <span class="count">(' + d.count + ')</span>'; });
       
   171 
       
   172                 function transition(d) {
       
   173                     if (transitioning || !d) {
       
   174                         return;
       
   175                     }
       
   176 
       
   177                     selectHandler(d);
       
   178                     transitioning = true;
       
   179 
       
   180                     var newNode = display(d),
       
   181                     transitionNodes = node.transition().duration(750),
       
   182                     transitionNewNodes = newNode.transition().duration(750);
       
   183 
       
   184                     x.domain([d.x, d.x + d.dx]);
       
   185                     y.domain([d.y, d.y + d.dy]);
       
   186 
       
   187                     element.style("shape-rendering", null);
       
   188 
       
   189                     element.selectAll(".node").sort(function(a, b) { return a.depth - b.depth; });
       
   190 
       
   191                     newNode.selectAll().style("fill-opacity", 0);
       
   192 
       
   193                     transitionNodes.style("opacity", 0)
       
   194                         .call(position);
       
   195 
       
   196                     transitionNewNodes.style("opacity", 1)
       
   197                         .call(position);
       
   198 
       
   199                     transitionNodes.remove().each("end", function() {
       
   200                         element.style("shape-rendering", "crispEdges");
       
   201                         transitioning = false;
       
   202                     });
       
   203                 }
       
   204 
       
   205                 function selectHandler (d){
       
   206                     if (d.id){
       
   207                         self.get('filter').setFilter('language', d.id);
       
   208                     }
       
   209                 }
       
   210 
       
   211                 return node;
       
   212             }
       
   213 
       
   214             function name(d) {
       
   215                 return d.parent ? name(d.parent) + '<span class="level">' + d.name + '</span>' : '<span class="root">' + d.name + '</span>';
       
   216             }
       
   217 
       
   218         });
   108         });
   219     }
   109       }
       
   110     }
       
   111     cleanTree(root);
       
   112 
       
   113     var transitioning = false;
       
   114 
       
   115     display(root, 1);
       
   116 
       
   117     function transition(d, node, nodeWrapper) {
       
   118       if (transitioning || !d) {
       
   119         return;
       
   120       }
       
   121 
       
   122       selectHandler(d);
       
   123       transitioning = true;
       
   124 
       
   125       var t = d3t.transition().duration(750).ease(d3e.easeLinear);
       
   126       var newNode = display(d, 0);
       
   127       var transitionNodes = node.transition(t);
       
   128       var transitionNewNodes = newNode.transition(t);
       
   129 
       
   130       newNode.style("fill-opacity", 0);
       
   131       transitionNodes.style("opacity", 0).remove().on("end", function () {
       
   132         nodeWrapper.remove();
       
   133         transitioning = false;
       
   134       });
       
   135       transitionNewNodes.style("opacity", 1);
       
   136 
       
   137     }
       
   138 
       
   139     function selectHandler(d) {
       
   140       if (d.id) {
       
   141         self.get('filter').setFilter('language', d.id);
       
   142       }
       
   143     }
       
   144 
       
   145     function display(rData, opacity) {
       
   146 
       
   147       var countArray = rData.children.map(function (d) { return d.count; });
       
   148       var dMin = Math.min.apply(null, countArray);
       
   149       var dMax = Math.max.apply(null, countArray);
       
   150       var globalMin = rData.children.reduce(function minRec(m, c) {
       
   151         if(c.count < m) {
       
   152           m = c.count;
       
   153         }
       
   154         if(c.children) {
       
   155           m = c.children.reduce(minRec, m);
       
   156         }
       
   157         return m;
       
   158       }, dMax);
       
   159       var dataScale = d3s.scaleLinear().domain([globalMin, dMax]).range([globalMin, dMax]);
       
   160 
       
   161       var nameArray = rData.children.reduce(function (res, d) {
       
   162         res = res.concat(d.name.split(/[\s\-]/));
       
   163         return res;
       
   164       }, ["(nnnnn)"]).sort(function(a,b) { return b.length - a.length; });
       
   165 
       
   166       var w = self.get('utils').getWidthOfText(nameArray[0], '11px');
       
   167       var p = w*w/(width*height);
       
   168 
       
   169       if(dMin/dMax < p) {
       
   170        dataScale = dataScale.range([p*100,100]);
       
   171       }
       
   172 
       
   173       var rNode = d3h.hierarchy(rData)
       
   174         .sum(function (d) { return (d.children) ? 0 : dataScale(d.count); })
       
   175         .sort(function (a, b) { return b.value - a.value; });
       
   176 
       
   177       self.setProperties({ minCount: dMin, maxCount: dMax });
       
   178       var scale = self.get('scale');
       
   179       var backgroundColor = function (_d) { return scale(_d.data.count); };
       
   180       var colorClass = function (_d) { return (self.get('colors').getPerceptiveLuminance(backgroundColor(_d)) >= 0.5) ? 'light-color' : 'dark-color'; };
       
   181 
       
   182       var nodeWrapper = element.append("div")
       
   183         .attr("class", "nodes")
       
   184         .datum(rNode);
       
   185 
       
   186       var descendants = treemap(rNode).descendants().filter(function (c) { return c.depth === rNode.depth + 1; });
       
   187 
       
   188       var node = nodeWrapper.selectAll()
       
   189         .data(descendants)
       
   190         .enter()
       
   191         .append("div")
       
   192         .attr("data-id", function (d) { return d.data.id; });
       
   193 
       
   194 
       
   195       node.attr("class", function (_d) { return "node " + colorClass(_d) + (_d.id === self.get('filter').get('language') ? " selected" : ""); })
       
   196         .style("width", function (d) { return (Math.round(d.x1 * ratio) - Math.round(d.x0 * ratio)) + 'px'; })
       
   197         .style("height", function (d) { return (d.y1 - d.y0) + 'px'; })
       
   198         .style("left", function (d) { return Math.round(d.x0*ratio) + 'px'; })
       
   199         .style("top", function (d) { return d.y0 + 'px'; })
       
   200         .style("background-color", backgroundColor)
       
   201         .style('opacity', opacity)
       
   202         .attr('title', function (d) { return d.data.name + ' (' + d.data.count + ')'; })
       
   203         .on("click", function (d) { selectHandler(d.data); });
       
   204 
       
   205       node.filter(function (d) { return d.children; })
       
   206         .classed("children", true)
       
   207         .on("click", function (d) { transition(d.data, node, nodeWrapper); })
       
   208         .append("i")
       
   209         .attr("class", "fa fa-folder-o");
       
   210 
       
   211       node.append("span")
       
   212         .html(function (d) { return d.data.name + ' <span class="count">(' + d.data.count + ')</span>'; });
       
   213 
       
   214       breadcrumbs
       
   215         .selectAll("span")
       
   216         .remove();
       
   217       let i = 0;
       
   218       ancestors(rNode.data).forEach(function (a) {
       
   219         breadcrumbs
       
   220           .append("span")
       
   221           .attr('class', (i++) ? "level" : "root")
       
   222           .html(a.name)
       
   223           .datum(a)
       
   224           .on("click", function (d) { transition(d, node, nodeWrapper); });
       
   225       });
       
   226       breadcrumbs
       
   227         .append("span")
       
   228         .attr('class', (i) ? "level" : "root")
       
   229         .html(rNode.data.name)
       
   230         .datum(rNode.data);
       
   231 
       
   232       return node;
       
   233     }
       
   234 
       
   235   }
   220 
   236 
   221 });
   237 });