Skip to content

Commit

Permalink
Merge pull request #795 from weaveworks/638-animate-sparklines
Browse files Browse the repository at this point in the history
Animate sparklines
  • Loading branch information
davkal committed Feb 5, 2016
2 parents afaaab3 + cd66819 commit 188eea8
Show file tree
Hide file tree
Showing 12 changed files with 298 additions and 132 deletions.
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import React from 'react';

import Sparkline from '../sparkline';
import metricFeeder from '../../hoc/metric-feeder';
import { formatMetric } from '../../utils/string-utils';

export default (props) => {
return (
<div className="node-details-health-item">
<div className="node-details-health-item-value">{formatMetric(props.item.value, props.item)}</div>
<div className="node-details-health-item-sparkline">
<Sparkline data={props.item.samples} min={0} max={props.item.max}
first={props.item.first} last={props.item.last} interpolate="none" />
class NodeDetailsHealthItem extends React.Component {
render() {
return (
<div className="node-details-health-item">
<div className="node-details-health-item-value">{formatMetric(this.props.value, this.props)}</div>
<div className="node-details-health-item-sparkline">
<Sparkline data={this.props.samples} max={this.props.max}
first={this.props.first} last={this.props.last} />
</div>
<div className="node-details-health-item-label">{this.props.label}</div>
</div>
<div className="node-details-health-item-label">{props.item.label}</div>
</div>
);
};
);
}
}

export default metricFeeder(NodeDetailsHealthItem);
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import React from 'react';

import metricFeeder from '../../hoc/metric-feeder';
import { formatMetric } from '../../utils/string-utils';

