crmMailingAB - Transplant report. Decouple stats API.
authorTim Otten <totten@civicrm.org>
Sat, 20 Dec 2014 09:32:21 +0000 (01:32 -0800)
committerTim Otten <totten@civicrm.org>
Sat, 20 Dec 2014 12:52:29 +0000 (04:52 -0800)
Note: Previously, the MailingAB.graph_stats API keyed off the pre-declared
"winning criteria" and target date, which made it necessary to choose these
things in advance.  This API now accepts these as parameters which can be
tweaked interactively (based on what the user whats to investigate).

The graphs don't really work right now, but I've transplanted the code to
show how to encapsulate it in a directive.

api/v3/MailingAB.php
css/angular-crmMailingAB.css
js/angular-crmMailingAB.js
js/angular-crmMailingAB/directives.js
partials/crmMailingAB/report.html [new file with mode: 0644]

index 7d55872f2d91d712ee637d41927bc78784a2b717..8b66a562ea1f0dbb106448b1ca269d16a5ca6618 100755 (executable)
@@ -171,10 +171,16 @@ function civicrm_api3_mailing_a_b_submit($params) {
  * @param array $params array or parameters determined by getfields
  */
 function _civicrm_api3_mailing_a_b_graph_stats_spec(&$params) {
+  $params['criteria']['title'] = 'Criteria';
+  $params['criteria']['default'] = 'Open';
+  // mailing_ab_winner_criteria
+  $params['target_date']['title'] = 'Target Date';
+  $params['target_date']['type'] = CRM_Utils_Type::T_DATE + CRM_Utils_Type::T_TIME;
   $params['split_count']['title'] = 'Split Count';
   $params['split_count']['api.default'] = 6;
   $params['split_count_select']['title'] = 'Split Count Select';
   $params['split_count_select']['api.required'] = 1;
+  $params['target_url']['title'] = 'Target URL';
 }
 
 /**
@@ -182,6 +188,7 @@ function _civicrm_api3_mailing_a_b_graph_stats_spec(&$params) {
  *
  * @param array $params
  * @return array
+ * @throws API_Exception
  */
 function civicrm_api3_mailing_a_b_graph_stats($params) {
   civicrm_api3_verify_mandatory($params,
@@ -190,26 +197,25 @@ function civicrm_api3_mailing_a_b_graph_stats($params) {
     FALSE
   );
 
-  $mailingAB = civicrm_api3('MailingAB', 'get', array('id' => $params['id']));
-  $mailingAB = $mailingAB['values'][$params['id']];
-
-  $optionGroupValue = civicrm_api3('OptionValue', 'get', array(
-    'option_group_name' => 'mailing_ab_winner_criteria',
-    'value' => $mailingAB['winner_criteria_id']
-  ));
-  $winningCriteria = $optionGroupValue['values'][$optionGroupValue['id']]['name'];
-
-  $declareWinnerDate = CRM_Utils_Date::processDate($mailingAB['declare_winning_time']);
+  $defaults = array(
+    'criteria' => 'Open',
+    'target_date' => CRM_Utils_Time::getTime('YmdHis'),
+    'split_count' => 6,
+    'split_count_select' => 1,
+  );
+  $params = array_merge($defaults, $params);
 
+  $mailingAB = civicrm_api3('MailingAB', 'getsingle', array('id' => $params['id']));
   $graphStats = array();
   $ABFormat = array('A' => 'mailing_id_a', 'B' => 'mailing_id_b');
 
   foreach ($ABFormat as $name => $column) {
-    switch ($winningCriteria) {
-      case 'Open':
+    switch (strtolower($params['criteria'])) {
+      case 'open':
         $result = CRM_Mailing_Event_BAO_Opened::getRows($mailingAB['mailing_id_a'], NULL, TRUE, 0, 1, "civicrm_mailing_event_opened.time_stamp ASC");
         $startDate = CRM_Utils_Date::processDate($result[0]['date']);
-        $dateDuration = round(round(strtotime($declareWinnerDate) - strtotime($startDate)) / $params['split_count']);
+        $targetDate = CRM_Utils_Date::processDate($params['target_date']);
+        $dateDuration = round(round(strtotime($targetDate) - strtotime($startDate)) / $params['split_count']);
         $toDate = strtotime($startDate) + ($dateDuration * $params['split_count_select']);
         $toDate = date('YmdHis', $toDate);
         $graphStats[$name] = array(
@@ -219,10 +225,11 @@ function civicrm_api3_mailing_a_b_graph_stats($params) {
           )
         );
         break;
-      case 'Total Unique Clicks':
+      case 'total unique clicks':
         $result = CRM_Mailing_Event_BAO_TrackableURLOpen::getRows($mailingAB['mailing_id_a'], NULL, TRUE, 0, 1, "civicrm_mailing_event_trackable_url_open.time_stamp ASC");
         $startDate = CRM_Utils_Date::processDate($result[0]['date']);
-        $dateDuration = round(abs(strtotime($declareWinnerDate) - strtotime($startDate)) / $params['split_count']);
+        $targetDate = CRM_Utils_Date::processDate($params['target_date']);
+        $dateDuration = round(abs(strtotime($targetDate) - strtotime($startDate)) / $params['split_count']);
         $toDate = strtotime($startDate) + ($dateDuration * $params['split_count_select']);
         $toDate = date('YmdHis', $toDate);
         $graphStats[$name] = array(
@@ -232,14 +239,16 @@ function civicrm_api3_mailing_a_b_graph_stats($params) {
           )
         );
         break;
-      case 'Total Clicks on a particular link':
-        if (empty($params['url'])) {
-          throw new API_Exception("Provide url to get stats result for '{$winningCriteria}'");
+      case 'total clicks on a particular link':
+        if (empty($params['target_url'])) {
+          throw new API_Exception("Provide url to get stats result for total clicks on a particular link");
         }
-        $url_id = CRM_Mailing_BAO_TrackableURL::getTrackerURLId($mailingAB[$column], $params['url']);
+        // FIXME: doesn't make sense to get url_id mailing_id_(a|b) while getting start date in mailing_id_a
+        $url_id = CRM_Mailing_BAO_TrackableURL::getTrackerURLId($mailingAB[$column], $params['target_url']);
         $result = CRM_Mailing_Event_BAO_TrackableURLOpen::getRows($mailingAB['mailing_id_a'], NULL, FALSE, $url_id, 0, 1, "civicrm_mailing_event_trackable_url_open.time_stamp ASC");
         $startDate = CRM_Utils_Date::processDate($result[0]['date']);
-        $dateDuration = round(abs(strtotime($declareWinnerDate) - strtotime($startDate)) / $params['split_count']);
+        $targetDate = CRM_Utils_Date::processDate($params['target_date']);
+        $dateDuration = round(abs(strtotime($targetDate) - strtotime($startDate)) / $params['split_count']);
         $toDate = strtotime($startDate) + ($dateDuration * $params['split_count_select']);
         $toDate = CRM_Utils_Date::processDate($toDate);
         $graphStats[$name] = array(
index 19a25128d551cd922475758f458c13705590e4f8..80d33411218c68b480eb68bdfe3ebc8349a9217e 100644 (file)
@@ -1,6 +1,17 @@
 .crm-mailing-ab-slider .slider-test .ui-slider-range {
     background: #5050b0;
 }
+
 .crm-mailing-ab-slider .slider-win .ui-slider-range {
     background: #50b050;
 }
+
+.crm-mailing-ab-stats .series {
+    fill: none;
+}
+
+.crm-mailing-ab-stats .axis path, .crm-mailing-ab-stats .axis line {
+    fill: none;
+    stroke: #000;
+    shape-rendering: crispEdges;
+}
index ea8839f2938259165d3ec72fcc500059f580dca3..6dc3d12cd215c60fd5fdddeb953dd3b0ab834e2a 100644 (file)
@@ -7,7 +7,7 @@
     return CRM.resourceUrls['civicrm'] + '/partials/' + module + '/' + relPath;
   };
 
-  angular.module('crmMailingAB', ['ngRoute', 'ui.utils', 'ngSanitize', 'crmUi', 'crmAttachment', 'crmMailing']);
+  angular.module('crmMailingAB', ['ngRoute', 'ui.utils', 'ngSanitize', 'crmUi', 'crmAttachment', 'crmMailing', 'crmD3']);
   angular.module('crmMailingAB').config([
     '$routeProvider',
     function ($routeProvider) {
           }
         }
       });
+      $routeProvider.when('/abtest/:id/report', {
+        templateUrl: partialUrl('report.html'),
+        controller: 'CrmMailingABReportCtrl',
+        resolve: {
+          abtest: function ($route, CrmMailingAB) {
+            var abtest = new CrmMailingAB($route.current.params.id);
+            return abtest.load();
+          }
+        }
+      });
     }
   ]);
 
     $scope.$watch('abtest.ab.testing_criteria_id', updateCriteriaName);
   });
 
+  angular.module('crmMailingAB').controller('CrmMailingABReportCtrl', function ($scope, abtest, crmMailingABCriteria, crmApi) {
+    var ts = $scope.ts = CRM.ts('CiviMail');
+
+    $scope.abtest = abtest;
+
+    $scope.stats = {};
+    crmApi('Mailing', 'stats', {mailing_id: abtest.ab.mailing_id_a}).then(function(data){
+      $scope.stats.a = data.values[abtest.ab.mailing_id_a];
+    });
+    crmApi('Mailing', 'stats', {mailing_id: abtest.ab.mailing_id_b}).then(function(data){
+      $scope.stats.b = data.values[abtest.ab.mailing_id_b];
+    });
+  });
+
 })(angular, CRM.$, CRM._);
