Index: externals/raphael/g.raphael.dot.js =================================================================== --- /dev/null +++ externals/raphael/g.raphael.dot.js @@ -0,0 +1,13 @@ +/** + * @requires raphael-core + * @requires raphael-g + * @provides raphael-g-dot + * @do-not-minify + */ +/*! + * g.Raphael 0.5 - Charting library, based on Raphaƫl + * + * Copyright (c) 2009 Dmitry Baranovskiy (http://g.raphaeljs.com) + * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license. + */ +(function(){var b=function(g,f,e,d){return"hsb("+[Math.min((1-g/f)*0.4,1),e||0.75,d||0.75]+")"};function a(e,N,M,d,j,B,A,t,I){var v=this;function U(g){+g[0]&&(g[0]=v.axis(N+s,M+s,d-2*s,E,p,I.axisxstep||Math.floor((d-2*s)/20),2,I.axisxlabels||null,I.axisxtype||"t",null,e));+g[1]&&(g[1]=v.axis(N+d-s,M+j-s,j-2*s,D,o,I.axisystep||Math.floor((j-2*s)/20),3,I.axisylabels||null,I.axisytype||"t",null,e));+g[2]&&(g[2]=v.axis(N+s,M+j-s+H,d-2*s,E,p,I.axisxstep||Math.floor((d-2*s)/20),0,I.axisxlabels||null,I.axisxtype||"t",null,e));+g[3]&&(g[3]=v.axis(N+s-H,M+j-s,j-2*s,D,o,I.axisystep||Math.floor((j-2*s)/20),1,I.axisylabels||null,I.axisytype||"t",null,e))}I=I||{};var z=v.snapEnds(Math.min.apply(Math,B),Math.max.apply(Math,B),B.length-1),E=z.from,p=z.to,s=I.gutter||10,L=v.snapEnds(Math.min.apply(Math,A),Math.max.apply(Math,A),A.length-1),D=L.from,o=L.to,C=Math.max(B.length,A.length,t.length),w=e[I.symbol]||"circle",J=e.set(),u=e.set(),G=I.max||100,r=Math.max.apply(Math,t),q=[],Q=Math.sqrt(r/Math.PI)*2/G;for(var S=0;S '/rsrc/js/application/projects/behavior-project-create.js', ), + 'javelin-behavior-punchcard' => + array( + 'uri' => '/res/2d086de7/rsrc/js/application/people/behavior_punchcard.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-vector', + 3 => 'raphael-g-dot', + ), + 'disk' => '/rsrc/js/application/people/behavior_punchcard.js', + ), 'javelin-behavior-refresh-csrf' => array( 'uri' => '/res/88beba4c/rsrc/js/application/core/behavior-refresh-csrf.js', @@ -2885,6 +2898,17 @@ ), 'disk' => '/rsrc/js/raphael/g.raphael.js', ), + 'raphael-g-dot' => + array( + 'uri' => '/res/12eb50ec/rsrc/js/raphael/g.raphael.dot.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'raphael-core', + 1 => 'raphael-g', + ), + 'disk' => '/rsrc/js/raphael/g.raphael.dot.js', + ), 'raphael-g-line' => array( 'uri' => '/res/a59c8556/rsrc/js/raphael/g.raphael.line.js', Index: src/applications/people/controller/PhabricatorPeopleProfileController.php =================================================================== --- src/applications/people/controller/PhabricatorPeopleProfileController.php +++ src/applications/people/controller/PhabricatorPeopleProfileController.php @@ -206,22 +206,107 @@ '; + $content .= $this->renderActivityAnalysis($user); return $content; } - private function renderUserFeed(PhabricatorUser $user) { - $viewer = $this->getRequest()->getUser(); + private function renderActivityAnalysis(PhabricatorUser $user) { + + $stories = $this->getStories($user, 500); + $counts = $this->countStories($stories); + + $behavior = $this->analyzeActivity($counts); + switch ($behavior) { + case self::DIURNAL: + $behavior_str = "//This creature appears to be diurnal.//"; + break; + case self::NOCTURNAL; + $behavior_str = "//This creature appears to be nocturnal.//"; + break; + case self::CATHEMERAL; + $behavior_str = "//This creature appears to be " . + "[[http://en.wikipedia.org/wiki/Cathemeral | cathemeral]].//"; + break; + } + $engine = PhabricatorMarkupEngine::newProfileMarkupEngine(); + $behavior_str = $engine->markupText($behavior_str); - $query = new PhabricatorFeedQuery(); - $query->setFilterPHIDs( + $id = celerity_generate_unique_node_id(); + $punchcard_node = phutil_render_tag( + 'div', array( - $user->getPHID(), - )); - $query->setLimit(100); - $query->setViewer($viewer); - $stories = $query->execute(); + 'id' => $id, + 'style' => 'border: 1px solid #6f6f6f; '. + 'margin: 1em 2em; '. + 'height: 280px; '. + 'width: 700px; ', + ), + ''); + + require_celerity_resource('javelin-behavior-punchcard'); + + Javelin::initBehavior('punchcard', array( + 'hardpoint' => $id, + 'counts' => $counts, + )); + + return + '
+

Sightings In The Wild

+
+ + + + + +
Behavior Pattern'.$behavior_str.'
+
+
+ '.$punchcard_node; + } + + // Tabulate stories by day of week and hour of day. + private function countStories($stories) { + + $counts = array_fill(1, 7, array_fill(0, 24, 0)); + foreach ($stories as $story) { + $epoch = $story->getEpoch(); + ++$counts[intval(date('N', $epoch))][intval(date('G', $epoch))]; + } + return $counts; + } + + const DIURNAL = 1; + const NOCTURNAL = 2; + const CATHEMERAL = 3; + + private function analyzeActivity($counts) { + $day_count = 0; + $night_count = 0; + foreach ($counts as $day) { + foreach ($day as $hour => $count) { + if (($hour >= 8) && ($hour < 20)) { + $day_count += $count; + } else { + $night_count += $count; + } + } + } + + if ($day_count / ($day_count + $night_count) > 0.66) { + return self::DIURNAL; + } else if ($day_count / ($day_count + $night_count) < 0.66) { + return self::NOCTURNAL; + } else { + return self::CATHEMERAL; + } + } + + private function renderUserFeed(PhabricatorUser $user) { + $viewer = $this->getRequest()->getUser(); + $stories = $this->getStories($user, 100); $builder = new PhabricatorFeedBuilder($stories); $builder->setUser($viewer); $view = $builder->buildView(); @@ -234,4 +319,17 @@ '; } + + private function getStories(PhabricatorUser $user, $limit) { + $viewer = $this->getRequest()->getUser(); + + $query = new PhabricatorFeedQuery(); + $query->setFilterPHIDs( + array( + $user->getPHID(), + )); + $query->setLimit($limit); + $query->setViewer($viewer); + return $query->execute(); + } } Index: webroot/rsrc/js/application/people/behavior_punchcard.js =================================================================== --- /dev/null +++ webroot/rsrc/js/application/people/behavior_punchcard.js @@ -0,0 +1,58 @@ +/** + * @provides javelin-behavior-punchcard + * @requires javelin-behavior + * javelin-dom + * javelin-vector + * raphael-g-dot + */ + +JX.behavior('punchcard', function(config) { + + var h = JX.$(config.hardpoint); + var p = JX.Vector.getPos(h); + var d = JX.Vector.getDim(h); + var mx = 10; + var my = 10; + + var r = Raphael(p.x, p.y, d.x, d.y); + // TODO(mshang): Internationalize this. + var axisy = ["Sun", "Sat", "Fri", "Thu", "Wed", "Tue", "Mon"]; + var axisx = ["12a", "1a", "2a", "3a", "4a", "5a", "6a", "7a", + "8a", "9a", "10a", "11a", "12p", "1p", "2p", "3p", + "4p", "5p", "6p", "7p", "8p", "9p", "10p", "11p"]; + + var counts = []; + var days = []; + var hours = []; + + for (var ii in config.counts) { + for (var jj = 0; jj < config.counts[ii].length; jj++) { + counts = counts.concat(config.counts[ii][jj]); + days = days.concat(-parseInt(ii)); + hours = hours.concat(jj); + } + } + + r.dotchart( + mx, + my, + d.x - (2 * mx), + d.y - (2 * my), + hours, + days, + counts, + { + symbol: "o", + max: 10, + heat: true, + axis: "0 0 1 1", + axisxstep: 23, + axisystep: 6, + axisxlabels: axisx, + axisxtype: " ", + axisylabels: axisy, + axisytype: " ", + } + ); +}); +