Add canvas line chart

This commit adds a canvas-based line chart using the new canvas
charting directives, replacing the nvd3 line charts on the home,
grouped run, and job pages.

Change-Id: Ifd937196e7f9f1f3176ddf82fa54cdab49df4461
This commit is contained in:
Tim Buckley 2016-10-02 18:58:50 -06:00
parent 38c9528eb1
commit 7d87c32d86
11 changed files with 347 additions and 169 deletions

View File

@ -114,10 +114,9 @@ function GroupedRunsController(
});
});
vm.chartData = [
{ key: 'Passes', values: passEntries, color: 'blue' },
{ key: 'Failures', values: failEntries, color: 'red' }
];
vm.passes = passEntries;
vm.failures = failEntries;
vm.failRates = failRateEntries;
vm.chartDataRate = [
{ key: '% Failures', values: failRateEntries }

View File

@ -50,11 +50,9 @@ function HomeController(
var dateStats = projectService.getStatsByDate(projects);
var entries = getChartEntries(dateStats, blanks);
vm.chartData = [
{ key: 'Passes', values: entries.passes, color: 'blue' },
{ key: 'Failures', values: entries.failures, color: 'red' }
];
vm.chartDataRate = [{ key: '% Failures', values: entries.failRate }];
vm.passes = entries.passes;
vm.failures = entries.failures;
vm.failRate = entries.failRate;
vm.projects = projects
.sort(byFailRateDesc)
.map(function(project) { return generateHorizontalBarData(project); });

View File

@ -140,15 +140,10 @@ function JobController(
});
}
vm.chartData = [
{ key: 'Passes', values: passEntries, color: 'blue' },
{ key: 'Failures', values: failEntries, color: 'red' },
{ key: 'Skips', values: skipEntries, color: 'violet' }
];
vm.chartDataRate = [
{ key: '% Failures', values: failRateEntries }
];
vm.passes = passEntries;
vm.failures = failEntries;
vm.skips = skipEntries;
vm.failRates = failRateEntries;
vm.tests = Object.keys(tests).map(function(test) {
return tests[test];

View File

@ -0,0 +1,163 @@
'use strict';
var directivesModule = require('./_index.js');
/**
* @ngInject
*/
function chartCanvasLine() {
var link = function(scope, el, attrs, ctrl) {
var base = ctrl.createCanvas(ctrl.width, ctrl.height, false);
var baseDirty = false;
var overlay = ctrl.createCanvas(ctrl.width, ctrl.height, false);
var overlayDirty = false;
var stroke = scope.stroke || 'black';
var lineWidth = scope.lineWidth || 1;
var dataset = null;
var screenX = null;
var screenY = null;
var dataX = null;
var dataY = null;
var nearest = null;
function updateAxes() {
dataset = ctrl.datasets[scope.dataset];
if (!dataset) {
return;
}
var axes = scope.axes.split(/[\s,]+/).map(function(name) {
return ctrl.axes[name];
});
screenX = axes.find(function(a) { return a.orient === 'horizontal'; });
screenY = axes.find(function(a) { return a.orient === 'vertical'; });
dataX = ctrl.data(dataset.name, screenX.name);
dataY = ctrl.data(dataset.name, screenY.name);
}
function renderBase() {
var dataset = ctrl.datasets[scope.dataset];
if (!dataset) {
return;
}
var ctx = base.ctx;
ctx.strokeStyle = stroke;
ctx.lineWidth = lineWidth * base.ratio;
ctx.beginPath();
ctx.moveTo(screenX.scale(dataX[0]), screenY.scale(dataY[0]));
for (var i = 1; i < dataX.length; i++) {
ctx.lineTo(screenX.scale(dataX[i]), screenY.scale(dataY[i]));
}
ctx.stroke();
baseDirty = false;
}
function renderOverlay() {
var ctx = overlay.ctx;
ctx.clearRect(0, 0, overlay.canvas.width, overlay.canvas.height);
if (nearest) {
ctx.fillStyle = 'rgba(50, 50, 50, 0.15)';
ctx.strokeStyle = 'rgba(100, 100, 100, 0.75)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(
screenX.scale(screenX.mapper(nearest)),
screenY.scale(screenY.mapper(nearest)),
5 * overlay.ratio,
0, Math.PI * 2
);
ctx.fill();
ctx.stroke();
}
overlayDirty = false;
}
function handleUpdate() {
updateAxes();
baseDirty = true;
overlayDirty = true;
}
scope.$on('render', function(event, canvas) {
if (baseDirty) {
renderBase();
}
canvas.ctx.drawImage(base.canvas, 0, 0);
});
scope.$on('renderOverlay', function(event, canvas) {
if (overlayDirty) {
renderOverlay();
}
canvas.ctx.drawImage(overlay.canvas, 0, 0);
});
scope.$on('update', handleUpdate);
scope.$on('resize', function(event, width, height) {
base.resize(width, height);
overlay.resize(width, height);
});
scope.$on('mousemove', function(event, p) {
if (!dataset) {
return;
}
nearest = ctrl.nearestPoint(p, dataset, screenX, screenY, 10 * base.ratio);
overlayDirty = true;
ctrl.render();
if (nearest) {
ctrl.tooltips.set(dataset.name, {
points: [nearest],
style: stroke
});
} else {
ctrl.tooltips.delete(dataset.name);
}
});
scope.$on('mouseout', function() {
if (!dataset) {
return;
}
nearest = null;
overlayDirty = true;
ctrl.render();
ctrl.tooltips.delete(dataset.name);
});
};
return {
restrict: 'E',
require: '^chart',
link: link,
scope: {
dataset: '@',
axes: '@',
lineWidth: '=',
stroke: '@'
}
};
}
directivesModule.directive('chartCanvasLine', chartCanvasLine);

View File

@ -1,73 +0,0 @@
'use strict';
var directivesModule = require('./_index.js');
var d3 = require('d3');
var nv = require('nvd3');
/**
* @ngInject
*/
function chartLine() {
var link = function(scope, el, attrs) {
scope.$on('loading-started', function() {
el.css({'display' : 'none'});
});
scope.$on('loading-complete', function() {
el.css({'display' : 'block'});
});
var chart = null;
var svg = d3.select(el[0]).append('svg')
.attr('width', attrs.width)
.attr('height', attrs.height);
var update = function(data) {
if (typeof data === 'undefined') {
return;
}
chart = nv.models.lineChart()
.margin({ left: 50, right: 50 })
.useInteractiveGuideline(true);
chart.tooltip.gravity('s').chartContainer(el[0]);
chart.xAxis.tickFormat(function(d) { return d3.time.format('%m/%d %H:%M')(new Date(d)); });
if (attrs.forceY) {
chart.forceY(angular.fromJson(attrs.forceY));
}
if (attrs.tickFormatX) {
chart.yAxis.tickFormat(d3.format(attrs.tickFormatX));
}
if (attrs.tickFormatY) {
chart.yAxis.tickFormat(d3.format(attrs.tickFormatY));
}
svg.datum(data).call(chart);
};
scope.$on('windowResize', function() {
if (chart !== null) {
chart.update();
}
});
scope.$watch('data', update);
};
return {
restrict: 'EA',
scope: {
'data': '=',
'width': '@',
'height': '@'
},
link: link
};
}
directivesModule.directive('chartLine', chartLine);

View File

@ -31,8 +31,32 @@
</div>
</div>
<div class="panel-body">
<chart-line data="groupedRuns.chartData" width="100%" height="250"
tick-format-x="d"></chart-line>
<chart width="100%" height="250px">
<chart-axis name="x" opposes="y" type="time"
path=".x" align="bottom" orient="horizontal"
granular-format="%x %X"></chart-axis>
<chart-axis name="y" opposes="x" type="linear"
path=".y" align="left" orient="vertical"
draw="true"></chart-axis>
<chart-dataset name="passes"
title="Passes"
data="groupedRuns.passes"></chart-dataset>
<chart-dataset name="failures"
title="Failures"
data="groupedRuns.failures"></chart-dataset>
<chart-canvas-line dataset="passes"
axes="x y"
stroke="blue"
line-width="1"></chart-canvas-line>
<chart-canvas-line dataset="failures"
axes="x y"
stroke="red"
line-width="1"></chart-canvas-line>
<chart-tooltip primary="x" secondary="y"></chart-tooltip>
</chart>
</div>
</div>
<div class="panel panel-default">
@ -50,8 +74,25 @@
</div>
</div>
<div class="panel-body">
<chart-line data="groupedRuns.chartDataRate" width="100%" height="250"
force-y="[0,1]" tick-format-x="%"></chart-line>
<chart width="100%" height="250px">
<chart-axis name="x" opposes="y" type="time"
path=".x" align="bottom" orient="horizontal"
granular-format="%x %X"></chart-axis>
<chart-axis name="y" opposes="x" type="linear"
path=".y" align="left" orient="vertical"
domain="[0, 1]" draw="true"></chart-axis>
<chart-dataset name="rate"
title="% Failures"
data="groupedRuns.failRates"></chart-dataset>
<chart-canvas-line dataset="rate"
axes="x y"
stroke="red"
line-width="1"></chart-canvas-line>
<chart-tooltip primary="x" secondary="y"></chart-tooltip>
</chart>
</div>
</div>
</div>

View File

@ -35,8 +35,32 @@
</div>
</div>
<div class="panel-body">
<chart-line data="home.chartData" width="100%" height="250"
tick-format-x="d"></chart-line>
<chart width="100%" height="250px">
<chart-axis name="x" opposes="y" type="time"
path=".x" align="bottom" orient="horizontal"
granular-format="%x %X"></chart-axis>
<chart-axis name="y" opposes="x" type="linear"
path=".y" align="left" orient="vertical"
draw="true"></chart-axis>
<chart-dataset name="passes"
title="Passes"
data="home.passes"></chart-dataset>
<chart-dataset name="failures"
title="Failures"
data="home.failures"></chart-dataset>
<chart-canvas-line dataset="passes"
axes="x y"
stroke="blue"
line-width="1"></chart-canvas-line>
<chart-canvas-line dataset="failures"
axes="x y"
stroke="red"
line-width="1"></chart-canvas-line>
<chart-tooltip primary="x" secondary="y"></chart-tooltip>
</chart>
</div>
</div>
<div class="panel panel-default">
@ -53,8 +77,24 @@
</div>
</div>
<div class="panel-body">
<chart-line data="home.chartDataRate" width="100%" height="250"
force-y="[0,1]" tick-format-x="%"></chart-line>
<chart width="100%" height="250px">
<chart-axis name="x" type="time" path=".x" opposes="y"
align="bottom" orient="horizontal"
granular-format="%x %X"></chart-axis>
<chart-axis name="y" type="linear" path=".y" opposes="x"
align="left" orient="vertical"
domain="[0, 1]" draw="true"></chart-axis>
<chart-dataset name="rate"
title="% Failures"
data="home.failRate"></chart-dataset>
<chart-canvas-line dataset="rate" axes="x y"
stroke="red"
line-width="1"></chart-canvas-line>
<chart-tooltip primary="x" secondary="y"></chart-tooltip>
</chart>
</div>
</div>
</div>

View File

@ -31,8 +31,39 @@
</div>
</div>
<div class="panel-body">
<chart-line data="job.chartData" width="100%" height="250"
tick-format-x="d"></chart-line>
<chart width="100%" height="250px">
<chart-axis name="x" opposes="y" type="time"
path=".x" align="bottom" orient="horizontal"
granular-format="%x %X"></chart-axis>
<chart-axis name="y" opposes="x" type="linear"
path=".y" align="left" orient="vertical"
draw="true"></chart-axis>
<chart-dataset name="passes"
title="Passes"
data="job.passes"></chart-dataset>
<chart-dataset name="failures"
title="Failures"
data="job.failures"></chart-dataset>
<chart-dataset name="skips"
title="Skips"
data="job.skips"></chart-dataset>
<chart-canvas-line dataset="passes"
axes="x y"
stroke="blue"
line-width="1"></chart-canvas-line>
<chart-canvas-line dataset="failures"
axes="x y"
stroke="red"
line-width="1"></chart-canvas-line>
<chart-canvas-line dataset="skips"
axes="x y"
stroke="violet"
line-width="1"></chart-canvas-line>
<chart-tooltip primary="x" secondary="y"></chart-tooltip>
</chart>
</div>
</div>
<div class="panel panel-default">
@ -50,8 +81,25 @@
</div>
</div>
<div class="panel-body">
<chart-line data="job.chartDataRate" width="100%" height="250"
forceY="[0,1]" tick-format-x="%"></chart-line>
<chart width="100%" height="250px">
<chart-axis name="x" opposes="y" type="time"
path=".x" align="bottom" orient="horizontal"
granular-format="%x %X"></chart-axis>
<chart-axis name="y" opposes="x" type="linear"
path=".y" align="left" orient="vertical"
domain="[0, 1]" draw="true"></chart-axis>
<chart-dataset name="rate"
title="% Failures"
data="job.failRates"></chart-dataset>
<chart-canvas-line dataset="rate"
axes="x y"
stroke="red"
line-width="1"></chart-canvas-line>
<chart-tooltip primary="x" secondary="y"></chart-tooltip>
</chart>
</div>
</div>

View File

@ -131,21 +131,14 @@ describe('GroupedRunsController', function() {
});
$httpBackend.flush();
var expectedChartData = [{
key: 'Passes',
values: [{
x: 1416355200000, y: 83
}],
color: 'blue'
}, {
key: 'Failures',
values: [{
x: 1416355200000,
y: 2
}],
color: 'red'
}];
expect(groupedRunsController.chartData).toEqual(expectedChartData);
expect(groupedRunsController.passes).toEqual([{
x: 1416355200000, y: 83
}]);
expect(groupedRunsController.failures).toEqual([{
x: 1416355200000,
y: 2
}]);
});
it('should process chart data rate correctly', function() {
@ -158,13 +151,9 @@ describe('GroupedRunsController', function() {
});
$httpBackend.flush();
var expectedChartDataRate = [{
key: '% Failures',
values: [{
x: 1416355200000,
y: 0.023529411764705883
}]
}];
expect(groupedRunsController.chartDataRate).toEqual(expectedChartDataRate);
expect(groupedRunsController.failRates).toEqual([{
x: 1416355200000,
y: 0.023529411764705883
}]);
});
});

