* @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';
}
/**
*
* @param array $params
* @return array
+ * @throws API_Exception
*/
function civicrm_api3_mailing_a_b_graph_stats($params) {
civicrm_api3_verify_mandatory($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(
)
);
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(
)
);
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(
.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;
+}
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._);
}
};
});
+
+ // 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._);
--- /dev/null
+<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>