index 53c12f4d099d2079a209592b6078abeb5ad6394e..64c9b3582009ea9cf95002cb5c223f627fcc214f 100644 (file)
       }
     };
   });
+
+  // FIXME: This code is long and hasn't been fully working for me, but I've moved it into a spot
+  // where it at least fits in a bit better.
+
+  // example: <div crm-mailing-ab-stats="{split_count: 6, criteria:'Open'}" crm-abtest="myabtest" />
+  // options (see also: Mailing.graph_stats API)
+  //  - split_count: int
+  //  - criteria: string
+  //  - target_date: string, date
+  //  - target_url: string
+  angular.module('crmMailingAB').directive('crmMailingAbStats', function (crmApi, $parse, crmNow) {
+    return {
+      scope: {
+        crmMailingAbStats: '@',
+        crmAbtest: '@'
+      },
+      template: '<div class="crm-mailing-ab-stats"></div>',
+      link: function (scope, element, attrs) {
+        var abtestModel = $parse(attrs.crmAbtest);
+        var optionModel = $parse(attrs.crmMailingAbStats);
+        var options = angular.extend({}, optionModel(scope.$parent), {
+          criteria: 'Open', // e.g. 'Open', 'Total Unique Clicks'
+          split_count: 5
+        });
+
+        scope.$watch(attrs.crmAbtest, refresh);
+        function refresh() {
+          var now = crmNow();
+          var abtest = abtestModel(scope.$parent);
+          if (!abtest) {
+            console.log('failed to draw stats - missing abtest');
+            return;
+          }
+
+          scope.graph_data = [
+            {},
+            {},
+            {},
+            {},
+            {}
+          ];
+          var keep_cnt = 0;
+
+          for (var i = 1; i <= options.split_count; i++) {
+            var result = crmApi('MailingAB', 'graph_stats', {
+              id: abtest.ab.id,
+              target_date: abtest.ab.declare_winning_time ? abtest.ab.declare_winning_time : now,
+              target_url: null, // FIXME
+              criteria: options.criteria,
+              split_count: options.split_count,
+              split_count_select: i
+            });
+            result.then(function (data) {
+              var temp = 0;
+              keep_cnt++;
+              for (var key in data.values.A) {
+                temp = key;
+              }
+              var t = data.values.A[temp].time.split(" ");
+              var m = t[0];
+              var year = t[2];
+              var day = t[1].substr(0, t[1].length - 3);
+              if (t[3] == "") {
+                var t1 = t[4].split(":");
+                var hur = t1[0];
+                if (t[5] == "AM") {
+                  hour = hur;
+                  if (hour == 12) {
+                    hour = 0;
+                  }
+                }
+                if (t[5] == "PM") {
+                  hour = parseInt(hur) + 12;
+                }
+                var min = t1[1];
+              }
+              else {
+                var t1 = t[3].split(":");
+                var hur = t1[0];
+                if (t[4] == "AM") {
+                  hour = hur;
+                  if (hour == 12) {
+                    hour = 0;
+                  }
+                }
+                if (t[4] == "PM") {
+                  hour = parseInt(hur) + 12;
+                }
+                var min = t1[1];
+              }
+              var month = 0;
+              switch (m) {
+                case "January":
+                  month = 0;
+                  break;
+                case "February":
+                  month = 1;
+                  break;
+                case "March":
+                  month = 2;
+                  break;
+                case "April":
+                  month = 3;
+                  break;
+                case "May":
+                  month = 4;
+                  break;
+                case "June":
+                  month = 5;
+                  break;
+                case "July":
+                  month = 6;
+                  break;
+                case "August":
+                  month = 7;
+                  break;
+                case "September":
+                  month = 8;
+                  break;
+                case "October":
+                  month = 9;
+                  break;
+                case "November":
+                  month = 10;
+                  break;
+                case "December":
+                  month = 11;
+                  break;
+
+              }
+              var tp = new Date(year, month, day, hour, min, 0, 0);
+              scope.graph_data[temp - 1] = {
+                time: tp,
+                x: data.values.A[temp].count,
+                y: data.values.B[temp].count
+              };
+
+              if (keep_cnt == options.split_count) {
+                scope.graphload = true;
+                var data = scope.graph_data;
+
+                // set up a colour variable
+                var color = d3.scale.category10();
+
+                // map one colour each to x, y and z
+                // keys grabs the key value or heading of each key value pair in the json
+                // but not time
+                color.domain(d3.keys(data[0]).filter(function (key) {
+                  return key !== "time";
+                }));
+
+                // create a nested series for passing to the line generator
+                // it's best understood by console logging the data
+                var series = color.domain().map(function (name) {
+                  return {
+                    name: name,
+                    values: data.map(function (d) {
+                      return {
+                        time: d.time,
+                        score: +d[name]
+                      };
+                    })
+                  };
+                });
+
+                // Set the dimensions of the canvas / graph
+                var margin = {
+                    top: 30,
+                    right: 20,
+                    bottom: 40,
+                    left: 75
+                  },
+                  width = 550 - margin.left - margin.right,
+                  height = 350 - margin.top - margin.bottom;
+
+                // Set the ranges
+                //var x = d3.time.scale().range([0, width]).domain([0,10]);
+                var x = d3.time.scale().range([0, width]);
+                var y = d3.scale.linear().range([height, 0]);
+
+                // Define the axes
+                var xAxis = d3.svg.axis().scale(x)
+                  .orient("bottom").ticks(10);
+
+                var yAxis = d3.svg.axis().scale(y)
+                  .orient("left").ticks(5);
+
+                // Define the line
+                // Note you plot the time / score pair from each key you created ealier
+                var valueline = d3.svg.line()
+                  .x(function (d) {
+                    return x(d.time);
+                  })
+                  .y(function (d) {
+                    return y(d.score);
+                  });
+
+                // Adds the svg canvas
+                var svg = d3.select($('.crm-mailing-ab-stats', element)[0])
+                  .append("svg")
+                  .attr("width", width + margin.left + margin.right)
+                  .attr("height", height + margin.top + margin.bottom)
+                  .append("g")
+                  .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
+
+                // Scale the range of the data
+                x.domain(d3.extent(data, function (d) {
+                  return d.time;
+                }));
+
+                // note the nested nature of this you need to dig an additional level
+                y.domain([
+                  d3.min(series, function (c) {
+                    return d3.min(c.values, function (v) {
+                      return v.score;
+                    });
+                  }),
+                  d3.max(series, function (c) {
+                    return d3.max(c.values, function (v) {
+                      return v.score;
+                    });
+                  })
+                ]);
+                svg.append("text")      // text label for the x axis
+                  .attr("x", width / 2)
+                  .attr("y", height + margin.bottom)
+                  .style("text-anchor", "middle")
+                  .text("Time");
+
+                svg.append("text")      // text label for the x axis
+                  .style("text-anchor", "middle")
+                  .text(scope.winnercriteria).attr("transform",function (d) {
+                    return "rotate(-90)"
+                  }).attr("x", -height / 2)
+                  .attr("y", -30);
+
+                // create a variable called series and bind the date
+                // for each series append a g element and class it as series for css styling
+                var series = svg.selectAll(".series")
+                  .data(series)
+                  .enter().append("g")
+                  .attr("class", "series");
+
+                // create the path for each series in the variable series i.e. x, y and z
+                // pass each object called x, y nad z to the lne generator
+                series.append("path")
+                  .attr("class", "line")
+                  .attr("d", function (d) {
+                    // console.log(d); // to see how d3 iterates through series
+                    return valueline(d.values);
+                  })
+                  .style("stroke", function (d) {
+                    return color(d.name);
+                  });
+
+                // Add the X Axis
+                svg.append("g") // Add the X Axis
+                  .attr("class", "x axis")
+                  .attr("transform", "translate(0," + height + ")")
+                  .call(xAxis)
+                  .selectAll("text")
+                  .attr("transform", function (d) {
+                    return "rotate(-30)";
+                  });
+
+                // Add the Y Axis
+                svg.append("g") // Add the Y Axis
+                  .attr("class", "y axis")
+                  .call(yAxis);
+              }
+            });
+          }
+        }
+      } // link()
+    };
+  });
 })(angular, CRM.$, CRM._);
