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