From 677f6884814d9f816d19f0a46dcf134064a67376 Mon Sep 17 00:00:00 2001 From: Anton Zhevak Date: Sat, 2 Nov 2013 23:23:14 +0600 Subject: [PATCH 01/15] =?UTF-8?q?=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D1=8B=20=D0=BD=D0=B5=D0=BE=D0=B1=D1=85=D0=BE=D0=B4=D0=B8=D0=BC?= =?UTF-8?q?=D1=8B=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 1 + bookmarklet.js | 5 +++++ graph.js | 1 + ls.js | 1 + main.js | 1 + 5 files changed, 9 insertions(+) create mode 100644 app.js create mode 100644 bookmarklet.js create mode 100644 graph.js create mode 100644 ls.js create mode 100644 main.js diff --git a/app.js b/app.js new file mode 100644 index 0000000..ad9a93a --- /dev/null +++ b/app.js @@ -0,0 +1 @@ +'use strict'; diff --git a/bookmarklet.js b/bookmarklet.js new file mode 100644 index 0000000..ec92c20 --- /dev/null +++ b/bookmarklet.js @@ -0,0 +1,5 @@ +javascript:(function() { + + + +})(); diff --git a/graph.js b/graph.js new file mode 100644 index 0000000..ad9a93a --- /dev/null +++ b/graph.js @@ -0,0 +1 @@ +'use strict'; diff --git a/ls.js b/ls.js new file mode 100644 index 0000000..ad9a93a --- /dev/null +++ b/ls.js @@ -0,0 +1 @@ +'use strict'; diff --git a/main.js b/main.js new file mode 100644 index 0000000..ad9a93a --- /dev/null +++ b/main.js @@ -0,0 +1 @@ +'use strict'; From 60b76d00cefc539c16c811a41d50000d7c07d6e9 Mon Sep 17 00:00:00 2001 From: Anton Zhevak Date: Sat, 2 Nov 2013 23:41:43 +0600 Subject: [PATCH 02/15] =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=87=D0=B0?= =?UTF-8?q?=D1=8F=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 208 +++++++++++++++++++++++++++++++++++++++++++++++++ bookmarklet.js | 26 +++++++ graph.js | 155 ++++++++++++++++++++++++++++++++++++ ls.js | 40 ++++++++++ main.js | 36 +++++++++ 5 files changed, 465 insertions(+) diff --git a/app.js b/app.js index ad9a93a..0565ff9 100644 --- a/app.js +++ b/app.js @@ -1 +1,209 @@ 'use strict'; + +define([ + 'jquery', + 'graph', + 'ls' +], function($, Graph, LS) { + + /** + * Инициализирует приложение. + * + * @constructor + * @param {Object} config настройки + */ + var App = function(config) { + config = config || {}; + config.userCount = config.userCount || 5; + config.blackList = config.blackList || []; + + // Извлекаем имена пользователей, с которых мы начнём строить дерево + this.startUsernames = this.getStartUsernames(config.userCount); + document.body.innerHTML = ''; + document.body.removeAttribute('style'); + + this.usersUrl = config.usersUrl; + + // Игнорируемые + this.blackList = config.blackList; + + // Визуализация графа + this.graph = new Graph; + + // Работа с localStorage + this.ls = new LS; + + // Сколько коренных пользователей должно найтись + // Число может меняться, если присутствуют пользователи из черного списка + this.rootsMustFind = config.userCount; + + // Сколько коренных пользователей найдено + this.rootsFound = 0; + + // Очередь детей, о которых надо получить информацию и добавить на граф + this.queue = []; + + // Хранение полученных данных о пользователе + this.storage = {}; + }; + + /** + * Строит граф. + */ + App.prototype.start = function() { + var self = this; + + // Ищем корни + for (var i = 0, len = this.startUsernames.length; i < len; i++) { + // Если пользователь в чёрном списке, пропускаем его + if (self.blackList.indexOf(this.startUsernames[i]) > -1) { + this.rootsMustFind--; + continue; + } + + this.getUserInfo(this.startUsernames[i]).then(function(user) { + self.findRoot(user); + }); + } + + // Когда найдутся все корни + this.thatsAll = setInterval(function() { + if (self.rootsFound >= self.rootsMustFind) { + var u = self.queue.shift(); + if (u) { + self.getUserInfo(u).then(function(user) { + self.addToStorage(user); + }); + } else { + // Очередь может быть пуста потому что новые данные находятся в процессе получения, + // поэтому подстраховываемся + setTimeout(function() { + if (self.queue.length === 0) { + alert('Граф построен'); + clearInterval(self.thatsAll); + self.thatsAll = true; + } + }, 2000); + } + } + }, 500); + + var endGraphUpdate = setInterval(function() { + self.updateGraph(); + if (self.thatsAll === true) { + clearInterval(endGraphUpdate); + } + }, 2000); + }; + + /** + * Обновляет граф. + */ + App.prototype.updateGraph = function() { + for (var u in this.storage) { + var user = this.storage[u]; + + if (self.rootsFound >= self.rootsMustFind) + delete this.storage[u]; + + this.graph.addNode(user); + this.graph.addLink(user.name, user.parent ? user.parent : 'НЛО'); + } + this.graph.update(); + }; + + /** + * Ищет первого зарегистрировавшегося, с которого началась ветка. + * + * @param {Object} user пользователь + * @return {Object} Deferred-объект + */ + App.prototype.findRoot = function(user) { + this.addToStorage(user); + // Если у пользователя есть родитель, значит пока он нас не интересует; + // добавляем его в хранилище и запрашиваем информацию о его родителе + if (user.parent) { + var self = this; + var d = this.getUserInfo(user.parent).then(function(parent) { + return self.findRoot(parent); + }); + return d; + } else { // Если пользователь зарегистрировался по приглашению НЛО + this.rootsFound++; + var d = $.Deferred(); + return d.resolve(user); + } + }; + + /** + * Добавляет пользователя в хранилище. + * + * @param {Object} user пользователь + */ + App.prototype.addToStorage = function(user) { + this.storage[user.name] = user; + }; + + /** + * Находит на странице имена либо всех, либо первых n пользователей. + * + * @param {number} n максимальное число имён пользователей + * @return {Array} имена пользователей + */ + App.prototype.getStartUsernames = function(n) { + var users = $('.username a'); + for (var i = 0, usernames = [], n = n || users.length; i < n; i++) { + usernames.push(users[i].innerHTML); + } + return usernames; + }; + + /** + * Получает информацию о запрашиваемом пользователе либо из хранилища, либо со страницы его профиля. + * + * @param {string} username ник пользователя + * @return {Object} Deferred-объект + */ + App.prototype.getUserInfo = function(username) { + var d = $.Deferred(), + self = this, + user = this.ls.loadUser(username); + if (user === null) { + d = $.get(this.usersUrl + username + '/').then(function(data) { + var user = {}; + + user.name = $(data).find('.user_header h2.username a').text(); + user.avatar = $(data).find('.user_header .avatar img').attr('src'); + user.parent = $(data).find('#invited-by').text() || null; + + // [rel=friend] - иначе при большом кол-ве приглашённых появляется ссылка "показать все" + var children = $(data).find('#invited_data_items a[rel=friend]'); + + if (children.length > 0) { + user.children = []; + for (var i = 0, len = children.length; i < len; i++) { + self.queue.push(children[i].innerHTML); // добавляем детей в отдельную очередь + user.children.push(children[i].innerHTML); + } + } + + // Кешируем в localStorage + self.ls.saveUser(user); + + return user; + }).promise(); + return d; + } else { + if (user.children) { + for (var i = 0, len = user.children.length; i < len; i++) { + this.queue.push(user.children[i]); // добавляем детей в отдельную очередь + user.children.push(user.children[i]); + } + } + return d.resolve(user); + } + }; + + return App; + +}); \ No newline at end of file diff --git a/bookmarklet.js b/bookmarklet.js index ec92c20..050b37b 100644 --- a/bookmarklet.js +++ b/bookmarklet.js @@ -1,5 +1,31 @@ javascript:(function() { + var config = { + require : '//cdnjs.cloudflare.com/ajax/libs/require.js/2.1.9/require.min.js', + start : 'https://rawgithub.com/mayton/5-async/master/main.js' + }; + var body = document.body; + if (body.getAttribute('count') !== null) { + alert('Для повторного запуска сценария необходимо обновить страницу.'); + return false; + } + + var userCount = prompt('Сколько пользователей брать для анализа?', 5); + if (userCount === null) + return false; + + body.setAttribute('count', userCount); + body.style.display = 'none'; + + var head = document.getElementsByTagName('head')[0]; + head.innerHTML = 'Домашняя работа №5'; + + if ( ! window.requirejs) { + var s = document.createElement('script'); + s.setAttribute('src', config.require); + s.setAttribute('data-main', config.start); + head.appendChild(s); + } })(); diff --git a/graph.js b/graph.js index ad9a93a..64921af 100644 --- a/graph.js +++ b/graph.js @@ -1 +1,156 @@ 'use strict'; + +define([ + 'd3' +], function(d3) { + + /** + * Отвечает за визуализацию дерева. + * + * @constructor + * @param {Object} config настройки + */ + var Graph = function(config) { + config = config || {}; + config.width = document.documentElement.clientWidth; + config.height = document.documentElement.clientHeight; + + this.svg = d3.select('body').append('svg') + .attr('width', '100%') + .attr('height', '100%'); + + // Список пользователей + this.nodes = [ + { name : 'НЛО', x : config.width / 2, y : config.height / 2, avatar : '/favicon.ico', fixed : true } + ]; + + // Связи (кто кого пригласил) + this.links = []; + + this.force = d3.layout.force() + .nodes(this.nodes) + .links(this.links) + .distance(100) + .linkDistance(100) + .charge(-100) + .size([config.width, config.height]) + .start(); + + var self = this; + d3.select(window).on('resize', function() { + self.force.size([document.documentElement.clientWidth, document.documentElement.clientHeight]); + }); + }; + + /** + * Добавляет узел и обновляет холст. + * + * @param {Object} node объект пользователя + */ + Graph.prototype.addNode = function(node) { + this.nodes.push({ + name : node.name, + avatar : node.avatar + }); + }; + + /** + * Добавляет связь и обновляет холст. + * + * @param {string} source имя пригласившего пользователя + * @param {string} target имя приглашённого пользователя + */ + Graph.prototype.addLink = function (source, target) { + source = this.findNode(source); + target = this.findNode(target); + + if (source && target) { + this.links.push({ + source : source, + target : target + }); + } + }; + + /** + * Ищет в списке узлов и по возможности возвращает пользователя с запрашиваемым именем. + * + * @param {string} name имя пользователя + * @returns {(Object|boolean)} объект пользователя или false + */ + Graph.prototype.findNode = function(name) { + for (var i in this.nodes) { + if (this.nodes[i]['name'] === name) + return this.nodes[i]; + } + return false; + }; + + /** + * Обновляет холст. + */ + Graph.prototype.update = function() { + // Связи + var link = this.svg.selectAll('.link') + .data(this.force.links(), function(d) { return d.source.name + '-' + d.target.name }); + + link.enter() + .append('line') + .attr('class', 'link') + .attr('stroke', '#666'); + link.exit().remove(); + + // Узлы + var node = this.svg.selectAll('.node') + .data(this.force.nodes(), function(d) { return d.name }); + var nodeEnter = node.enter() + .append('g') + .attr('class', 'node') + .call(this.force.drag); + /* + nodeEnter.append('circle') + .attr('r', 12) + .attr('fill', 'white') + .attr('stroke', 'black'); + */ + nodeEnter.append('title') + .text(function(d) { + return d.name; + }); + nodeEnter.append('image') + .attr('xlink:href', function(d) { + return d.avatar; + }) + .attr('x', -12) + .attr('y', -12) + .attr('width', 24) + .attr('height', 24); + /* + nodeEnter.append('text') + .attr('dx', 16) + .attr('dy', 4) + .text(function(d) { + return d.name; + }); + */ + node.exit().remove(); + + this.force.on('tick', function() { + link.attr('x1', function(d) { return d.source.x; }) + .attr('y1', function(d) { return d.source.y; }) + .attr('x2', function(d) { return d.target.x; }) + .attr('y2', function(d) { return d.target.y; }); + node.attr('x', function(d) { return d.x; }) + .attr('y', function(d) { return d.y; }) + node.attr('transform', function(d) { + return 'translate(' + d.x + ',' + d.y + ')'; + }); + }) + .nodes(this.nodes) + .links(this.links) + .start(); + }; + + return Graph; + +}); diff --git a/ls.js b/ls.js index ad9a93a..4c156c8 100644 --- a/ls.js +++ b/ls.js @@ -1 +1,41 @@ 'use strict'; + +define(function() { + + /** + * Небольшой класс для упрощения работы с локальным хранилищем. + * + * @constructor + */ + var LS = function() { + }; + + /** + * Префикс для ключей с данными о пользователях. + * + * @type {string} + */ + LS.prototype.USER_PREFIX = 'habraUser.'; + + /** + * Получение пользователя из localStorage. + * + * @param {Object} username пользователь + * @returns {Object} пользователь + */ + LS.prototype.loadUser = function(username) { + return JSON.parse(window.localStorage.getItem(this.USER_PREFIX + username)); + }; + + /** + * Сохранение пользователя в localStorage. + * + * @param {Object} user + */ + LS.prototype.saveUser = function(user) { + window.localStorage.setItem(this.USER_PREFIX + user.name, JSON.stringify(user)); + }; + + return LS; + +}); diff --git a/main.js b/main.js index ad9a93a..9e614d0 100644 --- a/main.js +++ b/main.js @@ -1 +1,37 @@ 'use strict'; + +require.config({ + paths : { + 'jquery' : '//yandex.st/jquery/1.10.2/jquery.min', + 'd3' : 'http://d3js.org/d3.v3.min', + 'app' : 'app', + 'ls' : 'ls', + 'graph' : 'graph' + }, + shim: { + d3: { + exports : 'd3' + } + } +}); + +require([ + 'app' +], function(App) { + + // Пользователи, которые портят граф или вообще всё + var blackList = [ + 'Ronnie83', // приглашён на сайт сразу двумя пользователями maovrn и shifttstas + + 'Milla', // приглашён на сайт пользователем tangro + 'tangro' // приглашён на сайт пользователем Milla + ]; + + var app = new App({ + usersUrl : '/users/', + blackList : blackList, + userCount : +document.body.getAttribute('count') + }); + app.start(); + +}); \ No newline at end of file From 4d870334583af8607d36203a4f51c4b99a585bb1 Mon Sep 17 00:00:00 2001 From: Anton Zhevak Date: Sat, 2 Nov 2013 23:48:27 +0600 Subject: [PATCH 03/15] =?UTF-8?q?=D0=BC=D0=B5=D0=BB=D0=BA=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 1 - graph.js | 2 -- 2 files changed, 3 deletions(-) diff --git a/app.js b/app.js index 0565ff9..8ab8e9d 100644 --- a/app.js +++ b/app.js @@ -79,7 +79,6 @@ define([ // поэтому подстраховываемся setTimeout(function() { if (self.queue.length === 0) { - alert('Граф построен'); clearInterval(self.thatsAll); self.thatsAll = true; } diff --git a/graph.js b/graph.js index 64921af..232601b 100644 --- a/graph.js +++ b/graph.js @@ -140,8 +140,6 @@ define([ .attr('y1', function(d) { return d.source.y; }) .attr('x2', function(d) { return d.target.x; }) .attr('y2', function(d) { return d.target.y; }); - node.attr('x', function(d) { return d.x; }) - .attr('y', function(d) { return d.y; }) node.attr('transform', function(d) { return 'translate(' + d.x + ',' + d.y + ')'; }); From ee475d3e356adb652e5def7e01309da043fe7049 Mon Sep 17 00:00:00 2001 From: Anton Zhevak Date: Sat, 2 Nov 2013 23:57:54 +0600 Subject: [PATCH 04/15] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B4=D0=BB=D1=8F=20firefox?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 7 ++++--- graph.js | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app.js b/app.js index 8ab8e9d..c388cfe 100644 --- a/app.js +++ b/app.js @@ -19,8 +19,10 @@ define([ // Извлекаем имена пользователей, с которых мы начнём строить дерево this.startUsernames = this.getStartUsernames(config.userCount); - document.body.innerHTML = ''; - document.body.removeAttribute('style'); + $('head').css('height', '100%'); + $(document.body).html('') + .css('height', '100%') + .show(); this.usersUrl = config.usersUrl; @@ -196,7 +198,6 @@ define([ if (user.children) { for (var i = 0, len = user.children.length; i < len; i++) { this.queue.push(user.children[i]); // добавляем детей в отдельную очередь - user.children.push(user.children[i]); } } return d.resolve(user); diff --git a/graph.js b/graph.js index 232601b..1256fbd 100644 --- a/graph.js +++ b/graph.js @@ -16,6 +16,7 @@ define([ config.height = document.documentElement.clientHeight; this.svg = d3.select('body').append('svg') + .style('display', 'block') .attr('width', '100%') .attr('height', '100%'); From 714ba16244c54782b22498e0e134117db5fd5f0d Mon Sep 17 00:00:00 2001 From: Anton Zhevak Date: Sun, 3 Nov 2013 00:05:58 +0600 Subject: [PATCH 05/15] head -> html --- app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.js b/app.js index c388cfe..429dd6a 100644 --- a/app.js +++ b/app.js @@ -19,7 +19,7 @@ define([ // Извлекаем имена пользователей, с которых мы начнём строить дерево this.startUsernames = this.getStartUsernames(config.userCount); - $('head').css('height', '100%'); + $('html').css('height', '100%'); $(document.body).html('') .css('height', '100%') .show(); From 69b15163f4eee0e0e62e5bd2da7d2ceb7d9337ab Mon Sep 17 00:00:00 2001 From: Anton Zhevak Date: Sun, 3 Nov 2013 14:01:45 +0600 Subject: [PATCH 06/15] =?UTF-8?q?=D0=BC=D0=B5=D0=BB=D0=BA=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 14 +++++++------- bookmarklet.js | 10 ++++------ 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/app.js b/app.js index 429dd6a..5bffdf5 100644 --- a/app.js +++ b/app.js @@ -169,6 +169,7 @@ define([ var d = $.Deferred(), self = this, user = this.ls.loadUser(username); + if (user === null) { d = $.get(this.usersUrl + username + '/').then(function(data) { var user = {}; @@ -179,7 +180,6 @@ define([ // [rel=friend] - иначе при большом кол-ве приглашённых появляется ссылка "показать все" var children = $(data).find('#invited_data_items a[rel=friend]'); - if (children.length > 0) { user.children = []; for (var i = 0, len = children.length; i < len; i++) { @@ -194,14 +194,14 @@ define([ return user; }).promise(); return d; - } else { - if (user.children) { - for (var i = 0, len = user.children.length; i < len; i++) { - this.queue.push(user.children[i]); // добавляем детей в отдельную очередь - } + } + + if (user.children) { + for (var i = 0, len = user.children.length; i < len; i++) { + this.queue.push(user.children[i]); // добавляем детей в отдельную очередь } - return d.resolve(user); } + return d.resolve(user); }; return App; diff --git a/bookmarklet.js b/bookmarklet.js index 050b37b..1259126 100644 --- a/bookmarklet.js +++ b/bookmarklet.js @@ -21,11 +21,9 @@ javascript:(function() { var head = document.getElementsByTagName('head')[0]; head.innerHTML = 'Домашняя работа №5'; - if ( ! window.requirejs) { - var s = document.createElement('script'); - s.setAttribute('src', config.require); - s.setAttribute('data-main', config.start); - head.appendChild(s); - } + var s = document.createElement('script'); + s.setAttribute('src', config.require); + s.setAttribute('data-main', config.start); + head.appendChild(s); })(); From bbaf8ea4a932371ec63bcad5d2ea46383df8f349 Mon Sep 17 00:00:00 2001 From: Anton Zhevak Date: Tue, 5 Nov 2013 00:45:41 +0600 Subject: [PATCH 07/15] =?UTF-8?q?=D0=B7=D0=B0=D0=BC=D0=B5=D0=BD=D0=B0=20la?= =?UTF-8?q?yout.force=20=D0=BD=D0=B0=20layout.tree,=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=B4=D0=B5=D0=BB=D0=B0=D0=BD=20=D0=B0=D0=BB=D0=B3=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D1=82=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 119 ++++++++++++++----------------------- bookmarklet.js | 11 +++- graph.js | 155 ------------------------------------------------- ls.js | 2 +- main.js | 2 +- tree.js | 141 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 194 insertions(+), 236 deletions(-) delete mode 100644 graph.js create mode 100644 tree.js diff --git a/app.js b/app.js index 5bffdf5..13369fc 100644 --- a/app.js +++ b/app.js @@ -2,9 +2,9 @@ define([ 'jquery', - 'graph', + 'tree', 'ls' -], function($, Graph, LS) { +], function($, Tree, LS) { /** * Инициализирует приложение. @@ -14,14 +14,13 @@ define([ */ var App = function(config) { config = config || {}; - config.userCount = config.userCount || 5; + config.userCount = config.userCount || 2; config.blackList = config.blackList || []; // Извлекаем имена пользователей, с которых мы начнём строить дерево this.startUsernames = this.getStartUsernames(config.userCount); - $('html').css('height', '100%'); $(document.body).html('') - .css('height', '100%') + .css({ 'margin' : 0 }) .show(); this.usersUrl = config.usersUrl; @@ -30,23 +29,13 @@ define([ this.blackList = config.blackList; // Визуализация графа - this.graph = new Graph; + this.tree = new Tree; // Работа с localStorage this.ls = new LS; - // Сколько коренных пользователей должно найтись - // Число может меняться, если присутствуют пользователи из черного списка - this.rootsMustFind = config.userCount; - - // Сколько коренных пользователей найдено - this.rootsFound = 0; - // Очередь детей, о которых надо получить информацию и добавить на граф this.queue = []; - - // Хранение полученных данных о пользователе - this.storage = {}; }; /** @@ -55,62 +44,58 @@ define([ App.prototype.start = function() { var self = this; - // Ищем корни + // Ищем корни и добавляем их на граф for (var i = 0, len = this.startUsernames.length; i < len; i++) { - // Если пользователь в чёрном списке, пропускаем его - if (self.blackList.indexOf(this.startUsernames[i]) > -1) { - this.rootsMustFind--; + if (self.checkUser(this.startUsernames[i])) continue; - } this.getUserInfo(this.startUsernames[i]).then(function(user) { - self.findRoot(user); + // TODO: внутри findRoot не проверяется черный список + self.findRoot(user).then(function(user) { + $.proxy(self.addUser(user), self); + }); }); } - // Когда найдутся все корни - this.thatsAll = setInterval(function() { - if (self.rootsFound >= self.rootsMustFind) { + // Каждые полсекунды будем доставать из очереди пользователя + setInterval(function() { + if (self.queue.length > 0) { var u = self.queue.shift(); - if (u) { - self.getUserInfo(u).then(function(user) { - self.addToStorage(user); - }); - } else { - // Очередь может быть пуста потому что новые данные находятся в процессе получения, - // поэтому подстраховываемся - setTimeout(function() { - if (self.queue.length === 0) { - clearInterval(self.thatsAll); - self.thatsAll = true; - } - }, 2000); - } - } - }, 500); - var endGraphUpdate = setInterval(function() { - self.updateGraph(); - if (self.thatsAll === true) { - clearInterval(endGraphUpdate); + if (self.checkUser(u)) + return; + + // Получать о нём информацию и добавлять на граф + self.getUserInfo(u).then(function(user) { + $.proxy(self.addUser(user), self); + }); } - }, 2000); + }, 500); }; /** - * Обновляет граф. + * Проверяет, есть ли пользователь с указанным именем в чёрном списке. + * + * @param {string} name имя пользователя + * @returns {boolean} */ - App.prototype.updateGraph = function() { - for (var u in this.storage) { - var user = this.storage[u]; + App.prototype.checkUser = function(name) { + return !! (this.blackList.indexOf(name) > -1); + }; - if (self.rootsFound >= self.rootsMustFind) - delete this.storage[u]; + /** + * Добавляет пользователя на граф, а имена его детей в очередь на обход. + * + * @param {Object} user пользователь + */ + App.prototype.addUser = function(user) { + this.tree.addNode(user); + this.tree.update(); - this.graph.addNode(user); - this.graph.addLink(user.name, user.parent ? user.parent : 'НЛО'); + if (user.children.length > 0) { + for (var i = 0, l = user.children.length; i < l; i++) + this.queue.push(user.children[i]); } - this.graph.update(); }; /** @@ -120,7 +105,6 @@ define([ * @return {Object} Deferred-объект */ App.prototype.findRoot = function(user) { - this.addToStorage(user); // Если у пользователя есть родитель, значит пока он нас не интересует; // добавляем его в хранилище и запрашиваем информацию о его родителе if (user.parent) { @@ -130,21 +114,11 @@ define([ }); return d; } else { // Если пользователь зарегистрировался по приглашению НЛО - this.rootsFound++; var d = $.Deferred(); return d.resolve(user); } }; - /** - * Добавляет пользователя в хранилище. - * - * @param {Object} user пользователь - */ - App.prototype.addToStorage = function(user) { - this.storage[user.name] = user; - }; - /** * Находит на странице имена либо всех, либо первых n пользователей. * @@ -174,18 +148,16 @@ define([ d = $.get(this.usersUrl + username + '/').then(function(data) { var user = {}; - user.name = $(data).find('.user_header h2.username a').text(); + user.name = username; // $(data).find('.user_header h2.username a').text(); user.avatar = $(data).find('.user_header .avatar img').attr('src'); user.parent = $(data).find('#invited-by').text() || null; + user.children = []; // [rel=friend] - иначе при большом кол-ве приглашённых появляется ссылка "показать все" var children = $(data).find('#invited_data_items a[rel=friend]'); if (children.length > 0) { - user.children = []; - for (var i = 0, len = children.length; i < len; i++) { - self.queue.push(children[i].innerHTML); // добавляем детей в отдельную очередь + for (var i = 0, l = children.length; i < l; i++) user.children.push(children[i].innerHTML); - } } // Кешируем в localStorage @@ -196,11 +168,6 @@ define([ return d; } - if (user.children) { - for (var i = 0, len = user.children.length; i < len; i++) { - this.queue.push(user.children[i]); // добавляем детей в отдельную очередь - } - } return d.resolve(user); }; diff --git a/bookmarklet.js b/bookmarklet.js index 1259126..b675042 100644 --- a/bookmarklet.js +++ b/bookmarklet.js @@ -5,15 +5,20 @@ javascript:(function() { start : 'https://rawgithub.com/mayton/5-async/master/main.js' }; + if (window.location.href !== 'http://habrahabr.ru/users/') { + alert('Для запуска сценария перейдите по адресу http://habrahabr.ru/users/'); + return; + } + var body = document.body; if (body.getAttribute('count') !== null) { alert('Для повторного запуска сценария необходимо обновить страницу.'); - return false; + return; } - var userCount = prompt('Сколько пользователей брать для анализа?', 5); + var userCount = prompt('Сколько пользователей брать для анализа?', 2); if (userCount === null) - return false; + return; body.setAttribute('count', userCount); body.style.display = 'none'; diff --git a/graph.js b/graph.js deleted file mode 100644 index 1256fbd..0000000 --- a/graph.js +++ /dev/null @@ -1,155 +0,0 @@ -'use strict'; - -define([ - 'd3' -], function(d3) { - - /** - * Отвечает за визуализацию дерева. - * - * @constructor - * @param {Object} config настройки - */ - var Graph = function(config) { - config = config || {}; - config.width = document.documentElement.clientWidth; - config.height = document.documentElement.clientHeight; - - this.svg = d3.select('body').append('svg') - .style('display', 'block') - .attr('width', '100%') - .attr('height', '100%'); - - // Список пользователей - this.nodes = [ - { name : 'НЛО', x : config.width / 2, y : config.height / 2, avatar : '/favicon.ico', fixed : true } - ]; - - // Связи (кто кого пригласил) - this.links = []; - - this.force = d3.layout.force() - .nodes(this.nodes) - .links(this.links) - .distance(100) - .linkDistance(100) - .charge(-100) - .size([config.width, config.height]) - .start(); - - var self = this; - d3.select(window).on('resize', function() { - self.force.size([document.documentElement.clientWidth, document.documentElement.clientHeight]); - }); - }; - - /** - * Добавляет узел и обновляет холст. - * - * @param {Object} node объект пользователя - */ - Graph.prototype.addNode = function(node) { - this.nodes.push({ - name : node.name, - avatar : node.avatar - }); - }; - - /** - * Добавляет связь и обновляет холст. - * - * @param {string} source имя пригласившего пользователя - * @param {string} target имя приглашённого пользователя - */ - Graph.prototype.addLink = function (source, target) { - source = this.findNode(source); - target = this.findNode(target); - - if (source && target) { - this.links.push({ - source : source, - target : target - }); - } - }; - - /** - * Ищет в списке узлов и по возможности возвращает пользователя с запрашиваемым именем. - * - * @param {string} name имя пользователя - * @returns {(Object|boolean)} объект пользователя или false - */ - Graph.prototype.findNode = function(name) { - for (var i in this.nodes) { - if (this.nodes[i]['name'] === name) - return this.nodes[i]; - } - return false; - }; - - /** - * Обновляет холст. - */ - Graph.prototype.update = function() { - // Связи - var link = this.svg.selectAll('.link') - .data(this.force.links(), function(d) { return d.source.name + '-' + d.target.name }); - - link.enter() - .append('line') - .attr('class', 'link') - .attr('stroke', '#666'); - link.exit().remove(); - - // Узлы - var node = this.svg.selectAll('.node') - .data(this.force.nodes(), function(d) { return d.name }); - var nodeEnter = node.enter() - .append('g') - .attr('class', 'node') - .call(this.force.drag); - /* - nodeEnter.append('circle') - .attr('r', 12) - .attr('fill', 'white') - .attr('stroke', 'black'); - */ - nodeEnter.append('title') - .text(function(d) { - return d.name; - }); - nodeEnter.append('image') - .attr('xlink:href', function(d) { - return d.avatar; - }) - .attr('x', -12) - .attr('y', -12) - .attr('width', 24) - .attr('height', 24); - /* - nodeEnter.append('text') - .attr('dx', 16) - .attr('dy', 4) - .text(function(d) { - return d.name; - }); - */ - node.exit().remove(); - - this.force.on('tick', function() { - link.attr('x1', function(d) { return d.source.x; }) - .attr('y1', function(d) { return d.source.y; }) - .attr('x2', function(d) { return d.target.x; }) - .attr('y2', function(d) { return d.target.y; }); - node.attr('transform', function(d) { - return 'translate(' + d.x + ',' + d.y + ')'; - }); - }) - .nodes(this.nodes) - .links(this.links) - .start(); - }; - - return Graph; - -}); diff --git a/ls.js b/ls.js index 4c156c8..97a8d9f 100644 --- a/ls.js +++ b/ls.js @@ -15,7 +15,7 @@ define(function() { * * @type {string} */ - LS.prototype.USER_PREFIX = 'habraUser.'; + LS.prototype.USER_PREFIX = 'habraUserv2.'; /** * Получение пользователя из localStorage. diff --git a/main.js b/main.js index 9e614d0..a2dd132 100644 --- a/main.js +++ b/main.js @@ -6,7 +6,7 @@ require.config({ 'd3' : 'http://d3js.org/d3.v3.min', 'app' : 'app', 'ls' : 'ls', - 'graph' : 'graph' + 'tree' : 'tree' }, shim: { d3: { diff --git a/tree.js b/tree.js new file mode 100644 index 0000000..86a1af8 --- /dev/null +++ b/tree.js @@ -0,0 +1,141 @@ +'use strict'; + +define([ + 'd3' +], function(d3) { + + var Tree = function(config) { + config = config || {}; + var w = document.documentElement.clientWidth - 20, + h = document.documentElement.clientHeight - 20; + + this.duration = config.duration || 500; + + this.svg = d3.select('body').append('svg') + .style('display', 'block') + .attr('width', w) + .attr('height', h) + .append('g') + .attr('transform', 'translate(10,10)'); + + this.tree = d3.layout.tree() + .size([w - 40, h - 40]); + + this.root = {}; + this.nodes = this.tree(this.root); + + this.root.parent = this.root; + this.root.name = 'НЛО'; + this.root.children = []; + this.root.px = this.root.x; + this.root.py = this.root.y; + this.root.avatar = '/favicon.ico'; + + var self = this; + d3.select(window).on('resize', function() { + var w = document.documentElement.clientWidth - 20; + var h = document.documentElement.clientHeight - 20; + d3.select('svg') + .attr('width', w) + .attr('height', h); + self.tree.size([ w - 40, h - 40 ]); + }); + }; + + /** + * Добавляет узел в дерево. + * + * @param {Object} node узел + * @returns {boolean} результат добавления + */ + Tree.prototype.addNode = function(node) { + var n = { + name : node.name, + avatar : node.avatar + }; + + if (node.parent === null) { + parent = this.nodes[0]; + } else { + for (var parent, i = 0, l = this.nodes.length; i < l; i++) { + if (this.nodes[i].name === node.parent) { + parent = this.nodes[i]; + break; + } + } + } + + if (parent === undefined) { + console.log('Не могу найти родителя узла ' + node.name); + return false; + } + + if (parent.children) + parent.children.push(n); + else + parent.children = [n]; + this.nodes.push(n); + + return true; + }; + + /** + * Обновляет дерево. + */ + Tree.prototype.update = function() { + var diagonal = d3.svg.diagonal() + .projection(function(d) { return [d.x, d.y]; }); + + var node = this.svg.selectAll('.node') + .data(this.tree.nodes(this.root), function(d) { return d.name; }); + var link = this.svg.selectAll('.link') + .data(this.tree.links(this.nodes), function(d) { return d.source.name + '-' + d.target.name; }); + + var nodeEnter = node.enter() + .append('g') + .attr('class', 'node') + .attr('transform', function(d) { + return 'translate(' + d.parent.px + ',' + d.parent.py + ')'; + }); + + nodeEnter.append('image') + .attr('xlink:href', function(d) { return d.avatar; }) + .attr('x', -12) + .attr('y' -12) + .attr('opacity', 0.1) + .attr('width', 24) + .attr('height', 24); + + nodeEnter.append('title') + .text(function(d) { return d.name; }); + + link.enter() + .insert('path', '.node') + .attr('class', 'link') + .attr('d', function(d) { + var o = {x: d.source.px, y: d.source.py}; + return diagonal({ source : o, target : o }); + }) + .attr('fill', 'none') + .attr('stroke', '#000'); + + var t = this.svg.transition() + .duration(this.duration); + + t.selectAll('.link') + .attr('d', diagonal); + + t.selectAll('.node') + .attr('transform', function(d) { + d.px = d.x; + d.py = d.y; + return 'translate(' + d.x + ',' + d.y + ')'; + }); + + t.selectAll('image') + .attr('opacity', 1); + }; + + return Tree; + +}); From 4a6e3fdd6e69871c9d653b0e15a873cbedcd28ef Mon Sep 17 00:00:00 2001 From: Anton Zhevak Date: Tue, 5 Nov 2013 01:39:53 +0600 Subject: [PATCH 08/15] =?UTF-8?q?=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=B0=D0=BB=D0=B3=D0=BE=D1=80=D0=B8=D1=82=D0=BC=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D0=BE=D0=B2=20=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=EF=BF=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit теперь данные о пользователе из кеша возвращаются мгновенно, а запросы к серверу ставятся в очередь --- app.js | 68 +++++++++++++++++++++++++++++++++------------------------ tree.js | 3 ++- 2 files changed, 41 insertions(+), 30 deletions(-) diff --git a/app.js b/app.js index 13369fc..21ba94e 100644 --- a/app.js +++ b/app.js @@ -28,23 +28,29 @@ define([ // Игнорируемые this.blackList = config.blackList; - // Визуализация графа + // Визуализация дерева this.tree = new Tree; // Работа с localStorage this.ls = new LS; - // Очередь детей, о которых надо получить информацию и добавить на граф + // Очередь детей, о которых надо получить информацию и добавить в дерево this.queue = []; + + // Величина задержки между первым запросом и последним + this.delay = 0; + + // Величина паузы между запросами к серверу + this.timeout = 500; }; /** - * Строит граф. + * Строит дерево. */ App.prototype.start = function() { var self = this; - // Ищем корни и добавляем их на граф + // Ищем корни и добавляем их в дерево for (var i = 0, len = this.startUsernames.length; i < len; i++) { if (self.checkUser(this.startUsernames[i])) continue; @@ -65,12 +71,12 @@ define([ if (self.checkUser(u)) return; - // Получать о нём информацию и добавлять на граф + // Получать о нём информацию и добавлять в дерево self.getUserInfo(u).then(function(user) { $.proxy(self.addUser(user), self); }); } - }, 500); + }, 100); }; /** @@ -84,7 +90,7 @@ define([ }; /** - * Добавляет пользователя на граф, а имена его детей в очередь на обход. + * Добавляет пользователя в дерево, а имена его детей в очередь на обход. * * @param {Object} user пользователь */ @@ -144,31 +150,35 @@ define([ self = this, user = this.ls.loadUser(username); + // Если пользователя нет в кеше, делаем запрос if (user === null) { - d = $.get(this.usersUrl + username + '/').then(function(data) { - var user = {}; - - user.name = username; // $(data).find('.user_header h2.username a').text(); - user.avatar = $(data).find('.user_header .avatar img').attr('src'); - user.parent = $(data).find('#invited-by').text() || null; - user.children = []; - - // [rel=friend] - иначе при большом кол-ве приглашённых появляется ссылка "показать все" - var children = $(data).find('#invited_data_items a[rel=friend]'); - if (children.length > 0) { - for (var i = 0, l = children.length; i < l; i++) - user.children.push(children[i].innerHTML); - } - - // Кешируем в localStorage - self.ls.saveUser(user); - - return user; - }).promise(); - return d; + setTimeout(function() { + $.get(self.usersUrl + username + '/').then(function(data) { + var user = {}; + + user.name = username; // $(data).find('.user_header h2.username a').text(); + user.avatar = $(data).find('.user_header .avatar img').attr('src'); + user.parent = $(data).find('#invited-by').text() || null; + user.children = []; + + // [rel=friend] - иначе при большом кол-ве приглашённых появляется ссылка "показать все" + var children = $(data).find('#invited_data_items a[rel=friend]'); + if (children.length > 0) { + for (var i = 0, l = children.length; i < l; i++) + user.children.push(children[i].innerHTML); + } + + // Кешируем в localStorage + self.ls.saveUser(user); + + d.resolve(user); + }); + }, this.delay += this.timeout); + } else { + d.resolve(user); } - return d.resolve(user); + return d; }; return App; diff --git a/tree.js b/tree.js index 86a1af8..8b8afe6 100644 --- a/tree.js +++ b/tree.js @@ -9,7 +9,8 @@ define([ var w = document.documentElement.clientWidth - 20, h = document.documentElement.clientHeight - 20; - this.duration = config.duration || 500; + // Продолжительность анимации, мс + this.duration = config.duration || 100; this.svg = d3.select('body').append('svg') .style('display', 'block') From 3975c69f467c4947ec9dfa6b236e7d3c599178c2 Mon Sep 17 00:00:00 2001 From: Anton Zhevak Date: Tue, 5 Nov 2013 01:52:25 +0600 Subject: [PATCH 09/15] =?UTF-8?q?=D0=BD=D0=B5=D0=B1=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D1=88=D0=B0=D1=8F,=20=D0=BD=D0=BE=20=D0=B2=D0=B0=D0=B6=D0=BD?= =?UTF-8?q?=D0=B0=D1=8F=20=D0=BA=D0=BE=D1=80=D1=80=D0=B5=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app.js b/app.js index 21ba94e..3a2357b 100644 --- a/app.js +++ b/app.js @@ -154,6 +154,8 @@ define([ if (user === null) { setTimeout(function() { $.get(self.usersUrl + username + '/').then(function(data) { + self.delay--; + var user = {}; user.name = username; // $(data).find('.user_header h2.username a').text(); @@ -173,7 +175,7 @@ define([ d.resolve(user); }); - }, this.delay += this.timeout); + }, this.delay++ * this.timeout); } else { d.resolve(user); } From 8564a9bb0780e4fd440c339d0750f45192971829 Mon Sep 17 00:00:00 2001 From: Anton Zhevak Date: Tue, 5 Nov 2013 01:55:19 +0600 Subject: [PATCH 10/15] =?UTF-8?q?=D1=83=D0=B2=D0=B5=D0=BB=D0=B8=D1=87?= =?UTF-8?q?=D0=B5=D0=BD=20=D1=82=D0=B0=D0=B9=D0=BC=D0=B0=D1=83=D1=82=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B8=20=D0=BC=D0=BE?= =?UTF-8?q?=D0=B4=D1=83=D0=BB=D0=B5=D0=B9=20requirejs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.js | 1 + 1 file changed, 1 insertion(+) diff --git a/main.js b/main.js index a2dd132..2a19ec9 100644 --- a/main.js +++ b/main.js @@ -1,6 +1,7 @@ 'use strict'; require.config({ + waitSeconds : 30, paths : { 'jquery' : '//yandex.st/jquery/1.10.2/jquery.min', 'd3' : 'http://d3js.org/d3.v3.min', From 3f0ba8d75905056ff5233110a290b930b9a0d033 Mon Sep 17 00:00:00 2001 From: Anton Zhevak Date: Tue, 5 Nov 2013 02:03:27 +0600 Subject: [PATCH 11/15] =?UTF-8?q?=D0=BA=D0=BE=D1=81=D0=BC=D0=B5=D1=82?= =?UTF-8?q?=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=B8=D0=B5=20=D0=B8=D0=B7=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app.js b/app.js index 3a2357b..583f3ea 100644 --- a/app.js +++ b/app.js @@ -20,7 +20,7 @@ define([ // Извлекаем имена пользователей, с которых мы начнём строить дерево this.startUsernames = this.getStartUsernames(config.userCount); $(document.body).html('') - .css({ 'margin' : 0 }) + .css({ margin : 0 }) .show(); this.usersUrl = config.usersUrl; @@ -37,10 +37,10 @@ define([ // Очередь детей, о которых надо получить информацию и добавить в дерево this.queue = []; - // Величина задержки между первым запросом и последним + // Количество запросов в очереди this.delay = 0; - // Величина паузы между запросами к серверу + // Величина паузы между запросами к серверу, мс this.timeout = 500; }; @@ -111,16 +111,17 @@ define([ * @return {Object} Deferred-объект */ App.prototype.findRoot = function(user) { + var d; // Если у пользователя есть родитель, значит пока он нас не интересует; // добавляем его в хранилище и запрашиваем информацию о его родителе if (user.parent) { var self = this; - var d = this.getUserInfo(user.parent).then(function(parent) { + d = this.getUserInfo(user.parent).then(function(parent) { return self.findRoot(parent); }); return d; } else { // Если пользователь зарегистрировался по приглашению НЛО - var d = $.Deferred(); + d = $.Deferred(); return d.resolve(user); } }; From 977ad4452b4ea0130d356d79fe37de84bbdf7fec Mon Sep 17 00:00:00 2001 From: Anton Zhevak Date: Tue, 5 Nov 2013 14:16:48 +0600 Subject: [PATCH 12/15] grunt --- Gruntfile.js | 36 ++++++++++++++++++++++++++++++++++++ app.js | 4 ++-- bookmarklet.js | 4 ++-- build/all.min.js | 1 + build/bookmarklet.min.js | 1 + package.json | 9 +++++++++ 6 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 Gruntfile.js create mode 100644 build/all.min.js create mode 100644 build/bookmarklet.min.js create mode 100644 package.json diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..0170c58 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,36 @@ +module.exports = function(grunt) { + + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + + uglify : { + main : { + options : { + banner : 'javascript:' + }, + files : { + 'build/bookmarklet.min.js' : 'bookmarklet.js' + } + } + }, + + requirejs : { + compile : { + options : { + name : 'main', + out : 'build/all.min.js', + paths : { + 'jquery' : 'empty:', + 'd3' : 'empty:' + } + } + } + } + }); + + grunt.loadNpmTasks('grunt-contrib-uglify'); + grunt.loadNpmTasks('grunt-contrib-requirejs'); + + grunt.registerTask('default', ['uglify', 'requirejs']); + +}; diff --git a/app.js b/app.js index 583f3ea..40042aa 100644 --- a/app.js +++ b/app.js @@ -113,7 +113,7 @@ define([ App.prototype.findRoot = function(user) { var d; // Если у пользователя есть родитель, значит пока он нас не интересует; - // добавляем его в хранилище и запрашиваем информацию о его родителе + // запрашиваем информацию о его родителе if (user.parent) { var self = this; d = this.getUserInfo(user.parent).then(function(parent) { @@ -141,7 +141,7 @@ define([ }; /** - * Получает информацию о запрашиваемом пользователе либо из хранилища, либо со страницы его профиля. + * Получает информацию о запрашиваемом пользователе из localStorage, либо со страницы его профиля. * * @param {string} username ник пользователя * @return {Object} Deferred-объект diff --git a/bookmarklet.js b/bookmarklet.js index b675042..1978e8f 100644 --- a/bookmarklet.js +++ b/bookmarklet.js @@ -1,8 +1,8 @@ -javascript:(function() { +(function() { var config = { require : '//cdnjs.cloudflare.com/ajax/libs/require.js/2.1.9/require.min.js', - start : 'https://rawgithub.com/mayton/5-async/master/main.js' + start : 'https://rawgithub.com/mayton/5-async/master/build/all.min.js' }; if (window.location.href !== 'http://habrahabr.ru/users/') { diff --git a/build/all.min.js b/build/all.min.js new file mode 100644 index 0000000..ea0b9f6 --- /dev/null +++ b/build/all.min.js @@ -0,0 +1 @@ +define("tree",["d3"],function(e){var t=function(t){t=t||{};var n=document.documentElement.clientWidth-20,r=document.documentElement.clientHeight-20;this.duration=t.duration||100,this.svg=e.select("body").append("svg").style("display","block").attr("width",n).attr("height",r).append("g").attr("transform","translate(10,10)"),this.tree=e.layout.tree().size([n-40,r-40]),this.root={},this.nodes=this.tree(this.root),this.root.parent=this.root,this.root.name="НЛО",this.root.children=[],this.root.px=this.root.x,this.root.py=this.root.y,this.root.avatar="/favicon.ico";var i=this;e.select(window).on("resize",function(){var t=document.documentElement.clientWidth-20,n=document.documentElement.clientHeight-20;e.select("svg").attr("width",t).attr("height",n),i.tree.size([t-40,n-40])})};return t.prototype.addNode=function(e){var t={name:e.name,avatar:e.avatar};if(e.parent===null)n=this.nodes[0];else for(var n,r=0,i=this.nodes.length;r0){var n=t.queue.shift();if(t.checkUser(n))return;t.getUserInfo(n).then(function(n){e.proxy(t.addUser(n),t)})}},100)},r.prototype.checkUser=function(e){return this.blackList.indexOf(e)>-1},r.prototype.addUser=function(e){this.tree.addNode(e),this.tree.update();if(e.children.length>0)for(var t=0,n=e.children.length;t0)for(var u=0,a=o.length;uДомашняя работа №5";var e=document.createElement("script");e.setAttribute("src",a.require),e.setAttribute("data-main",a.start),d.appendChild(e)}}(); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..de90ecc --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "name": "5-async-homework", + "version": "0.0.1", + "devDependencies": { + "grunt": "latest", + "grunt-contrib-requirejs": "latest", + "grunt-contrib-uglify": "latest" + } +} \ No newline at end of file From f02437c9fa0381e189f00a25d12de3be7fe945cc Mon Sep 17 00:00:00 2001 From: Anton Zhevak Date: Wed, 6 Nov 2013 14:42:14 +0600 Subject: [PATCH 13/15] =?UTF-8?q?ls=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B8=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE=D0=B2=D0=B0=D0=BD=20=D0=B2=20cache=20void=20?= =?UTF-8?q?=D0=B2=20=D0=B1=D1=83=D0=BA=D0=BC=D0=B0=D1=80=D0=BA=D0=BB=D0=B5?= =?UTF-8?q?=D1=82=D0=B5=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B2=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B0=D1=80=D0=B8=D1=8F=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 0 Gruntfile.js | 2 +- app.js | 43 +++++++++++++++++----------------------- build/all.min.js | 2 +- build/bookmarklet.min.js | 2 +- ls.js => cache.js | 19 ++++++++---------- main.js | 3 +-- package.json | 2 +- tree.js | 10 +++++++--- 9 files changed, 38 insertions(+), 45 deletions(-) create mode 100644 .gitignore rename ls.js => cache.js (63%) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/Gruntfile.js b/Gruntfile.js index 0170c58..8949604 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -6,7 +6,7 @@ module.exports = function(grunt) { uglify : { main : { options : { - banner : 'javascript:' + banner : 'javascript:void' }, files : { 'build/bookmarklet.min.js' : 'bookmarklet.js' diff --git a/app.js b/app.js index 40042aa..9d5f321 100644 --- a/app.js +++ b/app.js @@ -3,12 +3,11 @@ define([ 'jquery', 'tree', - 'ls' -], function($, Tree, LS) { + 'cache' +], function($, Tree, Cache) { /** - * Инициализирует приложение. - * + * Приложение. * @constructor * @param {Object} config настройки */ @@ -31,8 +30,8 @@ define([ // Визуализация дерева this.tree = new Tree; - // Работа с localStorage - this.ls = new LS; + // Работа с кешем + this.cache = new Cache; // Очередь детей, о которых надо получить информацию и добавить в дерево this.queue = []; @@ -63,14 +62,14 @@ define([ }); } - // Каждые полсекунды будем доставать из очереди пользователя + // Через определённые промежутки времени + // будем доставать из очереди пользователя setInterval(function() { if (self.queue.length > 0) { var u = self.queue.shift(); - - if (self.checkUser(u)) + if (self.checkUser(u)) { return; - + } // Получать о нём информацию и добавлять в дерево self.getUserInfo(u).then(function(user) { $.proxy(self.addUser(user), self); @@ -81,23 +80,20 @@ define([ /** * Проверяет, есть ли пользователь с указанным именем в чёрном списке. - * * @param {string} name имя пользователя - * @returns {boolean} + * @return {boolean} */ App.prototype.checkUser = function(name) { - return !! (this.blackList.indexOf(name) > -1); + return this.blackList.indexOf(name) > -1; }; /** * Добавляет пользователя в дерево, а имена его детей в очередь на обход. - * * @param {Object} user пользователь */ App.prototype.addUser = function(user) { this.tree.addNode(user); this.tree.update(); - if (user.children.length > 0) { for (var i = 0, l = user.children.length; i < l; i++) this.queue.push(user.children[i]); @@ -106,14 +102,12 @@ define([ /** * Ищет первого зарегистрировавшегося, с которого началась ветка. - * * @param {Object} user пользователь * @return {Object} Deferred-объект */ App.prototype.findRoot = function(user) { var d; - // Если у пользователя есть родитель, значит пока он нас не интересует; - // запрашиваем информацию о его родителе + // Если у пользователя есть родитель, значит запрашиваем информацию о его родителе if (user.parent) { var self = this; d = this.getUserInfo(user.parent).then(function(parent) { @@ -128,13 +122,14 @@ define([ /** * Находит на странице имена либо всех, либо первых n пользователей. - * * @param {number} n максимальное число имён пользователей * @return {Array} имена пользователей */ App.prototype.getStartUsernames = function(n) { - var users = $('.username a'); - for (var i = 0, usernames = [], n = n || users.length; i < n; i++) { + var users = $('.username a'), + usernames = []; + n = n || users.length; + for (var i = 0; i < n; i++) { usernames.push(users[i].innerHTML); } return usernames; @@ -142,14 +137,13 @@ define([ /** * Получает информацию о запрашиваемом пользователе из localStorage, либо со страницы его профиля. - * * @param {string} username ник пользователя * @return {Object} Deferred-объект */ App.prototype.getUserInfo = function(username) { var d = $.Deferred(), self = this, - user = this.ls.loadUser(username); + user = this.cache.loadUser(username); // Если пользователя нет в кеше, делаем запрос if (user === null) { @@ -158,7 +152,6 @@ define([ self.delay--; var user = {}; - user.name = username; // $(data).find('.user_header h2.username a').text(); user.avatar = $(data).find('.user_header .avatar img').attr('src'); user.parent = $(data).find('#invited-by').text() || null; @@ -172,7 +165,7 @@ define([ } // Кешируем в localStorage - self.ls.saveUser(user); + self.cache.saveUser(user); d.resolve(user); }); diff --git a/build/all.min.js b/build/all.min.js index ea0b9f6..7c6c8a8 100644 --- a/build/all.min.js +++ b/build/all.min.js @@ -1 +1 @@ -define("tree",["d3"],function(e){var t=function(t){t=t||{};var n=document.documentElement.clientWidth-20,r=document.documentElement.clientHeight-20;this.duration=t.duration||100,this.svg=e.select("body").append("svg").style("display","block").attr("width",n).attr("height",r).append("g").attr("transform","translate(10,10)"),this.tree=e.layout.tree().size([n-40,r-40]),this.root={},this.nodes=this.tree(this.root),this.root.parent=this.root,this.root.name="НЛО",this.root.children=[],this.root.px=this.root.x,this.root.py=this.root.y,this.root.avatar="/favicon.ico";var i=this;e.select(window).on("resize",function(){var t=document.documentElement.clientWidth-20,n=document.documentElement.clientHeight-20;e.select("svg").attr("width",t).attr("height",n),i.tree.size([t-40,n-40])})};return t.prototype.addNode=function(e){var t={name:e.name,avatar:e.avatar};if(e.parent===null)n=this.nodes[0];else for(var n,r=0,i=this.nodes.length;r0){var n=t.queue.shift();if(t.checkUser(n))return;t.getUserInfo(n).then(function(n){e.proxy(t.addUser(n),t)})}},100)},r.prototype.checkUser=function(e){return this.blackList.indexOf(e)>-1},r.prototype.addUser=function(e){this.tree.addNode(e),this.tree.update();if(e.children.length>0)for(var t=0,n=e.children.length;t0)for(var u=0,a=o.length;u0){var n=t.queue.shift();if(t.checkUser(n))return;t.getUserInfo(n).then(function(n){e.proxy(t.addUser(n),t)})}},100)},r.prototype.checkUser=function(e){return this.blackList.indexOf(e)>-1},r.prototype.addUser=function(e){this.tree.addNode(e),this.tree.update();if(e.children.length>0)for(var t=0,n=e.children.length;t0)for(var u=0,a=o.length;uДомашняя работа №5";var e=document.createElement("script");e.setAttribute("src",a.require),e.setAttribute("data-main",a.start),d.appendChild(e)}}(); \ No newline at end of file +javascript:void!function(){var a={require:"//cdnjs.cloudflare.com/ajax/libs/require.js/2.1.9/require.min.js",start:"https://rawgithub.com/mayton/5-async/master/build/all.min.js"};if("http://habrahabr.ru/users/"!==window.location.href)return alert("Для запуска сценария перейдите по адресу http://habrahabr.ru/users/"),void 0;var b=document.body;if(null!==b.getAttribute("count"))return alert("Для повторного запуска сценария необходимо обновить страницу."),void 0;var c=prompt("Сколько пользователей брать для анализа?",2);if(null!==c){b.setAttribute("count",c),b.style.display="none";var d=document.getElementsByTagName("head")[0];d.innerHTML="Домашняя работа №5";var e=document.createElement("script");e.setAttribute("src",a.require),e.setAttribute("data-main",a.start),d.appendChild(e)}}(); \ No newline at end of file diff --git a/ls.js b/cache.js similarity index 63% rename from ls.js rename to cache.js index 97a8d9f..f629df3 100644 --- a/ls.js +++ b/cache.js @@ -3,39 +3,36 @@ define(function() { /** - * Небольшой класс для упрощения работы с локальным хранилищем. - * + * Работа с локальным хранилищем. * @constructor */ - var LS = function() { + var Cache = function() { }; /** * Префикс для ключей с данными о пользователях. - * + * @const * @type {string} */ - LS.prototype.USER_PREFIX = 'habraUserv2.'; + Cache.prototype.USER_PREFIX = 'habraUserv2.'; /** * Получение пользователя из localStorage. - * * @param {Object} username пользователь - * @returns {Object} пользователь + * @return {Object} пользователь */ - LS.prototype.loadUser = function(username) { + Cache.prototype.loadUser = function(username) { return JSON.parse(window.localStorage.getItem(this.USER_PREFIX + username)); }; /** * Сохранение пользователя в localStorage. - * * @param {Object} user */ - LS.prototype.saveUser = function(user) { + Cache.prototype.saveUser = function(user) { window.localStorage.setItem(this.USER_PREFIX + user.name, JSON.stringify(user)); }; - return LS; + return Cache; }); diff --git a/main.js b/main.js index 2a19ec9..2c020de 100644 --- a/main.js +++ b/main.js @@ -6,7 +6,7 @@ require.config({ 'jquery' : '//yandex.st/jquery/1.10.2/jquery.min', 'd3' : 'http://d3js.org/d3.v3.min', 'app' : 'app', - 'ls' : 'ls', + 'cache' : 'cache', 'tree' : 'tree' }, shim: { @@ -23,7 +23,6 @@ require([ // Пользователи, которые портят граф или вообще всё var blackList = [ 'Ronnie83', // приглашён на сайт сразу двумя пользователями maovrn и shifttstas - 'Milla', // приглашён на сайт пользователем tangro 'tangro' // приглашён на сайт пользователем Milla ]; diff --git a/package.json b/package.json index de90ecc..195076e 100644 --- a/package.json +++ b/package.json @@ -6,4 +6,4 @@ "grunt-contrib-requirejs": "latest", "grunt-contrib-uglify": "latest" } -} \ No newline at end of file +} diff --git a/tree.js b/tree.js index 8b8afe6..487562a 100644 --- a/tree.js +++ b/tree.js @@ -4,6 +4,11 @@ define([ 'd3' ], function(d3) { + /** + * Графическое представление дерева. + * @constructor + * @param {Object} config настройки + */ var Tree = function(config) { config = config || {}; var w = document.documentElement.clientWidth - 20, @@ -45,9 +50,8 @@ define([ /** * Добавляет узел в дерево. - * * @param {Object} node узел - * @returns {boolean} результат добавления + * @return {boolean} результат добавления */ Tree.prototype.addNode = function(node) { var n = { @@ -103,7 +107,7 @@ define([ .attr('xlink:href', function(d) { return d.avatar; }) .attr('x', -12) .attr('y' -12) - .attr('opacity', 0.1) + .attr('opacity', .1) .attr('width', 24) .attr('height', 24); From de4ec9d02d95515c7497fde98d012cfbba8f5e4d Mon Sep 17 00:00:00 2001 From: Anton Zhevak Date: Wed, 6 Nov 2013 16:02:50 +0600 Subject: [PATCH 14/15] =?UTF-8?q?=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=20=D0=BA?= =?UTF-8?q?=D0=B5=D1=88=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 7 +++++-- build/all.min.js | 2 +- cache.js | 37 +++++++++++++++++++++++-------------- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/app.js b/app.js index 9d5f321..b4a6e70 100644 --- a/app.js +++ b/app.js @@ -41,6 +41,9 @@ define([ // Величина паузы между запросами к серверу, мс this.timeout = 500; + + // Префикс для кеширования данных о пользователях + this.USER_PREFIX = 'habraUserv2.'; }; /** @@ -143,7 +146,7 @@ define([ App.prototype.getUserInfo = function(username) { var d = $.Deferred(), self = this, - user = this.cache.loadUser(username); + user = this.cache.get(this.USER_PREFIX + username); // Если пользователя нет в кеше, делаем запрос if (user === null) { @@ -165,7 +168,7 @@ define([ } // Кешируем в localStorage - self.cache.saveUser(user); + self.cache.set(self.USER_PREFIX + user.name, user); d.resolve(user); }); diff --git a/build/all.min.js b/build/all.min.js index 7c6c8a8..9ac426f 100644 --- a/build/all.min.js +++ b/build/all.min.js @@ -1 +1 @@ -define("tree",["d3"],function(e){var t=function(t){t=t||{};var n=document.documentElement.clientWidth-20,r=document.documentElement.clientHeight-20;this.duration=t.duration||100,this.svg=e.select("body").append("svg").style("display","block").attr("width",n).attr("height",r).append("g").attr("transform","translate(10,10)"),this.tree=e.layout.tree().size([n-40,r-40]),this.root={},this.nodes=this.tree(this.root),this.root.parent=this.root,this.root.name="НЛО",this.root.children=[],this.root.px=this.root.x,this.root.py=this.root.y,this.root.avatar="/favicon.ico";var i=this;e.select(window).on("resize",function(){var t=document.documentElement.clientWidth-20,n=document.documentElement.clientHeight-20;e.select("svg").attr("width",t).attr("height",n),i.tree.size([t-40,n-40])})};return t.prototype.addNode=function(e){var t={name:e.name,avatar:e.avatar};if(e.parent===null)n=this.nodes[0];else for(var n,r=0,i=this.nodes.length;r0){var n=t.queue.shift();if(t.checkUser(n))return;t.getUserInfo(n).then(function(n){e.proxy(t.addUser(n),t)})}},100)},r.prototype.checkUser=function(e){return this.blackList.indexOf(e)>-1},r.prototype.addUser=function(e){this.tree.addNode(e),this.tree.update();if(e.children.length>0)for(var t=0,n=e.children.length;t0)for(var u=0,a=o.length;u0){var n=t.queue.shift();if(t.checkUser(n))return;t.getUserInfo(n).then(function(n){e.proxy(t.addUser(n),t)})}},100)},r.prototype.checkUser=function(e){return this.blackList.indexOf(e)>-1},r.prototype.addUser=function(e){this.tree.addNode(e),this.tree.update();if(e.children.length>0)for(var t=0,n=e.children.length;t0)for(var u=0,a=o.length;u Date: Sat, 9 Nov 2013 20:41:11 +0600 Subject: [PATCH 15/15] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=B2=D1=8B=D0=B2=D0=BE=D0=B4=20=D0=B8=D0=BD?= =?UTF-8?q?=D1=84=D1=8B=20=D0=BE=20=D1=82=D0=B5=D0=BA=D1=83=D1=89=D0=B5?= =?UTF-8?q?=D0=BC=20=D1=81=D0=BE=D1=81=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D0=B8?= =?UTF-8?q?=20=D0=B4=D0=B5=D0=BB=20=D0=B8=20=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE?= =?UTF-8?q?=D0=B6=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D0=BE=D1=87=D0=B8=D1=81?= =?UTF-8?q?=D1=82=D0=BA=D0=B8=20=D0=BA=D0=B5=D1=88=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 ++ Gruntfile.js | 7 +++-- app.js | 79 +++++++++++++++++++++++++++++++++++----------- build/all.min.js | 2 +- main.css | 82 ++++++++++++++++++++++++++++++++++++++++++++++++ main.html | 16 ++++++++++ main.js | 29 +++++++++++------ text.js | 1 + tree.js | 5 +-- 9 files changed, 189 insertions(+), 35 deletions(-) create mode 100644 main.css create mode 100644 main.html create mode 100644 text.js diff --git a/.gitignore b/.gitignore index e69de29..f0889a2 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +node_modules +.gitignore diff --git a/Gruntfile.js b/Gruntfile.js index 8949604..6f87aee 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -20,9 +20,10 @@ module.exports = function(grunt) { name : 'main', out : 'build/all.min.js', paths : { - 'jquery' : 'empty:', - 'd3' : 'empty:' - } + jquery : 'empty:', + d3 : 'empty:' + }, + exclude : ['text'] } } } diff --git a/app.js b/app.js index b4a6e70..022c193 100644 --- a/app.js +++ b/app.js @@ -1,10 +1,13 @@ 'use strict'; + define([ 'jquery', 'tree', - 'cache' -], function($, Tree, Cache) { + 'cache', + 'text!main.css', + 'text!main.html' +], function($, Tree, Cache, css, html) { /** * Приложение. @@ -12,20 +15,26 @@ define([ * @param {Object} config настройки */ var App = function(config) { - config = config || {}; - config.userCount = config.userCount || 2; - config.blackList = config.blackList || []; + this.config = config || {}; + this.config.userCount = config.userCount || 2; + this.config.blackList = config.blackList || []; // Извлекаем имена пользователей, с которых мы начнём строить дерево - this.startUsernames = this.getStartUsernames(config.userCount); - $(document.body).html('') - .css({ margin : 0 }) - .show(); + this.startUsernames = this.getStartUsernames(this.config.userCount); + $('head').append(''); + $(document.body).html(html).show(); + + this.config.gui = { + statusText : $(config.gui.statusText), + queueLength : $(config.gui.queueLength), + cacheLength : $(config.gui.cacheLength), + clearButton : $(config.gui.clearButton) + }; - this.usersUrl = config.usersUrl; + this.usersUrl = this.config.usersUrl; // Игнорируемые - this.blackList = config.blackList; + this.blackList = this.config.blackList; // Визуализация дерева this.tree = new Tree; @@ -44,6 +53,13 @@ define([ // Префикс для кеширования данных о пользователях this.USER_PREFIX = 'habraUserv2.'; + + var self = this; + this.config.gui.clearButton.on('click', function() { + self.cache.clear(); + self.updateGui(); + return false; + }); }; /** @@ -54,7 +70,7 @@ define([ // Ищем корни и добавляем их в дерево for (var i = 0, len = this.startUsernames.length; i < len; i++) { - if (self.checkUser(this.startUsernames[i])) + if (self.blackListCheck(this.startUsernames[i])) continue; this.getUserInfo(this.startUsernames[i]).then(function(user) { @@ -67,14 +83,17 @@ define([ // Через определённые промежутки времени // будем доставать из очереди пользователя - setInterval(function() { + var step = setInterval(function() { if (self.queue.length > 0) { - var u = self.queue.shift(); - if (self.checkUser(u)) { - return; - } + var username = self.queue.shift(); + + // Некоторые пользователи (BarsMonster) приглашали других дважды (grokru) + if (self.queue.indexOf(username) > -1) return; + + if (self.blackListCheck(username)) return; + // Получать о нём информацию и добавлять в дерево - self.getUserInfo(u).then(function(user) { + self.getUserInfo(username).then(function(user) { $.proxy(self.addUser(user), self); }); } @@ -86,7 +105,7 @@ define([ * @param {string} name имя пользователя * @return {boolean} */ - App.prototype.checkUser = function(name) { + App.prototype.blackListCheck = function(name) { return this.blackList.indexOf(name) > -1; }; @@ -170,6 +189,8 @@ define([ // Кешируем в localStorage self.cache.set(self.USER_PREFIX + user.name, user); + self.updateGui(); + d.resolve(user); }); }, this.delay++ * this.timeout); @@ -177,9 +198,29 @@ define([ d.resolve(user); } + this.updateGui(); + return d; }; + /** + * Обновляет информацию на экране о текущем состоянии дел. + */ + App.prototype.updateGui = function() { + var queue = this.delay + this.queue.length; + + this.config.gui.cacheLength.text(this.cache.count()); + if (queue > 0) { + var status = 'Идёт сканирование'; + $(document.body).removeClass('ready'); + } else { + var status = 'Дерево построено'; + $(document.body).addClass('ready'); + } + this.config.gui.statusText.text(status); + this.config.gui.queueLength.text(queue); + }; + return App; }); \ No newline at end of file diff --git a/build/all.min.js b/build/all.min.js index 9ac426f..b1d715d 100644 --- a/build/all.min.js +++ b/build/all.min.js @@ -1 +1 @@ -define("tree",["d3"],function(e){var t=function(t){t=t||{};var n=document.documentElement.clientWidth-20,r=document.documentElement.clientHeight-20;this.duration=t.duration||100,this.svg=e.select("body").append("svg").style("display","block").attr("width",n).attr("height",r).append("g").attr("transform","translate(10,10)"),this.tree=e.layout.tree().size([n-40,r-40]),this.root={},this.nodes=this.tree(this.root),this.root.parent=this.root,this.root.name="НЛО",this.root.children=[],this.root.px=this.root.x,this.root.py=this.root.y,this.root.avatar="/favicon.ico";var i=this;e.select(window).on("resize",function(){var t=document.documentElement.clientWidth-20,n=document.documentElement.clientHeight-20;e.select("svg").attr("width",t).attr("height",n),i.tree.size([t-40,n-40])})};return t.prototype.addNode=function(e){var t={name:e.name,avatar:e.avatar};if(e.parent===null)n=this.nodes[0];else for(var n,r=0,i=this.nodes.length;r0){var n=t.queue.shift();if(t.checkUser(n))return;t.getUserInfo(n).then(function(n){e.proxy(t.addUser(n),t)})}},100)},r.prototype.checkUser=function(e){return this.blackList.indexOf(e)>-1},r.prototype.addUser=function(e){this.tree.addNode(e),this.tree.update();if(e.children.length>0)for(var t=0,n=e.children.length;t0)for(var u=0,a=o.length;u\n
\n Состояние:\n \n \n
\n
\n Пользователей в очереди:\n \n
\n
\n Пользователей в кеше:\n \n Очистить\n
\n'}),define("app",["jquery","tree","cache","text!main.css","text!main.html"],function(e,t,n,r,i){var s=function(s){this.config=s||{},this.config.userCount=s.userCount||2,this.config.blackList=s.blackList||[],this.startUsernames=this.getStartUsernames(this.config.userCount),e("head").append(""),e(document.body).html(i).show(),this.config.gui={statusText:e(s.gui.statusText),queueLength:e(s.gui.queueLength),cacheLength:e(s.gui.cacheLength),clearButton:e(s.gui.clearButton)},this.usersUrl=this.config.usersUrl,this.blackList=this.config.blackList,this.tree=new t,this.cache=new n,this.queue=[],this.delay=0,this.timeout=500,this.USER_PREFIX="habraUserv2.";var o=this;this.config.gui.clearButton.on("click",function(){return o.cache.clear(),o.updateGui(),!1})};return s.prototype.start=function(){var t=this;for(var n=0,r=this.startUsernames.length;n0){var n=t.queue.shift();if(t.queue.indexOf(n)>-1)return;if(t.blackListCheck(n))return;t.getUserInfo(n).then(function(n){e.proxy(t.addUser(n),t)})}},100)},s.prototype.blackListCheck=function(e){return this.blackList.indexOf(e)>-1},s.prototype.addUser=function(e){this.tree.addNode(e),this.tree.update();if(e.children.length>0)for(var t=0,n=e.children.length;t0)for(var u=0,a=o.length;u0){var n="Идёт сканирование";e(document.body).removeClass("ready")}else{var n="Дерево построено";e(document.body).addClass("ready")}this.config.gui.statusText.text(n),this.config.gui.queueLength.text(t)},s}),require.config({waitSeconds:30,paths:{text:"//cdnjs.cloudflare.com/ajax/libs/require-text/2.0.10/text.min",jquery:"//yandex.st/jquery/1.10.2/jquery.min",d3:"http://d3js.org/d3.v3.min",app:"app",cache:"cache",tree:"tree"},shim:{d3:{exports:"d3"}}}),require(["jquery","app"],function(e,t){var n=["Ronnie83","Milla","tangro"],r=new t({usersUrl:"/users/",blackList:n,userCount:+document.body.getAttribute("count"),gui:{statusText:"#status",queueLength:"#queue-length",cacheLength:"#cache-length",clearButton:"#clear-cache"}});r.start()}),define("main",function(){}); \ No newline at end of file diff --git a/main.css b/main.css new file mode 100644 index 0000000..68c49a9 --- /dev/null +++ b/main.css @@ -0,0 +1,82 @@ +html, +body +{ + font: 14px/normal sans-serif; + margin: 0; + height: 100%; + min-width: 800px; +} + +.dashboard +{ + position: absolute; + top: 0; + left: 0; + line-height: 30px; + border-bottom: 1px solid #ccc; + border-right: 1px solid #ccc; +} + +.group +{ + margin: 0 10px; +} + +a +{ + color: blue; + cursor: pointer; + text-decoration: none; + border-bottom: 1px dotted blue; +} + +.ready .ajax-loader +{ + display: none; +} + +.ajax-loader +{ + width: 10px; + height: 10px; + display: inline-block; + vertical-align: middle; + border: 4px solid #666; + border-right-color: transparent; + border-radius: 50%; + -webkit-animation: spin 1s linear infinite; + -moz-animation: spin 1s linear infinite; + -ms-animation: spin 1s linear infinite; + -o-animation: spin 1s linear infinite; + animation: spin 1s linear infinite; +} + +@-webkit-keyframes spin +{ + from { -webkit-transform: rotate(0deg); } + to { -webkit-transform: rotate(360deg); } +} + +@-moz-keyframes spin +{ + from { -moz-transform: rotate(0deg); } + to { -moz-transform: rotate(360deg); } +} + +@-ms-keyframes spin +{ + from { -ms-transform: rotate(0deg); } + to { -ms-transform: rotate(360deg); } +} + +@-o-keyframes spin +{ + from { -o-transform: rotate(0deg); } + to { -o-transform: rotate(360deg); } +} + +@keyframes spin +{ + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} \ No newline at end of file diff --git a/main.html b/main.html new file mode 100644 index 0000000..82a0e3e --- /dev/null +++ b/main.html @@ -0,0 +1,16 @@ +
+
+ Состояние: + + +
+
+ Пользователей в очереди: + +
+
+ Пользователей в кеше: + + Очистить +
+
\ No newline at end of file diff --git a/main.js b/main.js index 2c020de..e9a0b15 100644 --- a/main.js +++ b/main.js @@ -3,22 +3,24 @@ require.config({ waitSeconds : 30, paths : { - 'jquery' : '//yandex.st/jquery/1.10.2/jquery.min', - 'd3' : 'http://d3js.org/d3.v3.min', - 'app' : 'app', - 'cache' : 'cache', - 'tree' : 'tree' + text : '//cdnjs.cloudflare.com/ajax/libs/require-text/2.0.10/text.min', + jquery : '//yandex.st/jquery/1.10.2/jquery.min', + d3 : 'http://d3js.org/d3.v3.min', + app : 'app', + cache : 'cache', + tree : 'tree' }, - shim: { - d3: { - exports : 'd3' + shim : { + d3 : { + exports : 'd3' } } }); require([ + 'jquery', 'app' -], function(App) { +], function($, App) { // Пользователи, которые портят граф или вообще всё var blackList = [ @@ -30,8 +32,15 @@ require([ var app = new App({ usersUrl : '/users/', blackList : blackList, - userCount : +document.body.getAttribute('count') + userCount : +document.body.getAttribute('count'), + gui : { + statusText : '#status', + queueLength : '#queue-length', + cacheLength : '#cache-length', + clearButton : '#clear-cache' + } }); + app.start(); }); \ No newline at end of file diff --git a/text.js b/text.js new file mode 100644 index 0000000..143168e --- /dev/null +++ b/text.js @@ -0,0 +1 @@ +define(["module"],function(a){"use strict";var b,c,d,e,f,g=["Msxml2.XMLHTTP","Microsoft.XMLHTTP","Msxml2.XMLHTTP.4.0"],h=/^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im,i=/]*>\s*([\s\S]+)\s*<\/body>/im,j="undefined"!=typeof location&&location.href,k=j&&location.protocol&&location.protocol.replace(/\:/,""),l=j&&location.hostname,m=j&&(location.port||void 0),n={},o=a.config&&a.config()||{};return b={version:"2.0.10",strip:function(a){if(a){a=a.replace(h,"");var b=a.match(i);b&&(a=b[1])}else a="";return a},jsEscape:function(a){return a.replace(/(['\\])/g,"\\$1").replace(/[\f]/g,"\\f").replace(/[\b]/g,"\\b").replace(/[\n]/g,"\\n").replace(/[\t]/g,"\\t").replace(/[\r]/g,"\\r").replace(/[\u2028]/g,"\\u2028").replace(/[\u2029]/g,"\\u2029")},createXhr:o.createXhr||function(){var a,b,c;if("undefined"!=typeof XMLHttpRequest)return new XMLHttpRequest;if("undefined"!=typeof ActiveXObject)for(b=0;3>b;b+=1){c=g[b];try{a=new ActiveXObject(c)}catch(d){}if(a){g=[c];break}}return a},parseName:function(a){var b,c,d,e=!1,f=a.indexOf("."),g=0===a.indexOf("./")||0===a.indexOf("../");return-1!==f&&(!g||f>1)?(b=a.substring(0,f),c=a.substring(f+1,a.length)):b=a,d=c||b,f=d.indexOf("!"),-1!==f&&(e="strip"===d.substring(f+1),d=d.substring(0,f),c?c=d:b=d),{moduleName:b,ext:c,strip:e}},xdRegExp:/^((\w+)\:)?\/\/([^\/\\]+)/,useXhr:function(a,c,d,e){var f,g,h,i=b.xdRegExp.exec(a);return i?(f=i[2],g=i[3],g=g.split(":"),h=g[1],g=g[0],!(f&&f!==c||g&&g.toLowerCase()!==d.toLowerCase()||(h||g)&&h!==e)):!0},finishLoad:function(a,c,d,e){d=c?b.strip(d):d,o.isBuild&&(n[a]=d),e(d)},load:function(a,c,d,e){if(e.isBuild&&!e.inlineText)return d(),void 0;o.isBuild=e.isBuild;var f=b.parseName(a),g=f.moduleName+(f.ext?"."+f.ext:""),h=c.toUrl(g),i=o.useXhr||b.useXhr;return 0===h.indexOf("empty:")?(d(),void 0):(!j||i(h,k,l,m)?b.get(h,function(c){b.finishLoad(a,f.strip,c,d)},function(a){d.error&&d.error(a)}):c([g],function(a){b.finishLoad(f.moduleName+"."+f.ext,f.strip,a,d)}),void 0)},write:function(a,c,d){if(n.hasOwnProperty(c)){var f=b.jsEscape(n[c]);d.asModule(a+"!"+c,"define(function () { return '"+f+"';});\n")}},writeFile:function(a,c,d,e,f){var g=b.parseName(c),h=g.ext?"."+g.ext:"",i=g.moduleName+h,j=d.toUrl(g.moduleName+h)+".js";b.load(i,d,function(){var d=function(a){return e(j,a)};d.asModule=function(a,b){return e.asModule(a,j,b)},b.write(a,i,d,f)},f)}},"node"===o.env||!o.env&&"undefined"!=typeof process&&process.versions&&process.versions.node&&!process.versions["node-webkit"]?(c=require.nodeRequire("fs"),b.get=function(a,b,d){try{var e=c.readFileSync(a,"utf8");0===e.indexOf("\ufeff")&&(e=e.substring(1)),b(e)}catch(f){d(f)}}):"xhr"===o.env||!o.env&&b.createXhr()?b.get=function(a,c,d,e){var g,f=b.createXhr();if(f.open("GET",a,!0),e)for(g in e)e.hasOwnProperty(g)&&f.setRequestHeader(g.toLowerCase(),e[g]);o.onXhr&&o.onXhr(f,a),f.onreadystatechange=function(){var e,g;4===f.readyState&&(e=f.status,e>399&&600>e?(g=new Error(a+" HTTP status: "+e),g.xhr=f,d(g)):c(f.responseText),o.onXhrComplete&&o.onXhrComplete(f,a))},f.send(null)}:"rhino"===o.env||!o.env&&"undefined"!=typeof Packages&&"undefined"!=typeof java?b.get=function(a,b){var c,d,e="utf-8",f=new java.io.File(a),g=java.lang.System.getProperty("line.separator"),h=new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(f),e)),i="";try{for(c=new java.lang.StringBuffer,d=h.readLine(),d&&d.length()&&65279===d.charAt(0)&&(d=d.substring(1)),null!==d&&c.append(d);null!==(d=h.readLine());)c.append(g),c.append(d);i=String(c.toString())}finally{h.close()}b(i)}:("xpconnect"===o.env||!o.env&&"undefined"!=typeof Components&&Components.classes&&Components.interfaces)&&(d=Components.classes,e=Components.interfaces,Components.utils["import"]("resource://gre/modules/FileUtils.jsm"),f="@mozilla.org/windows-registry-key;1"in d,b.get=function(a,b){var c,g,h,i={};f&&(a=a.replace(/\//g,"\\")),h=new FileUtils.File(a);try{c=d["@mozilla.org/network/file-input-stream;1"].createInstance(e.nsIFileInputStream),c.init(h,1,0,!1),g=d["@mozilla.org/intl/converter-input-stream;1"].createInstance(e.nsIConverterInputStream),g.init(c,"utf-8",c.available(),e.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER),g.readString(c.available(),i),g.close(),c.close(),b(i.value)}catch(j){throw new Error((h&&h.path||"")+": "+j)}}),b}); \ No newline at end of file diff --git a/tree.js b/tree.js index 487562a..e800b46 100644 --- a/tree.js +++ b/tree.js @@ -59,10 +59,11 @@ define([ avatar : node.avatar }; + var parent; if (node.parent === null) { parent = this.nodes[0]; } else { - for (var parent, i = 0, l = this.nodes.length; i < l; i++) { + for (var i = 0, l = this.nodes.length; i < l; i++) { if (this.nodes[i].name === node.parent) { parent = this.nodes[i]; break; @@ -94,7 +95,7 @@ define([ var node = this.svg.selectAll('.node') .data(this.tree.nodes(this.root), function(d) { return d.name; }); var link = this.svg.selectAll('.link') - .data(this.tree.links(this.nodes), function(d) { return d.source.name + '-' + d.target.name; }); + .data(this.tree.links(this.nodes), function(d) { return d.target.name; }); var nodeEnter = node.enter() .append('g')