diff --git a/partials/crmMailingAB/report.html b/partials/crmMailingAB/report.html
new file mode 100644 (file)
index 0000000..0078119
--- /dev/null
@@ -0,0 +1,49 @@
+<div>
+
+  <table>
+    <thead>
+    <tr>
+      <th>{{ts('Details')}}</th>
+      <th style="width: 10em;">{{ts('Mailing A')}}</th>
+      <th style="width: 10em;">{{ts('Mailing B')}}</th>
+    </tr>
+    </thead>
+    <tbody>
+    <tr>
+      <td>{{ts('Delivered')}}</td>
+      <td>{{stats.a.Delivered}}</td>
+      <td>{{stats.b.Delivered}}</td>
+    </tr>
+    <tr>
+      <td>{{ts('Bounces')}}</td>
+      <td>{{stats.a.Bounces}}</td>
+      <td>{{stats.b.Bounces}}</td>
+    </tr>
+    <tr>
+      <td>{{ts('Unsubscribers')}}</td>
+      <td>{{stats.a.Unsubscribers}}</td>
+      <td>{{stats.b.Unsubscribers}}</td>
+    </tr>
+    <tr>
+      <td>{{'Opened'}}</td>
+      <td>{{stats.a.Opened}}</td>
+      <td>{{stats.b.Opened}}</td>
+    </tr>
+    <tr>
+      <td>{{ts('Unique Clicks')}}</td>
+      <td>{{stats.a['Unique Clicks']}}</td>
+      <td>{{stats.b['Unique Clicks']}}</td>
+    </tr>
+    </tbody>
+  </table>
+
+  <div crm-ui-tab-set>
+    <div crm-ui-tab id="tab-opens" crm-title="ts('Opens (WIP)')">
+      <div crm-mailing-ab-stats="{criteria: 'open', split_count: 5}" crm-abtest="abtest"></div>
+    </div>
+    <div crm-ui-tab id="tab-clicks" crm-title="ts('Total Clicks (WIP)')">
+      <div crm-mailing-ab-stats="{criteria: 'total unique clicks', split_count: 5}" crm-abtest="abtest"></div>
+    </div>
+  </div>
+
+</div>