--- 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;
+ }
+
+ }
+
});