View File

@ -77,21 +77,12 @@ describe('HomeController', function() {
});
it('should contain data for passes and failures', function() {
var expectedPasses = {
key: 'Passes', values: [{ x: timestamp, y: 3 }], color: 'blue'
};
var expectedFailures = {
key: 'Failures', values: [{ x: timestamp, y: 4 }], color: 'red'
};
expect(homeController.chartData).toContain(expectedPasses);
expect(homeController.chartData).toContain(expectedFailures);
expect(homeController.passes).toEqual([{ x: timestamp, y: 3 }]);
expect(homeController.failures).toEqual([{ x: timestamp, y: 4 }]);
});
it('should contain data for failure rate', function() {
var expectedChartDataRate = [{
key: '% Failures', values: [{ x: 1443729600000, y: 0.57 }]
}];
expect(homeController.chartDataRate).toEqual(expectedChartDataRate);
expect(homeController.failRate).toEqual([{ x: 1443729600000, y: 0.57 }]);
});
});

View File

@ -116,29 +116,20 @@ describe('JobController', function() {
});
$httpBackend.flush();
var expectedChartData = [{
key: 'Passes',
values: [{
x: 1416358800000,
y: 52
}],
color: 'blue'
}, {
key: 'Failures',
values: [{
x: 1416358800000,
y: 1
}],
color: 'red'
}, {
key: 'Skips',
values: [{
x: 1416358800000,
y: 1
}],
color: 'violet'
}];
expect(jobController.chartData).toEqual(expectedChartData);
expect(jobController.passes).toEqual([{
x: 1416358800000,
y: 52
}]);
expect(jobController.failures).toEqual([{
x: 1416358800000,
y: 1
}]);
expect(jobController.skips).toEqual([{
x: 1416358800000,
y: 1
}]);
});
it('should process chart data rate correctly', function() {
@ -150,14 +141,10 @@ describe('JobController', function() {
});
$httpBackend.flush();
var expectedChartDataRate = [{
key: '% Failures',
values: [{
x: 1416358800000,
y: 0.018867924528301886
}]
}];
expect(jobController.chartDataRate).toEqual(expectedChartDataRate);
expect(jobController.failRates).toEqual([{
x: 1416358800000,
y: 0.018867924528301886
}]);
});
it('should process tests correctly', function() {