export default class NodeDetailsHealthOverflowItem extends React.Component {
class NodeDetailsHealthOverflowItem extends React.Component {
render() {
return (
<div className="node-details-health-overflow-item">
<div className="node-details-health-overflow-item-value">{formatMetric(this.props.item.value, this.props.item)}</div>
<div className="node-details-health-overflow-item-label truncate">{this.props.item.label}</div>
<div className="node-details-health-overflow-item-value">{formatMetric(this.props.value, this.props)}</div>
<div className="node-details-health-overflow-item-label truncate">{this.props.label}</div>
</div>
);
}
}

export default metricFeeder(NodeDetailsHealthOverflowItem);
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default class NodeDetailsHealthOverflow extends React.Component {

return (
<div className="node-details-health-overflow" onClick={this.props.handleClickMore}>
{items.map(item => <NodeDetailsHealthOverflowItem key={item.id} item={item} />)}
{items.map(item => <NodeDetailsHealthOverflowItem key={item.id} {...item} />)}
<div className="node-details-health-overflow-expand">
Show more
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default class NodeDetailsHealth extends React.Component {
return (
<div className="node-details-health" style={{flexWrap, justifyContent}}>
{primeMetrics.map(item => {
return <NodeDetailsHealthItem key={item.id} item={item} />;
return <NodeDetailsHealthItem key={item.id} {...item} />;
})}
{showOverflow && <NodeDetailsHealthOverflow items={overflowMetrics} handleClickMore={this.handleClickMore} />}
{showLess && <div className="node-details-health-expand" onClick={this.handleClickMore}>show less</div>}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';

import { formatMetric } from '../../utils/string-utils';

class NodeDetailsTableNodeMetric extends React.Component {
render() {
return (
<td className="node-details-table-node-metric">
{formatMetric(this.props.value, this.props)}
</td>
);
}
}

export default NodeDetailsTableNodeMetric;
16 changes: 10 additions & 6 deletions client/app/scripts/components/node-details/node-details-table.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import _ from 'lodash';
import React from 'react';

import NodeDetailsTableNodeLink from './node-details-table-node-link';
import { formatMetric } from '../../utils/string-utils';
import NodeDetailsTableNodeMetric from './node-details-table-node-metric';

export default class NodeDetailsTable extends React.Component {

Expand Down Expand Up @@ -60,6 +60,7 @@ export default class NodeDetailsTable extends React.Component {
['metrics', 'metadata'].forEach(collection => {
if (node[collection]) {
node[collection].forEach(field => {
field.valueType = collection;
values[field.id] = field;
});
}
Expand Down Expand Up @@ -116,11 +117,14 @@ export default class NodeDetailsTable extends React.Component {
return this.props.columns.map(col => {
const field = fields[col];
if (field) {
return (
<td className="node-details-table-node-value" key={field.id}>
{formatMetric(field.value, field)}
</td>
);
if (field.valueType === 'metadata') {
return (
<td className="node-details-table-node-value" key={field.id}>
{field.value}
</td>
);
}
return <NodeDetailsTableNodeMetric key={field.id} {...field} />;
}
});
}
Expand Down
177 changes: 76 additions & 101 deletions client/app/scripts/components/sparkline.js
Original file line number Diff line number Diff line change
@@ -1,126 +1,101 @@
// Forked from: https://github.com/KyleAMathews/react-sparkline at commit a9d7c5203d8f240938b9f2288287aaf0478df013
import React from 'react';
import ReactDOM from 'react-dom';
import d3 from 'd3';

const parseDate = d3.time.format.iso.parse;

export default class Sparkline extends React.Component {
componentDidMount() {
return this.renderSparkline();
}

renderSparkline() {
// If the sparkline has already been rendered, remove it.
const el = ReactDOM.findDOMNode(this);
while (el.firstChild) {
el.removeChild(el.firstChild);
}
constructor(props, context) {
super(props, context);
this.x = d3.scale.linear();
this.y = d3.scale.linear();
this.line = d3.svg.line()
.x(d => this.x(d.date))
.y(d => this.y(d.value));
}

const data = this.props.data.slice();
getGraphData() {
// data is of shape [{date, value}, ...] and is sorted by date (ASC)
let data = this.props.data;

// Do nothing if no data is passed in.
if (data.length === 0) {
return;
// Do nothing if no data or data w/o date are passed in.
if (data.length === 0 || data[0].date === undefined) {
return <div />;
}

const x = d3.scale.linear().range([2, this.props.width - 2]);
const y = d3.scale.linear().range([this.props.height - 2, 2]);

// react-sparkline allows you to pass in two types of data.
// Data tied to dates and linear data. We need to change our line and x/y
// functions depending on the type of data.

// These are objects with a date key
let line;
let lastX;
let lastY;
let title;
if (data[0].date) {
// Convert dates into D3 dates
data.forEach(d => {
d.date = d3.time.format.iso.parse(d.date);
});

line = d3.svg.line().
interpolate(this.props.interpolate).
x(d => x(d.date)).
y(d => y(d.value));

const first = this.props.first ? d3.time.format.iso.parse(this.props.first) : d3.min(data, d => d.date);
const last = this.props.last ? d3.time.format.iso.parse(this.props.last) : d3.max(data, d => d.date);
x.domain([first, last]);

y.domain([
this.props.min || d3.min(data, d => d.value),
this.props.max || d3.max(data, d => d.value)
]);

lastX = x(data[data.length - 1].date);
lastY = y(data[data.length - 1].value);
title = 'Last ' + d3.round((last - first) / 1000) + ' seconds, ' + data.length + ' samples, min: ' + d3.round(d3.min(data, d => d.value), 2) + ', max: ' + d3.round(d3.max(data, d => d.value), 2) + ', mean: ' + d3.round(d3.mean(data, d => d.value), 2);
} else {
line = d3.svg.line().
interpolate(this.props.interpolate).
x((d, i) => x(i)).
y(d => y(d));

x.domain([
this.props.first || 0,
this.props.last || data.length
]);

y.domain([
this.props.min || d3.min(data),
this.props.max || d3.max(data)
]);

lastX = x(data.length - 1);
lastY = y(data[data.length - 1]);
title = data.length + ' samples, min: ' + d3.round(d3.min(data), 2) + ', max: ' + d3.round(d3.max(data), 2) + ', mean: ' + d3.round(d3.mean(data), 2);
// adjust scales
this.x.range([2, this.props.width - 2]);
this.y.range([this.props.height - 2, 2]);
this.line.interpolate(this.props.interpolate);

// Convert dates into D3 dates
data = data.map(d => {
return {
date: parseDate(d.date),
value: d.value
};
});

// determine date range
let firstDate = this.props.first ? parseDate(this.props.first) : data[0].date;
let lastDate = this.props.last ? parseDate(this.props.last) : data[data.length - 1].date;
// if last prop is after last value, we need to add that difference as
// padding before first value to right-align sparkline
const skip = lastDate - data[data.length - 1].date;
if (skip > 0) {
firstDate -= skip;
lastDate -= skip;
}

d3.select(ReactDOM.findDOMNode(this)).attr('title', title);

const svg = d3.select(ReactDOM.findDOMNode(this)).
append('svg').
attr('width', this.props.width).
attr('height', this.props.height).
append('g');

svg.append('path').
datum(data).
attr('class', 'sparkline').
style('fill', 'none').
style('stroke', this.props.strokeColor).
style('stroke-width', this.props.strokeWidth).
attr('d', line);

svg.append('circle').
attr('class', 'sparkcircle').
attr('cx', lastX).
attr('cy', lastY).
attr('fill', '#46466a').
attr('fill-opacity', 0.6).
attr('stroke', 'none').
attr('r', this.props.circleDiameter);
this.x.domain([firstDate, lastDate]);

// determine value range
const minValue = this.props.min !== undefined ? this.props.min : d3.min(data, d => d.value);
const maxValue = this.props.max !== undefined ? Math.max(this.props.max, d3.max(data, d => d.value)) : d3.max(data, d => d.value);
this.y.domain([minValue, maxValue]);

const lastValue = data[data.length - 1].value;
const lastX = this.x(lastDate);
const lastY = this.y(lastValue);
const title = 'Last ' + d3.round((lastDate - firstDate) / 1000) + ' seconds, ' +
data.length + ' samples, min: ' + d3.round(d3.min(data, d => d.value), 2) +
', max: ' + d3.round(d3.max(data, d => d.value), 2) +
', mean: ' + d3.round(d3.mean(data, d => d.value), 2);

return {title, lastX, lastY, data};
}

render() {
// Do nothing if no data or data w/o date are passed in.
if (this.props.data.length === 0 || this.props.data[0].date === undefined) {
return <div />;
}

const {lastX, lastY, title, data} = this.getGraphData();

return (
<div/>
<div title={title}>
<svg width={this.props.width} height={this.props.height}>
<path className="sparkline" fill="none" stroke={this.props.strokeColor}
strokeWidth={this.props.strokeWidth} ref="path" d={this.line(data)} />
<circle className="sparkcircle" cx={lastX} cy={lastY} fill="#46466a"
fillOpacity="0.6" stroke="none" r={this.props.circleDiameter} />
</svg>
</div>
);
}

componentDidUpdate() {
return this.renderSparkline();
}
}

Sparkline.propTypes = {
data: React.PropTypes.array.isRequired
};

Sparkline.defaultProps = {
width: 80,
height: 16,
height: 24,
strokeColor: '#7d7da8',
strokeWidth: '0.5px',
interpolate: 'basis',
circleDiameter: 1.75,
data: [1, 23, 5, 5, 23, 0, 0, 0, 4, 32, 3, 12, 3, 1, 24, 1, 5, 5, 24, 23] // Some semi-random data.
interpolate: 'none',
circleDiameter: 1.75
};
3 changes: 3 additions & 0 deletions client/app/scripts/constants/timer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/* Intervals in ms */
export const API_INTERVAL = 30000;
export const TOPOLOGY_INTERVAL = 5000;
Loading

0 comments on commit 188eea8

Please sign in to comment.