From 510870e2f920cb54c72da7e14f05191ed0b77b76 Mon Sep 17 00:00:00 2001 From: Paul Bellamy Date: Wed, 11 Nov 2015 17:14:49 +0000 Subject: [PATCH] Add Sparklines to the UI based on Metrics. - basic sparklines rendering for load - Graphs are normalized so they all render on the y-axis. - Time-axis is fixed to 15-seconds, so that data fills in correctly when data is insufficient - Move load scalar behind sparkline - add title to sparklines, showing timespan, samples, etc --- .../scripts/components/node-details-table.js | 17 +-- client/app/scripts/components/sparkline.js | 129 ++++++++++++++++++ client/app/styles/main.less | 9 ++ 3 files changed, 145 insertions(+), 10 deletions(-) create mode 100644 client/app/scripts/components/sparkline.js diff --git a/client/app/scripts/components/node-details-table.js b/client/app/scripts/components/node-details-table.js index 5e5d4ea639..b2a7cf1c1b 100644 --- a/client/app/scripts/components/node-details-table.js +++ b/client/app/scripts/components/node-details-table.js @@ -1,10 +1,9 @@ const React = require('react'); +const Sparkline = require('./sparkline'); const NodeDetailsTable = React.createClass({ render: function() { - const isNumeric = this.props.isNumeric; - return (

@@ -15,14 +14,12 @@ const NodeDetailsTable = React.createClass({ return (
{row.key}
- {isNumeric &&
{row.value_major}
} - {isNumeric &&
{row.value_minor}
} - {!isNumeric &&
- {row.value_major} -
} - {!isNumeric && row.value_minor &&
- {row.value_minor} -
} + { row.value_type === 'numeric' &&
{row.value_major}
} + { row.value_type === 'numeric' &&
{row.value_minor}
} + { row.value_type === 'sparkline' &&
{row.value_major}
} + { row.value_type === 'sparkline' &&
{row.value_minor}
} + { row.value_type !== 'numeric' && row.value_type !== 'sparkline' &&
{row.value_major}
} + { row.value_type !== 'numeric' && row.value_type !== 'sparkline' && row.value_minor &&
{row.value_minor}
}
); })} diff --git a/client/app/scripts/components/sparkline.js b/client/app/scripts/components/sparkline.js new file mode 100644 index 0000000000..f762184a10 --- /dev/null +++ b/client/app/scripts/components/sparkline.js @@ -0,0 +1,129 @@ +// Forked from: https://github.com/KyleAMathews/react-sparkline at commit a9d7c5203d8f240938b9f2288287aaf0478df013 +const React = require('react'); +const d3 = require('d3'); + +const Sparkline = React.createClass({ + getDefaultProps: function() { + return { + width: 100, + height: 16, + 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. + }; + }, + + componentDidMount: function() { + return this.renderSparkline(); + }, + + renderSparkline: function() { + // If the sparkline has already been rendered, remove it. + let el = this.getDOMNode(); + while (el.firstChild) { + el.removeChild(el.firstChild); + } + + let data = this.props.data.slice(); + + // Do nothing if no data is passed in. + if (data.length === 0) { + return; + } + + let x = d3.scale.linear().range([2, this.props.width - 2]); + let 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)); + + let first = this.props.first ? d3.time.format.iso.parse(this.props.first) : d3.min(data, d => d.date); + let 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); + } + + d3.select(this.getDOMNode()).attr('title', title); + + let svg = d3.select(this.getDOMNode()). + 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); + }, + + render: function() { + return ( +
+ ); + }, + + componentDidUpdate: function() { + return this.renderSparkline(); + } +}); + +module.exports = Sparkline; diff --git a/client/app/styles/main.less b/client/app/styles/main.less index 1cb7b1d4f0..8c41679c81 100644 --- a/client/app/styles/main.less +++ b/client/app/styles/main.less @@ -458,6 +458,15 @@ h2 { color: @text-secondary-color; } + &-value-sparkline { + > div { + display: inline-block; + } + span { + margin-left: 1em; + } + } + } }