From 040f9751f209955d7dd37cc8edf6f2c4f9cabdd7 Mon Sep 17 00:00:00 2001 From: Julius Volz Date: Thu, 24 Jan 2013 13:55:32 +0100 Subject: [PATCH] Make graphs linkable, object-oriented, and multiple per page. --- static/css/prometheus.css | 13 +- static/graph.html | 82 ++++++---- static/js/graph.js | 337 ++++++++++++++++++++++++++++++++++++++ static/js/rickshaw.js | 212 ------------------------ 4 files changed, 389 insertions(+), 255 deletions(-) create mode 100644 static/js/graph.js delete mode 100644 static/js/rickshaw.js diff --git a/static/css/prometheus.css b/static/css/prometheus.css index 6cbeb293a..52979a4c4 100644 --- a/static/css/prometheus.css +++ b/static/css/prometheus.css @@ -7,13 +7,12 @@ body { background-color: #eee; } -#chart_container { +.graph_container { font-family: Arial, Helvetica, sans-serif; } -#chart { +.graph { position: relative; - width: 80%; } svg { @@ -21,7 +20,7 @@ svg { margin-bottom: 5px; } -#legend { +.legend { display: inline-block; vertical-align: top; margin: 0 0 0 0px; @@ -35,11 +34,7 @@ input:not([type=submit]):not([type=file]):not([type=button]) { border-radius: 3px; } -input:focus { - border:solid 2px #73A6FF; -} - -.grouping-box { +.grouping_box { position: relative; padding: 5px; margin: 2px; diff --git a/static/graph.html b/static/graph.html index 9e1e68091..de5915066 100644 --- a/static/graph.html +++ b/static/graph.html @@ -11,43 +11,57 @@ - + + + + + + + -
-
- -
- - - - - - - - - - - - - - - - - - - - ajax-spinner - -
-
-
-
-
-
-
-
+
+
diff --git a/static/js/graph.js b/static/js/graph.js new file mode 100644 index 000000000..b90f545d6 --- /dev/null +++ b/static/js/graph.js @@ -0,0 +1,337 @@ +// Graph options we might want: +// Grid off/on +// Stacked off/on +// Area off/on +// Legend position +// Short link +// Graph title +// Palette +// Background +// Enable tooltips +// width/height +// Axis options +// Y-Range min/max +// (X-Range min/max) +// X-Axis format +// Y-Axis format +// Y-Axis title +// X-Axis title +// Log scale + +var Prometheus = Prometheus || {}; +var graphs = []; + +Prometheus.Graph = function(element, options) { + this.el = element; + this.options = options; + this.changeHandler = null; + this.rickshawGraph = null; + this.data = []; + + this.initialize(); +}; + +Prometheus.Graph.timeFactors = { + "y": 60 * 60 * 24 * 365, + "w": 60 * 60 * 24 * 7, + "d": 60 * 60 * 24, + "h": 60 * 60, + "m": 60, + "s": 1 +}; + +Prometheus.Graph.stepValues = [ + "1s", "10s", "1m", "5m", "15m", "30m", "1h", "2h", "6h", "12h", "1d", "2d", + "1w", "2w", "4w", "8w", "1y", "2y" +]; + +Prometheus.Graph.numGraphs = 0; + +Prometheus.Graph.prototype.initialize = function() { + var self = this; + self.id = Prometheus.Graph.numGraphs++; + + // Set default options. + self.options['id'] = self.id; + self.options['range_input'] = self.options['range_input'] || "1h"; + self.options['stacked_checked'] = self.options['stacked'] ? "checked" : ""; + + // Draw graph controls and container from Handlebars template. + var source = $("#graph_template").html(); + var template = Handlebars.compile(source); + var graphHtml = template(self.options); + self.el.append(graphHtml); + + // Get references to all the interesting elements in the graph container and + // bind event handlers. + var graphWrapper = self.el.find("#graph_wrapper" + self.id); + self.queryForm = graphWrapper.find(".query_form"); + self.expr = graphWrapper.find("input[name=expr]"); + self.rangeInput = self.queryForm.find("input[name=range_input]"); + self.stacked = self.queryForm.find("input[name=stacked]"); + + self.graph = graphWrapper.find(".graph"); + self.legend = graphWrapper.find(".legend"); + self.spinner = graphWrapper.find(".spinner"); + self.evalStats = graphWrapper.find(".eval_stats"); + + self.stacked.change(function() { self.updateGraph(); }); + self.queryForm.submit(function() { self.submitQuery(); return false; }); + self.spinner.hide(); + + self.queryForm.find("input[name=inc_range]").click(function() { self.increaseRange(); }); + self.queryForm.find("input[name=dec_range]").click(function() { self.decreaseRange(); }); + self.expr.focus(); // TODO: move to external Graph method. + + if (self.expr.val()) { + self.submitQuery(); + } +}; + +Prometheus.Graph.prototype.onChange = function(handler) { + this.changeHandler = handler; +}; + +Prometheus.Graph.prototype.getOptions = function(handler) { + var self = this; + var options = {}; + + var optionInputs = [ + "expr", + "range_input", + "end", + "step_input", + "stacked" + ]; + + self.queryForm.find("input").each(function(index, element) { + var name = element.name; + if ($.inArray(name, optionInputs) >= 0) { + if (name == "stacked") { + options[name] = element.checked; + } else { + options[name] = element.value; + } + } + }); + return options; +}; + +Prometheus.Graph.prototype.parseRange = function(rangeText) { + var rangeRE = new RegExp("^([0-9]+)([ywdhms]+)$"); + var matches = rangeText.match(rangeRE); + if (matches.length != 3) { + return 60; + } + var value = parseInt(matches[1]); + var unit = matches[2]; + return value * Prometheus.Graph.timeFactors[unit]; +}; + +Prometheus.Graph.prototype.increaseRange = function() { + var self = this; + var rangeSeconds = self.parseRange(self.rangeInput.val()); + for (var i = 0; i < Prometheus.Graph.stepValues.length; i++) { + if (rangeSeconds < self.parseRange(Prometheus.Graph.stepValues[i])) { + self.rangeInput.val(Prometheus.Graph.stepValues[i]); + if (self.expr.val()) { + self.submitQuery(); + } + return; + } + } +}; + +Prometheus.Graph.prototype.decreaseRange = function() { + var self = this; + var rangeSeconds = self.parseRange(self.rangeInput.val()); + for (var i = Prometheus.Graph.stepValues.length - 1; i >= 0; i--) { + if (rangeSeconds > self.parseRange(Prometheus.Graph.stepValues[i])) { + self.rangeInput.val(Prometheus.Graph.stepValues[i]); + if (self.expr.val()) { + self.submitQuery(); + } + return; + } + } +}; + +Prometheus.Graph.prototype.submitQuery = function() { + var self = this; + + self.spinner.show(); + self.evalStats.empty(); + + var startTime = new Date().getTime(); + + var rangeSeconds = self.parseRange(self.rangeInput.val()); + self.queryForm.find("input[name=range]").val(rangeSeconds); + var resolution = self.queryForm.find("input[name=step_input]").val() || Math.max(Math.floor(rangeSeconds / 250), 1); + self.queryForm.find("input[name=step]").val(resolution); + + $.ajax({ + method: self.queryForm.attr("method"), + url: self.queryForm.attr("action"), + dataType: "json", + data: self.queryForm.serialize(), + success: function(json, textStatus) { + if (json.Type == "error") { + alert(json.Value); + return; + } + self.data = self.transformData(json); + if (self.data.length == 0) { + alert("No datapoints found."); + return; + } + self.updateGraph(true); + }, + error: function() { + alert("Error executing query!"); + }, + complete: function() { + var duration = new Date().getTime() - startTime; + self.evalStats.html("Load time: " + duration + "ms, resolution: " + resolution + "s"); + self.spinner.hide(); + } + }); +}; + +Prometheus.Graph.prototype.metricToTsName = function(labels) { + var tsName = labels["name"] + "{"; + var labelStrings = []; + for (label in labels) { + if (label != "name") { + labelStrings.push(label + "='" + labels[label] + "'"); + } + } + tsName += labelStrings.join(",") + "}"; + return tsName; +}; + +Prometheus.Graph.prototype.parseValue = function(value) { + if (value == "NaN" || value == "Inf" || value == "-Inf") { + return 0; // TODO: what should we really do here? + } else { + return parseFloat(value) + } +}; + +Prometheus.Graph.prototype.transformData = function(json) { + self = this; + var palette = new Rickshaw.Color.Palette(); + if (json.Type != "matrix") { + alert("Result is not of matrix type! Please enter a correct expression."); + return []; + } + var data = json.Value.map(function(ts) { + return { + name: self.metricToTsName(ts.Metric), + data: ts.Values.map(function(value) { + return { + x: value.Timestamp, + y: self.parseValue(value.Value) + } + }), + color: palette.color() + }; + }); + Rickshaw.Series.zeroFill(data); + return data; +}; + +Prometheus.Graph.prototype.showGraph = function() { + var self = this; + self.rickshawGraph = new Rickshaw.Graph({ + element: self.graph[0], + height: 800, + renderer: (self.stacked.is(":checked") ? "stack" : "line"), + interpolation: "linear", + series: self.data + }); + + var xAxis = new Rickshaw.Graph.Axis.Time({ graph: self.rickshawGraph }); + + var yAxis = new Rickshaw.Graph.Axis.Y({ + graph: self.rickshawGraph, + tickFormat: Rickshaw.Fixtures.Number.formatKMBT, + }); + + self.rickshawGraph.render(); +}; + +Prometheus.Graph.prototype.updateGraph = function(reloadGraph) { + var self = this; + if (self.data.length == 0) { return; } + if (self.rickshawGraph == null || reloadGraph) { + self.graph.empty(); + self.legend.empty(); + self.showGraph(); + } else { + self.rickshawGraph.configure({ + renderer: (self.stacked.is(":checked") ? "stack" : "line"), + interpolation: "linear", + series: self.data + }); + self.rickshawGraph.render(); + } + + var hoverDetail = new Rickshaw.Graph.HoverDetail({ + graph: self.rickshawGraph + }); + + var legend = new Rickshaw.Graph.Legend({ + element: self.legend[0], + graph: self.rickshawGraph + }); + + var shelving = new Rickshaw.Graph.Behavior.Series.Toggle({ + graph: self.rickshawGraph, + legend: legend + }); + + self.changeHandler(); +}; + +function parseGraphOptionsFromUrl() { + var hashOptions = window.location.hash.slice(1); + if (!hashOptions) { + return []; + } + var optionsJSON = decodeURIComponent(window.location.hash.slice(1)); + return JSON.parse(optionsJSON); +} + +function storeGraphOptionsInUrl(options) { + var allGraphsOptions = []; + for (var i = 0; i < graphs.length; i++) { + allGraphsOptions.push(graphs[i].getOptions()); + console.log(graphs[i].id); + console.log(graphs[i].getOptions()); + } + var optionsJSON = JSON.stringify(allGraphsOptions); + window.location.hash = encodeURIComponent(optionsJSON); +} + +function addGraph(options) { + var graph = new Prometheus.Graph($("#graph_container"), options); + graphs.push(graph); + graph.onChange(function() { + storeGraphOptionsInUrl(); + }); +} + +function init() { + jQuery.ajaxSetup({ + cache: false + }); + + var options = parseGraphOptionsFromUrl(); + for (var i = 0; i < options.length; i++) { + addGraph(options[i]); + } + + $("#add_graph").click(function() { addGraph({}); }); +} + +$(init); diff --git a/static/js/rickshaw.js b/static/js/rickshaw.js deleted file mode 100644 index 13b318dd7..000000000 --- a/static/js/rickshaw.js +++ /dev/null @@ -1,212 +0,0 @@ -// Graph options we might want: -// Grid off/on -// Stacked off/on -// Area off/on -// Legend position -// Short link -// Graph title -// Palette -// Background -// Enable tooltips -// width/height -// Axis options -// Y-Range min/max -// (X-Range min/max) -// X-Axis format -// Y-Axis format -// Y-Axis title -// X-Axis title -// Log scale - -var graph = null; -var data = []; - -var timeFactors = { - "y": 60 * 60 * 24 * 365, - "w": 60 * 60 * 24 * 7, - "d": 60 * 60 * 24, - "h": 60 * 60, - "m": 60, - "s": 1 -}; - -var steps = ["1s", "10s", "1m", "5m", "15m", "30m", "1h", "2h", "6h", "12h", - "1d", "2d", "1w", "2w", "4w", "8w", "1y", "2y"]; - -function parseRange(rangeText) { - var rangeRE = new RegExp("^([0-9]+)([ywdhms]+)$"); - var matches = rangeText.match(rangeRE); - if (matches.length != 3) { - return 60; - } - var value = parseInt(matches[1]); - var unit = matches[2]; - return value * timeFactors[unit]; -} - -function increaseRange() { - var rangeSeconds = parseRange($("#range_input").val()); - for (var i = 0; i < steps.length; i++) { - if (rangeSeconds < parseRange(steps[i])) { - $("#range_input").val(steps[i]); - submitQuery(); - return; - } - } -} - -function decreaseRange() { - var rangeSeconds = parseRange($("#range_input").val()); - for (var i = steps.length - 1; i >= 0; i--) { - if (rangeSeconds > parseRange(steps[i])) { - $("#range_input").val(steps[i]); - submitQuery(); - return; - } - } -} - -function submitQuery() { - $("#spinner").show(); - $("#eval_stats").empty(); - - var form = $("#query_form"); - var startTime = new Date().getTime(); - - var rangeSeconds = parseRange($("#range_input").val()); - $("#range").val(rangeSeconds); - var resolution = $("#step_input").val() || Math.max(Math.floor(rangeSeconds / 250), 1); - $("#step").val(resolution); - - $.ajax({ - method: form.attr("method"), - url: form.attr("action"), - dataType: "json", - data: form.serialize(), - success: function(json, textStatus) { - if (json.Type == "error") { - alert(json.Value); - return; - } - data = transformData(json); - if (data.length == 0) { - alert("No datapoints found."); - return; - } - updateGraph(true); - }, - error: function() { - alert("Error executing query!"); - }, - complete: function() { - var duration = new Date().getTime() - startTime; - $("#eval_stats").html("Load time: " + duration + "ms, resolution: " + resolution + "s"); - $("#spinner").hide(); - } - }); - return false; -} - -function metricToTsName(labels) { - var tsName = labels["name"] + "{"; - var labelStrings = []; - for (label in labels) { - if (label != "name") { - labelStrings.push(label + "='" + labels[label] + "'"); - } - } - tsName += labelStrings.join(",") + "}"; - return tsName; -} - -function parseValue(value) { - if (value == "NaN" || value == "Inf" || value == "-Inf") { - return 0; // TODO: what should we really do here? - } else { - return parseFloat(value) - } -} - -function transformData(json) { - var palette = new Rickshaw.Color.Palette(); - if (json.Type != "matrix") { - alert("Result is not of matrix type! Please enter a correct expression."); - return []; - } - var data = json.Value.map(function(ts) { - return { - name: metricToTsName(ts.Metric), - data: ts.Values.map(function(value) { - return { - x: value.Timestamp, - y: parseValue(value.Value) - } - }), - color: palette.color() - }; - }); - Rickshaw.Series.zeroFill(data); - return data; -} - -function showGraph() { - graph = new Rickshaw.Graph({ - element: document.querySelector("#chart"), - height: 800, - renderer: ($("#stacked").is(":checked") ? "stack" : "line"), - interpolation: "linear", - series: data - }); - - var x_axis = new Rickshaw.Graph.Axis.Time({ graph: graph }); - - var y_axis = new Rickshaw.Graph.Axis.Y({ - graph: graph, - tickFormat: Rickshaw.Fixtures.Number.formatKMBT, - }); - - graph.render(); -} - -function updateGraph(reloadGraph) { - if (graph == null || reloadGraph) { - $("#chart").empty(); - $("#legend").empty(); - showGraph(); - } else { - graph.configure({ - renderer: ($("#stacked").is(":checked") ? "stack" : "line"), - interpolation: "linear", - series: data - }); - graph.render(); - } - - var hoverDetail = new Rickshaw.Graph.HoverDetail({ - graph: graph - }); - - var legend = new Rickshaw.Graph.Legend({ - element: document.querySelector("#legend"), - graph: graph - }); - - var shelving = new Rickshaw.Graph.Behavior.Series.Toggle({ - graph: graph, - legend: legend - }); -} - -function init() { - jQuery.ajaxSetup({ - cache: false - }); - $("#spinner").hide(); - $("#query_form").submit(submitQuery); - $("#inc_range").click(increaseRange); - $("#dec_range").click(decreaseRange); - $("#stacked").change(updateGraph); - $("#expr").focus(); -} - -$(init);