From 360aaa75320b47a483284b857cc15be0ba479ade Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Sat, 20 Dec 2014 01:32:21 -0800 Subject: [PATCH] crmMailingAB - Transplant report. Decouple stats API. 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 | 49 +++-- css/angular-crmMailingAB.css | 11 + js/angular-crmMailingAB.js | 26 ++- js/angular-crmMailingAB/directives.js | 276 ++++++++++++++++++++++++++ partials/crmMailingAB/report.html | 49 +++++ 5 files changed, 390 insertions(+), 21 deletions(-) create mode 100644 partials/crmMailingAB/report.html diff --git a/api/v3/MailingAB.php b/api/v3/MailingAB.php index 7d55872f2d..8b66a562ea 100755 --- a/api/v3/MailingAB.php +++ b/api/v3/MailingAB.php @@ -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( diff --git a/css/angular-crmMailingAB.css b/css/angular-crmMailingAB.css index 19a25128d5..80d3341121 100644 --- a/css/angular-crmMailingAB.css +++ b/css/angular-crmMailingAB.css @@ -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; +} diff --git a/js/angular-crmMailingAB.js b/js/angular-crmMailingAB.js index ea8839f293..6dc3d12cd2 100644 --- a/js/angular-crmMailingAB.js +++ b/js/angular-crmMailingAB.js @@ -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) { @@ -30,6 +30,16 @@ } } }); + $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(); + } + } + }); } ]); @@ -164,4 +174,18 @@ $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._); diff --git a/js/angular-crmMailingAB/directives.js b/js/angular-crmMailingAB/directives.js index 53c12f4d09..64c9b35820 100644 --- a/js/angular-crmMailingAB/directives.js +++ b/js/angular-crmMailingAB/directives.js @@ -88,4 +88,280 @@ } }; }); + + // 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:
+ // 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: '
', + 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 index 0000000000..0078119653 --- /dev/null +++ b/partials/crmMailingAB/report.html @@ -0,0 +1,49 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ts('Details')}}{{ts('Mailing A')}}{{ts('Mailing B')}}
{{ts('Delivered')}}{{stats.a.Delivered}}{{stats.b.Delivered}}
{{ts('Bounces')}}{{stats.a.Bounces}}{{stats.b.Bounces}}
{{ts('Unsubscribers')}}{{stats.a.Unsubscribers}}{{stats.b.Unsubscribers}}
{{'Opened'}}{{stats.a.Opened}}{{stats.b.Opened}}
{{ts('Unique Clicks')}}{{stats.a['Unique Clicks']}}{{stats.b['Unique Clicks']}}
+ +
+
+
+
+
+
+
+
+ +
-- 2.25.1