blob: ada32a1010f09b9b924a225bf697df79253fa13b [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/**
* A graph showing multiple metrics in different colors along with their
* summary items showing their current/pass values and line colors. User can
* "select" one or more metrics by clicking on their summary items. The graph
* then will only show selected metrics (if the selection is not empty).
*
* The graph will also overlay a graph of the metric stored in the
* "hoveredMetric" observable passed from some other component.
*/
var hg = require('mercury');
var h = require('mercury').h;
var svg = require('virtual-dom/virtual-hyperscript/svg');
var dateformat = require('dateformat');
var Consts = require('../../constants');
var MouseMoveEvent = require('../../mousemove-handler');
var Util = require('../../util.js');
module.exports = create;
module.exports.render = render;
var colors = [
'#16a085',
'#2980b9',
'#8e44ad',
'#808B96',
'#f39c12',
'#d35400',
'#27ae60',
'#c0392b'
];
/** Constructor. */
function create(data) {
// Transform the raw data to metrics array.
// Also calculate time and value range across all metrics.
var minTime = Number.MAX_VALUE;
var maxTime = 0;
var minValue = Number.MAX_VALUE;
var maxValue = 0;
var metrics = data.metrics.map(function(metric, index) {
minTime = Math.min(minTime, metric.MinTime);
maxTime = Math.max(maxTime, metric.MaxTime);
minValue = Math.min(minValue, metric.MinValue);
maxValue = Math.max(maxValue, metric.MaxValue);
return {
label: metric.Name,
value: metric.CurrentValue,
color: colors[index % colors.length],
threshold: metric.Threshold,
healthy: metric.Healthy,
historyTimestamps: metric.HistoryTimestamps,
historyValues: metric.HistoryValues,
};
});
// Calculate the overall health for all metrics.
var healthy = true;
metrics.forEach(function(metric) {
healthy &= metric.healthy;
});
var state = hg.state({
// The timestamp when the data is loaded from the backend server.
collectionTimestamp: data.collectionTimestamp,
// The graph title.
title: data.title,
// Time range.
minTime: minTime,
maxTime: maxTime,
// Value range.
minValue: minValue,
maxValue: maxValue,
// Metrics shown in this graph.
metrics: metrics,
// Overall health.
healthy: healthy,
// Keep track of selected metrics.
// A metric line will be visible if it is selected.
selectedMetrics: hg.varhash({}),
// See comments in instance-view/index.js.
mouseOffsetFactor: data.mouseOffsetFactor,
hoveredMetric: data.hoveredMetric,
channels: {
mouseMove: mouseMove,
mouseOut: mouseOut,
mouseClickOnSummaryItem: mouseClickOnSummaryItem
}
});
return state;
}
/** Callback when mouse is moving on the graph. */
function mouseMove(state, data) {
if (state.mouseOffsetFactor() !== data.f) {
state.mouseOffsetFactor.set(data.f);
}
}
/** Callback when mouse moves out of the graph. */
function mouseOut(state) {
state.mouseOffsetFactor.set(-1);
}
/** Callback when user clicks on a metric's summary item. */
function mouseClickOnSummaryItem(state, label) {
if (!state.selectedMetrics.get(label)) {
state.selectedMetrics.put(label, 1);
} else {
state.selectedMetrics.delete(label);
}
}
/** The main render function. */
function render(state) {
// Render graphs.
var items = [
renderContent(state),
renderThreshold(state)
];
// Render an overlay graph for the metric stored in state.hoveredMetric.
if (Object.keys(state.hoveredMetric).length !== 0) {
var metric = state.hoveredMetric;
var numPoints = metric.historyTimestamps.length;
var minTime = metric.historyTimestamps[0];
var maxTime = metric.historyTimestamps[numPoints - 1];
items.push(svg('svg', {
'viewBox': '0 0 100 100',
'preserveAspectRatio': 'none'
}, [
Util.renderMetric(state.hoveredMetric,
minTime, maxTime, metric.minValue, metric.maxValue, 1, '#AAA', 1)
]));
}
// Render mouse line.
var offset = 100 * state.mouseOffsetFactor;
items.push(svg('svg', {
'class': 'overlay',
'viewBox': '0 0 100 100',
'preserveAspectRatio': 'none'
}, [
svg('polyline', {
'points': offset + ',0 ' + offset + ',100'
})
]));
var endDate = new Date(state.collectionTimestamp * 1000);
var startDate = new Date((state.collectionTimestamp - 3600) * 1000);
return h('div.full-graph-container', [
h('div.full-graph-title', state.title),
h('div.content-container', [
// The summary row at the top of the graph.
renderSummaryRow(state),
// The main graph.
h('div.full-graph', {
'ev-mousemove': new MouseMoveEvent(state.channels.mouseMove),
'ev-mouseout': hg.send(state.channels.mouseOut)
}, items),
// The time axis at the bottom.
h('div.time', [
h('div.time-label', dateformat(startDate, 'HH:MM')),
h('div.time-label', dateformat(endDate, 'HH:MM'))
])
])
]);
}
/** Renders the main graph. */
function renderContent(state) {
var items = state.metrics.map(function(metric) {
var strokeOpacity = isMetricVisible(state.selectedMetrics, metric.label) ?
1 : 0.1;
var metricLine = Util.renderMetric(
metric, state.minTime, state.maxTime,
state.minValue, state.maxValue, 1.5, metric.color, strokeOpacity);
return metricLine;
});
return svg('svg', {
'class': 'content',
'viewBox': '0 0 100 100',
'preserveAspectRatio': 'none'
}, items);
}
/** Renders the threshold line. */
function renderThreshold(state) {
var minThreshold = Number.MAX_VALUE;
state.metrics.forEach(function(metric) {
minThreshold = Math.min(minThreshold, metric.threshold);
});
var thresholdOffset = Util.getOffsetForValue(
minThreshold, state.minValue, state.maxValue);
return svg('svg', {
'class': 'threshold-line',
'viewBox': '0 0 100 100',
'preserveAspectRatio': 'none',
}, [
svg('path', {
'd': 'M0 ' + thresholdOffset + ' L 100 ' + thresholdOffset,
'stroke-dasharray': '2,2'
})
]);
}
/** Renders the summary row. */
function renderSummaryRow(state) {
var items = state.metrics.map(function(metric) {
var curValue = Util.formatValue(
Util.interpolateValue(metric.value, state.mouseOffsetFactor,
metric.historyTimestamps, metric.historyValues));
var metricSummaryClassNames = [];
if (!isMetricVisible(state.selectedMetrics, metric.label)) {
metricSummaryClassNames.push('hidden');
}
if (!metric.healthy) {
metricSummaryClassNames.push('unhealthy');
}
var metricValueClassNames = [];
if (state.mouseOffsetFactor >= 0) {
metricValueClassNames.push('historyValue');
}
if (!metric.healthy) {
metricValueClassNames.push('unhealthy');
}
return h('div.metric-summary-item', {
className: metricSummaryClassNames.join(' '),
'ev-click': hg.send(state.channels.mouseClickOnSummaryItem, metric.label)
}, [
h('div.metric-summary-title', Consts.getDisplayName(metric.label)),
h('div.metric-summary-value', {
className: metricValueClassNames.join(' ')
}, curValue),
h('div.metric-summary-color', {
'style': {backgroundColor: metric.color}
})
]);
});
return h('div.metric-summary-container', items);
}
/**
* Checks whether the given metric is visible based on the metrics stored in
* the selectedMetrics object.
* @param {Object} selectedMetrics - An object indexed by metric names.
* @param {string} metric - The name of the metric to check.
* @return {boolean}
*/
function isMetricVisible(selectedMetrics, metric) {
return Object.keys(selectedMetrics).length === 0 || selectedMetrics[metric];
}