Skip to content

Commit

Permalink
show sparkline-like charts next to resource metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
Joe Bowers committed Feb 29, 2016
1 parent fa0e2d0 commit 7ef80f3
Show file tree
Hide file tree
Showing 10 changed files with 282 additions and 27 deletions.
1 change: 1 addition & 0 deletions src/app/frontend/_variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ $hover-secondary: #ff1c19;
$body: #eee;
$emphasis: #000;
$content-background: #fff;
$chart-1: #00c752;

// TODO(bryk): Get those variables from Angular Material scss files.
$foreground-1: rgba(0, 0, 0, .87);
Expand Down
4 changes: 3 additions & 1 deletion src/app/frontend/common/components/components_module.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import filtersModule from '../filters/filters_module';
import labelsDirective from './labels/labels_directive';
import middleEllipsisDirective from './middleellipsis/middleellipsis_directive';
import sparklineDirective from './sparkline/sparkline_directive';

/**
* Module containing common components for the application.
Expand All @@ -27,4 +28,5 @@ export default angular
filtersModule.name,
])
.directive('kdLabels', labelsDirective)
.directive('kdMiddleEllipsis', middleEllipsisDirective);
.directive('kdMiddleEllipsis', middleEllipsisDirective)
.directive('kdSparkline', sparklineDirective);
23 changes: 23 additions & 0 deletions src/app/frontend/common/components/sparkline/sparkline.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!--
Copyright 2015 Google Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->

<svg viewBox="0,0 1,1"
preserveAspectRatio="none"
class="kd-sparkline">
<polygon
ng-attr-points="0,1 {{::sparklineCtrl.polygonPoints()}} 1,1"
class="kd-sparkline-series"/>
</svg>
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
* @final
*/
export default class SparklineController {
/**
* Constructs sparkline controller.
* @ngInject
*/
constructor() {
/**
* An array of {backendAPI.MetricResult} objects. The timestamp
* values of each object must be unique, and value must be greater
* than or equal to zero.
* @export {!Array<!backendApi.MetricResult>} Initialized from the scope.
*/
this.timeseries;
}

/**
* Formats the underlying series suitable for display as an SVG polygon.
* @return string
* @export
*/
polygonPoints() {
const series = this.timeseries.map(({timestamp, value}) => [Date.parse(timestamp), value]);
const sorted = series.slice().sort((a, b) => a[0] - b[0]);
const xShift = Math.min(...sorted.map((pt) => pt[0]));
const shifted = sorted.map(([x, y]) => [x - xShift, y]);
const xScale = Math.max(...shifted.map((pt) => pt[0])) || 1;
const yScale = Math.max(...shifted.map((pt) => pt[1])) || 1;
const scaled = shifted.map(([x, y]) => [x / xScale, y / yScale]);

// Invert Y because SVG Y=0 is at the top, and we want low values
// of Y to be closer to the bottom of the graphic
return scaled.map(([x, y]) => `${x},${(1 - y)}`).join(' ');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import SparklineController from './sparkline_controller';

/**
* Returns directive definition for sparkline.
* @return {!angular.Directive}
*/
export default function sparklineDirective() {
return {
controller: SparklineController,
controllerAs: 'sparklineCtrl',
templateUrl: 'common/components/sparkline/sparkline.html',
templateNamespace: 'svg',
scope: {},
bindToController: {'timeseries': '='},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,21 +135,31 @@ <h1 flex="auto" class="md-title kd-replicationcontrollerdetail-app-name">
-
</span>
</td>
<td kd-responsive-header="CPU" class="kd-replicationcontrollerdetail-table-cell"
ng-if="::ctrl.replicationControllerDetail.hasMetrics">
<span ng-if="::ctrl.hasCpuUsage(pod)">
{{::(pod.metrics.cpuUsage | kdCores)}}
<!-- width=1 is a hack to shrink-wrap the column around its content -->
<td kd-responsive-header="CPU"
class="kd-replicationcontrollerdetail-table-cell kd-replicationcontrollerdetail-sparkline-table-cell"
ng-if="::ctrl.replicationControllerDetail.hasMetrics"
ng-attr-width="{{ctrl.shouldShrinkSparklineCells()}}">
<span ng-if="::ctrl.hasCpuUsage(pod)" class="kd-replicationcontrollerdetail-labeled-sparkline">
<span class="kd-replicationcontrollerdetail-labeled-sparkline-label"
>{{::(pod.metrics.cpuUsage | kdCores)}}</span>
<kd-sparkline timeseries="pod.metrics.cpuUsageHistory"></kd-sparkline>
</span>
<span ng-if="::!ctrl.hasCpuUsage(pod)">
<span ng-if="::(!ctrl.hasCpuUsage(pod))">
-
</span>
</td>
<td kd-responsive-header="Memory" class="kd-replicationcontrollerdetail-table-cell"
ng-if="::ctrl.replicationControllerDetail.hasMetrics">
<span ng-if="::ctrl.hasMemoryUsage(pod)">
{{::(pod.metrics.memoryUsage | kdMemory)}}
<!-- width=1 is a hack to shrink-wrap the column around its content -->
<td kd-responsive-header="Memory"
class="kd-replicationcontrollerdetail-table-cell kd-replicationcontrollerdetail-sparkline-table-cell"
ng-if="::ctrl.replicationControllerDetail.hasMetrics"
ng-attr-width="{{ctrl.shouldShrinkSparklineCells()}}">
<span ng-if="::ctrl.hasMemoryUsage(pod)" class="kd-replicationcontrollerdetail-labeled-sparkline">
<span class="kd-replicationcontrollerdetail-labeled-sparkline-label"
>{{::(pod.metrics.memoryUsage | kdMemory)}}</span>
<kd-sparkline timeseries="pod.metrics.memoryUsageHistory"></kd-sparkline>
</span>
<span ng-if="::!ctrl.hasMemoryUsage(pod)">
<span ng-if="::(!ctrl.hasMemoryUsage(pod))">
-
</span>
</td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,45 @@ md-icon {
color: $delicate;
font-size: $subhead-font-size-base;
}

.kd-replicationcontrollerdetail-labeled-sparkline-label {
white-space: nowrap;
word-spacing: normal;
}

.kd-sparkline {
background-color: $body;
height: 2.5 * $baseline-grid;
vertical-align: -$baseline-grid / 2; // 20% below baseline, to flow with text
width: 12.5 * $baseline-grid;

@media only screen and (min-width: $layout-breakpoint-lg) {
margin-left: $baseline-grid;
}

@media only screen and (min-width: $layout-breakpoint-xs) and (max-width: $layout-breakpoint-lg) {
margin-top: $baseline-grid / 2;
}
}

.kd-sparkline-series {
fill: $chart-1;
}

.kd-replicationcontrollerdetail-labeled-sparkline {
@media only screen and (min-width: $layout-breakpoint-lg) {
display: block;
text-align: right;
white-space: nowrap;
}

@media only screen and (min-width: $layout-breakpoint-xs) and (max-width: $layout-breakpoint-lg) {
display: block;
padding-bottom: $baseline-grid;
padding-top: $baseline-grid;
}

@media only screen and (max-width: $layout-breakpoint-xs) {
word-spacing: $baseline-grid;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export default class ReplicationControllerDetailController {
* @returns {boolean}
* @export
*/
isSidebarVisible() { return this.mdMedia('gt-sm'); }
isSidebarVisible() { return this.mdMedia('gt-md'); }

/**
* Returns true if event is a warning.
Expand Down Expand Up @@ -183,7 +183,7 @@ export default class ReplicationControllerDetailController {
* @export
*/
hasCpuUsage(pod) {
return !!pod.metrics && (!!pod.metrics.cpuUsage || pod.metrics.cpuUsage === 0);
return !!pod.metrics && !!pod.metrics.cpuUsageHistory && pod.metrics.cpuUsageHistory.length > 0;
}

/**
Expand All @@ -192,6 +192,19 @@ export default class ReplicationControllerDetailController {
* @export
*/
hasMemoryUsage(pod) {
return !!pod.metrics && (!!pod.metrics.memoryUsage || pod.metrics.memoryUsage === 0);
return !!pod.metrics && !!pod.metrics.memoryUsageHistory &&
pod.metrics.memoryUsageHistory.length > 0;
}

/**
* Returns either 1 (if the table cells containing sparklines should
* shrink around their contents) or undefined (if those table cells
* should obey regular layout rules). The idiosyncratic return
* protocol is for compatibility with ng-attr's behavior - we want
* to generate either "width=1" or nothing at all.
*
* @return {(number|undefined)}
* @export
*/
shouldShrinkSparklineCells() { return (this.mdMedia('gt-xs') || undefined) && 1; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import SparklineController from 'common/components/sparkline/sparkline_controller';
import componentsModule from 'common/components/components_module';

describe('Sparkline controller', () => {
/**
* @type {!SparklineController}
*/
let ctrl;

beforeEach(() => {
angular.mock.module(componentsModule.name);

angular.mock.inject(($controller) => { ctrl = $controller(SparklineController); });
});

it('should produce no points with an empty series', () => {
// given
ctrl.timeseries = [];

// then
expect(ctrl.polygonPoints()).toBe('');
});

it('should shift and scale times such that the minimum time is zero and the maximum time is 1',
() => {
// given
ctrl.timeseries = [
{timestamp: '1976-01-15T00:00:00Z', value: 1},
{timestamp: '2026-01-15T00:00:00Z', value: 1},
];

// then
expect(ctrl.polygonPoints()).toBe('0,0 1,0');
});

it('should handle zero values and times without throwing an exception', () => {
// given
ctrl.timeseries = [
{timestamp: '1970-01-01T00:00:00Z', value: 0},
];

// then
expect(ctrl.polygonPoints()).toBe('0,1');
});

it('should scale values to <= 1 and invert them', () => {
// given
ctrl.timeseries = [
{timestamp: '1976-01-15T10:00:00Z', value: 10},
{timestamp: '1976-01-15T10:00:10Z', value: 1},
];

// then
expect(ctrl.polygonPoints()).toBe('0,0 1,0.9');
});

it('should sort a time series by time', () => {
// given
ctrl.timeseries = [
{timestamp: '1976-01-15T10:00:10Z', value: 1},
{timestamp: '1976-01-15T10:00:00Z', value: 10},
];

// then
expect(ctrl.polygonPoints()).toBe('0,0 1,0.9');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -111,21 +111,23 @@ describe('Replication Controller Detail controller', () => {
it('should show/hide cpu and memory metrics for pods', () => {
expect(ctrl.hasMemoryUsage({})).toBe(false);
expect(ctrl.hasMemoryUsage({metrics: {}})).toBe(false);
expect(ctrl.hasMemoryUsage({metrics: {memoryUsage: 0}})).toBe(true);
expect(ctrl.hasMemoryUsage({metrics: {memoryUsage: 1}})).toBe(true);
expect(ctrl.hasMemoryUsage({metrics: {memoryUsage: null}})).toBe(false);
expect(ctrl.hasMemoryUsage({metrics: {memoryUsage: undefined}})).toBe(false);
expect(ctrl.hasMemoryUsage({metrics: {cpuUsage: 1}})).toBe(false);
expect(ctrl.hasMemoryUsage({metrics: {memoryUsageHistory: []}})).toBe(false);
expect(ctrl.hasMemoryUsage({metrics: {memoryUsageHistory: [0]}})).toBe(true);
expect(ctrl.hasMemoryUsage({metrics: {memoryUsageHistory: [1]}})).toBe(true);
expect(ctrl.hasMemoryUsage({metrics: {memoryUsageHistory: null}})).toBe(false);
expect(ctrl.hasMemoryUsage({metrics: {memoryUsageHistory: undefined}})).toBe(false);
expect(ctrl.hasMemoryUsage({metrics: {cpuUsageHistory: [1]}})).toBe(false);

expect(ctrl.hasCpuUsage({})).toBe(false);
expect(ctrl.hasCpuUsage({metrics: {}})).toBe(false);
expect(ctrl.hasCpuUsage({metrics: {memoryUsage: 0}})).toBe(false);
expect(ctrl.hasCpuUsage({metrics: {memoryUsage: 1}})).toBe(false);
expect(ctrl.hasCpuUsage({metrics: {memoryUsage: null}})).toBe(false);
expect(ctrl.hasCpuUsage({metrics: {memoryUsage: undefined}})).toBe(false);
expect(ctrl.hasCpuUsage({metrics: {cpuUsage: 1}})).toBe(true);
expect(ctrl.hasCpuUsage({metrics: {cpuUsage: 0}})).toBe(true);
expect(ctrl.hasCpuUsage({metrics: {cpuUsage: null}})).toBe(false);
expect(ctrl.hasCpuUsage({metrics: {cpuUsage: undefined}})).toBe(false);
expect(ctrl.hasCpuUsage({metrics: {memoryUsageHistory: []}})).toBe(false);
expect(ctrl.hasCpuUsage({metrics: {memoryUsageHistory: [1]}})).toBe(false);
expect(ctrl.hasCpuUsage({metrics: {memoryUsageHistory: null}})).toBe(false);
expect(ctrl.hasCpuUsage({metrics: {memoryUsageHistory: undefined}})).toBe(false);
expect(ctrl.hasCpuUsage({metrics: {cpuUsageHistory: [1]}})).toBe(true);
expect(ctrl.hasCpuUsage({metrics: {cpuUsageHistory: [0]}})).toBe(true);
expect(ctrl.hasCpuUsage({metrics: {cpuUsageHistory: []}})).toBe(false);
expect(ctrl.hasCpuUsage({metrics: {cpuUsageHistory: null}})).toBe(false);
expect(ctrl.hasCpuUsage({metrics: {cpuUsageHistory: undefined}})).toBe(false);
});
});

0 comments on commit 7ef80f3

Please sign in to comment.