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:
parent
38c9528eb1
commit
7d87c32d86
@ -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 }
|
||||
|
@ -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); });
|
||||
|
@ -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];
|
||||
|
163
app/js/directives/chart-canvas-line.js
Normal file
163
app/js/directives/chart-canvas-line.js
Normal 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);
|
@ -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);
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -131,21 +131,14 @@ describe('GroupedRunsController', function() {
|
||||
});
|
||||
$httpBackend.flush();
|
||||
|
||||
var expectedChartData = [{
|
||||
key: 'Passes',
|
||||
values: [{
|
||||
expect(groupedRunsController.passes).toEqual([{
|
||||
x: 1416355200000, y: 83
|
||||
}],
|
||||
color: 'blue'
|
||||
}, {
|
||||
key: 'Failures',
|
||||
values: [{
|
||||
}]);
|
||||
|
||||
expect(groupedRunsController.failures).toEqual([{
|
||||
x: 1416355200000,
|
||||
y: 2
|
||||
}],
|
||||
color: 'red'
|
||||
}];
|
||||
expect(groupedRunsController.chartData).toEqual(expectedChartData);
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should process chart data rate correctly', function() {
|
||||
@ -158,13 +151,9 @@ describe('GroupedRunsController', function() {
|
||||
});
|
||||
$httpBackend.flush();
|
||||
|
||||
var expectedChartDataRate = [{
|
||||
key: '% Failures',
|
||||
values: [{
|
||||
expect(groupedRunsController.failRates).toEqual([{
|
||||
x: 1416355200000,
|
||||
y: 0.023529411764705883
|
||||
}]
|
||||
}];
|
||||
expect(groupedRunsController.chartDataRate).toEqual(expectedChartDataRate);
|
||||
}]);
|
||||
});
|
||||
});
|
||||
|
@ -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 }]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -116,29 +116,20 @@ describe('JobController', function() {
|
||||
});
|
||||
$httpBackend.flush();
|
||||
|
||||
var expectedChartData = [{
|
||||
key: 'Passes',
|
||||
values: [{
|
||||
expect(jobController.passes).toEqual([{
|
||||
x: 1416358800000,
|
||||
y: 52
|
||||
}],
|
||||
color: 'blue'
|
||||
}, {
|
||||
key: 'Failures',
|
||||
values: [{
|
||||
}]);
|
||||
|
||||
expect(jobController.failures).toEqual([{
|
||||
x: 1416358800000,
|
||||
y: 1
|
||||
}],
|
||||
color: 'red'
|
||||
}, {
|
||||
key: 'Skips',
|
||||
values: [{
|
||||
}]);
|
||||
|
||||
expect(jobController.skips).toEqual([{
|
||||
x: 1416358800000,
|
||||
y: 1
|
||||
}],
|
||||
color: 'violet'
|
||||
}];
|
||||
expect(jobController.chartData).toEqual(expectedChartData);
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should process chart data rate correctly', function() {
|
||||
@ -150,14 +141,10 @@ describe('JobController', function() {
|
||||
});
|
||||
$httpBackend.flush();
|
||||
|
||||
var expectedChartDataRate = [{
|
||||
key: '% Failures',
|
||||
values: [{
|
||||
expect(jobController.failRates).toEqual([{
|
||||
x: 1416358800000,
|
||||
y: 0.018867924528301886
|
||||
}]
|
||||
}];
|
||||
expect(jobController.chartDataRate).toEqual(expectedChartDataRate);
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should process tests correctly', function() {
|
||||
|
Loading…
Reference in New Issue
Block a user