Skip to content

Commit

Permalink
normalize the load graphs so they all render on the same y-axis
Browse files Browse the repository at this point in the history
  • Loading branch information
paulbellamy committed Nov 5, 2015
1 parent 0afb3c9 commit 540838c
Show file tree
Hide file tree
Showing 6 changed files with 4,772 additions and 4,734 deletions.
9,312 changes: 4,594 additions & 4,718 deletions app/static.go

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions client/app/scripts/components/node-details-table.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const React = require('react');
const Sparkline = require('react-sparkline');
const Sparkline = require('./sparkline');

const NodeDetailsTable = React.createClass({

Expand All @@ -16,7 +16,7 @@ const NodeDetailsTable = React.createClass({
<div className="node-details-table-row-key truncate" title={row.key}>{row.key}</div>
{ row.value_type === 'numeric' && <div className="node-details-table-row-value-scalar">{row.value_major}</div> }
{ row.value_type === 'numeric' && <div className="node-details-table-row-value-unit">{row.value_minor}</div> }
{ row.value_type === 'sparkline' && <div className="node-details-table-row-value-sparkline"><Sparkline data={row.metric} /></div> }
{ row.value_type === 'sparkline' && <div className="node-details-table-row-value-sparkline"><Sparkline data={row.metric.samples} min={0} max={row.metric.max} interpolate="none" />{row.value_major}</div> }
{ row.value_type === 'sparkline' && <div className="node-details-table-row-value-unit">{row.value_minor}</div> }
{ row.value_type !== 'numeric' && row.value_type !== 'sparkline' && <div className="node-details-table-row-value-major truncate" title={row.value_major}>{row.value_major}</div> }
{ row.value_type !== 'numeric' && row.value_type !== 'sparkline' && row.value_minor && <div className="node-details-table-row-value-minor truncate" title={row.value_minor}>{row.value_minor}</div> }
Expand Down
118 changes: 118 additions & 0 deletions client/app/scripts/components/sparkline.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// 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: 'black',
strokeWidth: '0.5px',
interpolate: 'basis',
circleDiameter: 1.5,
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;
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));

x.domain(d3.extent(data, d => d.date));

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);
} else {
line = d3.svg.line().
interpolate(this.props.interpolate).
x((d, i) => x(i)).
y(d => y(d));

x.domain([0, 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]);
}

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', 'red').
attr('stroke', 'none').
attr('r', this.props.circleDiameter);
},

render: function() {
return (
<div/>
);
},

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

module.exports = Sparkline;
5 changes: 2 additions & 3 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@
"materialize-css": "0.96.1",
"object-assign": "2.0.0",
"page": "1.6.3",
"react": "~0.14.2",
"react": "~0.13.3",
"react-motion": "0.2.7",
"react-sparkline": "2.0.0",
"react-tap-event-plugin": "0.2.1",
"react-tap-event-plugin": "0.1.7",
"reqwest": "~1.1.5",
"timely": "0.1.0"
},
Expand Down
22 changes: 21 additions & 1 deletion render/detailed_node.go
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,21 @@ func getDockerLabelRows(nmd report.Node) []Row {
}

func hostOriginTable(nmd report.Node) (Table, bool) {
// Ensure that all metrics have the same max
var (
maxLoad = 0.0
)
for _, key := range []string{host.Load1, host.Load5, host.Load15} {
if metric, ok := nmd.Metrics[key]; ok {
if len(metric.Samples) == 0 {
continue
}
if metric.Max > maxLoad {
maxLoad = metric.Max
}
}
}

rows := []Row{}
for _, tuple := range []struct{ key, human string }{
// TODO(paulbellamy): render this as a sparkline and number
Expand All @@ -405,7 +420,12 @@ func hostOriginTable(nmd report.Node) (Table, bool) {
if val, ok := nmd.Metadata[tuple.key]; ok {
rows = append(rows, Row{Key: tuple.human, ValueMajor: val, ValueMinor: ""})
} else if val, ok := nmd.Metrics[tuple.key]; ok {
rows = append(rows, Row{Key: tuple.human, Metric: val, ValueType: "sparkline"})
lastStr := ""
if last := val.LastSample(); last != nil {
lastStr = fmt.Sprint(last.Value)
}
val.Max = maxLoad
rows = append(rows, Row{Key: tuple.human, ValueMajor: lastStr, Metric: val, ValueType: "sparkline"})
}
}

Expand Down
45 changes: 35 additions & 10 deletions report/topology.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,11 @@ func (m Metrics) Copy() Metrics {

// Metric is a list of timeseries data. Clients must use the Add
// method to add values.
type Metric []Sample
type Metric struct {
Samples []Sample `json:"samples"`
Min float64 `json:"min"`
Max float64 `json:"max"`
}

type Sample struct {
Timestamp time.Time `json:"date"`
Expand All @@ -370,31 +374,52 @@ func MakeMetric() Metric {
// Add adds the sample to the Metric. Add is the only valid way to grow a
// Metric. Add returns the Metric to enable chaining.
func (m Metric) Add(t time.Time, v float64) Metric {
i := sort.Search(len(m), func(i int) bool { return t.Before(m[i].Timestamp) })
if i < len(m) && m[i].Timestamp.Equal(t) {
i := sort.Search(len(m.Samples), func(i int) bool { return t.Before(m.Samples[i].Timestamp) })
if i < len(m.Samples) && m.Samples[i].Timestamp.Equal(t) {
// The list already has the element.
return m
}
// It is a new element, insert it in order.
m = append(m, Sample{})
copy(m[i+1:], m[i:])
m[i] = Sample{Timestamp: t, Value: v}
m.Samples = append(m.Samples, Sample{})
copy(m.Samples[i+1:], m.Samples[i:])
m.Samples[i] = Sample{Timestamp: t, Value: v}
if v > m.Max {
m.Max = v
}
if v < m.Min {
m.Min = v
}
if m.First.IsZero() || t.Before(m.First) {
m.First = t
}
if m.Last.IsZero() || t.After(m.Last) {
m.Last = t
}
return m
}

// Merge combines the two Metrics and returns a new result.
func (m Metric) Merge(other Metric) Metric {
for _, sample := range other {
for _, sample := range other.Samples {
m = m.Add(sample.Timestamp, sample.Value)
}
return m
}

// Copy returns a value copy of the Metric.
func (m Metric) Copy() Metric {
result := make(Metric, len(m))
copy(result, m)
return result
samples := make([]Sample, len(m.Samples))
copy(samples, m.Samples)
return Metric{Samples: samples, Max: m.Max, Min: m.Min}
}

// Last returns the last sample in the metric.
// Returns nil if there are no samples.
func (m Metric) LastSample() *Sample {
if len(m.Samples) == 0 {
return nil
}
return &m.Samples[len(m.Samples)-1]
}

// EdgeMetadatas collect metadata about each edge in a topology. Keys are the
Expand Down

0 comments on commit 540838c

Please sign in to comment.