-
Notifications
You must be signed in to change notification settings - Fork 712
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #795 from weaveworks/638-animate-sparklines
Animate sparklines
- Loading branch information
Showing
12 changed files
with
298 additions
and
132 deletions.
There are no files selected for viewing
27 changes: 16 additions & 11 deletions
27
client/app/scripts/components/node-details/node-details-health-item.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
9 changes: 6 additions & 3 deletions
9
client/app/scripts/components/node-details/node-details-health-overflow-item.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
15 changes: 15 additions & 0 deletions
15
client/app/scripts/components/node-details/node-details-table-node-metric.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.