http://git-wip-us.apache.org/repos/asf/ambari/blob/79f3a1a3/ambari-web/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric3_view.js ---------------------------------------------------------------------- diff --git a/ambari-web/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric3_view.js b/ambari-web/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric3_view.js new file mode 100644 index 0000000..66e7917 --- /dev/null +++ b/ambari-web/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric3_view.js @@ -0,0 +1,61 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +/** + * @class + * + * This is a view for showing cluster CPU metrics + * + * @extends App.ChartView + * @extends Ember.Object + * @extends Ember.View + */ +App.Metric3View = App.ChartView.extend({ + id: "service-metrics-hdfs-file-operations", + title: 'File Operations', + renderer: 'line', + + ajaxIndex: 'metrics3', + yAxisFormatter: App.ChartView.CreateRateFormatter('ops', App.ChartView.DefaultFormatter), + + transformToSeries: function (jsonData) { + var seriesArray = []; + if (jsonData && jsonData.metrics && jsonData.metrics.dfs && jsonData.metrics.dfs.namenode) { + for ( var name in jsonData.metrics.dfs.namenode) { + var displayName; + var seriesData = jsonData.metrics.dfs.namenode[name]; + switch (name) { + case "FileInfoOps": + displayName = 'File Info Ops'; + break; + case "DeleteFileOps": + displayName = 'Delete File Ops'; + break; + case "CreateFileOps": + displayName = 'Create File Ops'; + break; + default: + break; + } + if (seriesData) { + seriesArray.push(this.transformData(seriesData, displayName)); + } + } + } + return seriesArray; + } +});
http://git-wip-us.apache.org/repos/asf/ambari/blob/79f3a1a3/ambari-web/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric4_view.js ---------------------------------------------------------------------- diff --git a/ambari-web/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric4_view.js b/ambari-web/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric4_view.js new file mode 100644 index 0000000..9f9298b --- /dev/null +++ b/ambari-web/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric4_view.js @@ -0,0 +1,54 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +/** + * @class + * + * This is a view for showing cluster CPU metrics + * + * @extends App.ChartView + * @extends Ember.Object + * @extends Ember.View + */ +App.Metric4View = App.ChartView.extend({ + id: "service-metrics-hdfs-rpc", + title: 'RPC', + yAxisFormatter: App.ChartView.TimeElapsedFormatter, + + ajaxIndex: 'metrics4', + + transformToSeries: function (jsonData) { + var seriesArray = []; + if (jsonData && jsonData.metrics && jsonData.metrics.rpc) { + for ( var name in jsonData.metrics.rpc) { + var displayName; + var seriesData = jsonData.metrics.rpc[name]; + switch (name) { + case "RpcQueueTime_avg_time": + displayName = 'RPC Queue Time Avg Time'; + break; + default: + break; + } + if (seriesData) { + seriesArray.push(this.transformData(seriesData, displayName)); + } + } + } + return seriesArray; + } +}); http://git-wip-us.apache.org/repos/asf/ambari/blob/79f3a1a3/ambari-web/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric_view.js ---------------------------------------------------------------------- diff --git a/ambari-web/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric_view.js b/ambari-web/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric_view.js new file mode 100644 index 0000000..61a5739 --- /dev/null +++ b/ambari-web/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric_view.js @@ -0,0 +1,70 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +/** + * @class + * + * This is a view for showing cluster CPU metrics + * + * @extends App.ChartView + * @extends Ember.Object + * @extends Ember.View + */ +App.MetricView = App.ChartView.extend({ + + id: "service-metrics-hdfs-space-utilization", + + title: 'Space Utilization', + + yAxisFormatter: App.ChartView.BytesFormatter, + + renderer: 'line', + + ajaxIndex: 'metrics', + + transformToSeries: function (jsonData) { + var seriesArray = []; + var GB = Math.pow(2, 30); + if (jsonData && jsonData.metrics && jsonData.metrics.dfs && jsonData.metrics.dfs.FSNamesystem) { + for ( var name in jsonData.metrics.dfs.FSNamesystem) { + var displayName; + var seriesData = jsonData.metrics.dfs.FSNamesystem[name]; + switch (name) { + case "CapacityRemainingGB": + displayName = 'Capacity Remaining GB'; + break; + case "CapacityUsedGB": + displayName = 'Capacity Used GB'; + break; + case "CapacityTotalGB": + displayName = 'Capacity Total GB'; + break; + default: + break; + } + if (seriesData) { + var s = this.transformData(seriesData, displayName); + for (var i = 0; i < s.data.length; i++) { + s.data[i].y *= GB; + } + seriesArray.push(s); + } + } + } + return seriesArray; + } +}); http://git-wip-us.apache.org/repos/asf/ambari/blob/79f3a1a3/ambari-web/contrib/views/slider/src/main/resources/ui/app/views/slider_app/summary_view.js ---------------------------------------------------------------------- diff --git a/ambari-web/contrib/views/slider/src/main/resources/ui/app/views/slider_app/summary_view.js b/ambari-web/contrib/views/slider/src/main/resources/ui/app/views/slider_app/summary_view.js new file mode 100644 index 0000000..bad2eb9 --- /dev/null +++ b/ambari-web/contrib/views/slider/src/main/resources/ui/app/views/slider_app/summary_view.js @@ -0,0 +1,27 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +App.SliderAppSummaryView = Ember.View.extend({ + + classNames: ['app_summary'], + + graphs: [ + [App.MetricView, App.Metric2View, App.Metric3View, App.Metric4View] + ] + +}); http://git-wip-us.apache.org/repos/asf/ambari/blob/79f3a1a3/ambari-web/contrib/views/slider/src/main/resources/ui/vendor/scripts/common/cubism.v1.js ---------------------------------------------------------------------- diff --git a/ambari-web/contrib/views/slider/src/main/resources/ui/vendor/scripts/common/cubism.v1.js b/ambari-web/contrib/views/slider/src/main/resources/ui/vendor/scripts/common/cubism.v1.js new file mode 100644 index 0000000..be9ff4d --- /dev/null +++ b/ambari-web/contrib/views/slider/src/main/resources/ui/vendor/scripts/common/cubism.v1.js @@ -0,0 +1,1085 @@ +(function (exports) { + var cubism = exports.cubism = {version:"1.2.0"}; + var cubism_id = 0; + + function cubism_identity(d) { + return d; + } + + cubism.option = function (name, defaultValue) { + var values = cubism.options(name); + return values.length ? values[0] : defaultValue; + }; + + cubism.options = function (name, defaultValues) { + var options = location.search.substring(1).split("&"), + values = [], + i = -1, + n = options.length, + o; + while (++i < n) { + if ((o = options[i].split("="))[0] == name) { + values.push(decodeURIComponent(o[1])); + } + } + return values.length || arguments.length < 2 ? values : defaultValues; + }; + cubism.context = function () { + var context = new cubism_context, + step = 1e4, // ten seconds, in milliseconds + size = 1440, // four hours at ten seconds, in pixels + start0, stop0, // the start and stop for the previous change event + start1, stop1, // the start and stop for the next prepare event + serverDelay = 5e3, + clientDelay = 5e3, + event = d3.dispatch("prepare", "beforechange", "change", "focus"), + scale = context.scale = d3.time.scale().range([0, size]), + timeout, + focus; + + function update() { + var now = Date.now(); + stop0 = new Date(Math.floor((now - serverDelay - clientDelay) / step) * step); + start0 = new Date(stop0 - size * step); + stop1 = new Date(Math.floor((now - serverDelay) / step) * step); + start1 = new Date(stop1 - size * step); + scale.domain([start0, stop0]); + return context; + } + + context.start = function () { + if (timeout) clearTimeout(timeout); + var delay = +stop1 + serverDelay - Date.now(); + + // If we're too late for the first prepare event, skip it. + if (delay < clientDelay) delay += step; + + timeout = setTimeout(function prepare() { + stop1 = new Date(Math.floor((Date.now() - serverDelay) / step) * step); + start1 = new Date(stop1 - size * step); + event.prepare.call(context, start1, stop1); + + setTimeout(function () { + scale.domain([start0 = start1, stop0 = stop1]); + event.beforechange.call(context, start1, stop1); + event.change.call(context, start1, stop1); + event.focus.call(context, focus); + }, clientDelay); + + timeout = setTimeout(prepare, step); + }, delay); + return context; + }; + + context.stop = function () { + timeout = clearTimeout(timeout); + return context; + }; + + timeout = setTimeout(context.start, 10); + + // Set or get the step interval in milliseconds. + // Defaults to ten seconds. + context.step = function (_) { + if (!arguments.length) return step; + step = +_; + return update(); + }; + + // Set or get the context size (the count of metric values). + // Defaults to 1440 (four hours at ten seconds). + context.size = function (_) { + if (!arguments.length) return size; + scale.range([0, size = +_]); + return update(); + }; + + // The server delay is the amount of time we wait for the server to compute a + // metric. This delay may result from clock skew or from delays collecting + // metrics from various hosts. Defaults to 4 seconds. + context.serverDelay = function (_) { + if (!arguments.length) return serverDelay; + serverDelay = +_; + return update(); + }; + + // The client delay is the amount of additional time we wait to fetch those + // metrics from the server. The client and server delay combined represent the + // age of the most recent displayed metric. Defaults to 1 second. + context.clientDelay = function (_) { + if (!arguments.length) return clientDelay; + clientDelay = +_; + return update(); + }; + + // Sets the focus to the specified index, and dispatches a "focus" event. + context.focus = function (i) { + event.focus.call(context, focus = i); + return context; + }; + + // Add, remove or get listeners for events. + context.on = function (type, listener) { + if (arguments.length < 2) return event.on(type); + + event.on(type, listener); + + // Notify the listener of the current start and stop time, as appropriate. + // This way, metrics can make requests for data immediately, + // and likewise the axis can display itself synchronously. + if (listener != null) { + if (/^prepare(\.|$)/.test(type)) listener.call(context, start1, stop1); + if (/^beforechange(\.|$)/.test(type)) listener.call(context, start0, stop0); + if (/^change(\.|$)/.test(type)) listener.call(context, start0, stop0); + if (/^focus(\.|$)/.test(type)) listener.call(context, focus); + } + + return context; + }; + + d3.select(window).on("keydown.context-" + ++cubism_id, function () { + switch (!d3.event.metaKey && d3.event.keyCode) { + case 37: // left + if (focus == null) focus = size - 1; + if (focus > 0) context.focus(--focus); + break; + case 39: // right + if (focus == null) focus = size - 2; + if (focus < size - 1) context.focus(++focus); + break; + default: + return; + } + d3.event.preventDefault(); + }); + + return update(); + }; + + function cubism_context() { + } + + var cubism_contextPrototype = cubism.context.prototype = cubism_context.prototype; + + cubism_contextPrototype.constant = function (value) { + return new cubism_metricConstant(this, +value); + }; + cubism_contextPrototype.cube = function (host) { + if (!arguments.length) host = ""; + var source = {}, + context = this; + + source.metric = function (expression) { + return context.metric(function (start, stop, step, callback) { + d3.json(host + "/1.0/metric" + + "?expression=" + encodeURIComponent(expression) + + "&start=" + cubism_cubeFormatDate(start) + + "&stop=" + cubism_cubeFormatDate(stop) + + "&step=" + step, function (data) { + if (!data) return callback(new Error("unable to load data")); + callback(null, data.map(function (d) { + return d.value; + })); + }); + }, expression += ""); + }; + + // Returns the Cube host. + source.toString = function () { + return host; + }; + + return source; + }; + + var cubism_cubeFormatDate = d3.time.format.iso; + cubism_contextPrototype.graphite = function (host) { + if (!arguments.length) host = ""; + var source = {}, + context = this; + + source.metric = function (expression) { + var sum = "sum"; + + var metric = context.metric(function (start, stop, step, callback) { + var target = expression; + + // Apply the summarize, if necessary. + if (step !== 1e4) target = "summarize(" + target + ",'" + + (!(step % 36e5) ? step / 36e5 + "hour" : !(step % 6e4) ? step / 6e4 + "min" : step + "sec") + + "','" + sum + "')"; + + d3.text(host + "/render?format=raw" + + "&target=" + encodeURIComponent("alias(" + target + ",'')") + + "&from=" + cubism_graphiteFormatDate(start - 2 * step) // off-by-two? + + "&until=" + cubism_graphiteFormatDate(stop - 1000), function (text) { + if (!text) return callback(new Error("unable to load data")); + callback(null, cubism_graphiteParse(text)); + }); + }, expression += ""); + + metric.summarize = function (_) { + sum = _; + return metric; + }; + + return metric; + }; + + source.find = function (pattern, callback) { + d3.json(host + "/metrics/find?format=completer" + + "&query=" + encodeURIComponent(pattern), function (result) { + if (!result) return callback(new Error("unable to find metrics")); + callback(null, result.metrics.map(function (d) { + return d.path; + })); + }); + }; + + // Returns the graphite host. + source.toString = function () { + return host; + }; + + return source; + }; + +// Graphite understands seconds since UNIX epoch. + function cubism_graphiteFormatDate(time) { + return Math.floor(time / 1000); + } + +// Helper method for parsing graphite's raw format. + function cubism_graphiteParse(text) { + var i = text.indexOf("|"), + meta = text.substring(0, i), + c = meta.lastIndexOf(","), + b = meta.lastIndexOf(",", c - 1), + a = meta.lastIndexOf(",", b - 1), + start = meta.substring(a + 1, b) * 1000, + step = meta.substring(c + 1) * 1000; + return text + .substring(i + 1) + .split(",") + .slice(1)// the first value is always None? + .map(function (d) { + return +d; + }); + } + + function cubism_metric(context) { + if (!(context instanceof cubism_context)) throw new Error("invalid context"); + this.context = context; + } + + var cubism_metricPrototype = cubism_metric.prototype; + + cubism.metric = cubism_metric; + + cubism_metricPrototype.valueAt = function () { + return NaN; + }; + + cubism_metricPrototype.alias = function (name) { + this.toString = function () { + return name; + }; + return this; + }; + + cubism_metricPrototype.extent = function () { + var i = 0, + n = this.context.size(), + value, + min = Infinity, + max = -Infinity; + while (++i < n) { + value = this.valueAt(i); + if (value < min) min = value; + if (value > max) max = value; + } + return [min, max]; + }; + + cubism_metricPrototype.on = function (type, listener) { + return arguments.length < 2 ? null : this; + }; + + cubism_metricPrototype.shift = function () { + return this; + }; + + cubism_metricPrototype.on = function () { + return arguments.length < 2 ? null : this; + }; + + cubism_contextPrototype.metric = function (request, name) { + var context = this, + metric = new cubism_metric(context), + id = ".metric-" + ++cubism_id, + start = -Infinity, + stop, + step = context.step(), + size = context.size(), + values = [], + event = d3.dispatch("change"), + listening = 0, + fetching; + + // Prefetch new data into a temporary array. + function prepare(start1, stop) { + var steps = Math.min(size, Math.round((start1 - start) / step)); + if (!steps || fetching) return; // already fetched, or fetching! + fetching = true; + steps = Math.min(size, steps + cubism_metricOverlap); + var start0 = new Date(stop - steps * step); + request(start0, stop, step, function (error, data) { + fetching = false; + if (error) return console.warn(error); + var i = isFinite(start) ? Math.round((start0 - start) / step) : 0; + for (var j = 0, m = data.length; j < m; ++j) values[j + i] = data[j]; + event.change.call(metric, start, stop); + }); + } + + // When the context changes, switch to the new data, ready-or-not! + function beforechange(start1, stop1) { + if (!isFinite(start)) start = start1; + values.splice(0, Math.max(0, Math.min(size, Math.round((start1 - start) / step)))); + start = start1; + stop = stop1; + } + + // + metric.valueAt = function (i) { + return values[i]; + }; + + // + metric.shift = function (offset) { + return context.metric(cubism_metricShift(request, +offset)); + }; + + // + metric.on = function (type, listener) { + if (!arguments.length) return event.on(type); + + // If there are no listeners, then stop listening to the context, + // and avoid unnecessary fetches. + if (listener == null) { + if (event.on(type) != null && --listening == 0) { + context.on("prepare" + id, null).on("beforechange" + id, null); + } + } else { + if (event.on(type) == null && ++listening == 1) { + context.on("prepare" + id, prepare).on("beforechange" + id, beforechange); + } + } + + event.on(type, listener); + + // Notify the listener of the current start and stop time, as appropriate. + // This way, charts can display synchronous metrics immediately. + if (listener != null) { + if (/^change(\.|$)/.test(type)) listener.call(context, start, stop); + } + + return metric; + }; + + // + if (arguments.length > 1) metric.toString = function () { + return name; + }; + + return metric; + }; + +// Number of metric to refetch each period, in case of lag. + var cubism_metricOverlap = 6; + +// Wraps the specified request implementation, and shifts time by the given offset. + function cubism_metricShift(request, offset) { + return function (start, stop, step, callback) { + request(new Date(+start + offset), new Date(+stop + offset), step, callback); + }; + } + + function cubism_metricConstant(context, value) { + cubism_metric.call(this, context); + value = +value; + var name = value + ""; + this.valueOf = function () { + return value; + }; + this.toString = function () { + return name; + }; + } + + var cubism_metricConstantPrototype = cubism_metricConstant.prototype = Object.create(cubism_metric.prototype); + + cubism_metricConstantPrototype.valueAt = function () { + return +this; + }; + + cubism_metricConstantPrototype.extent = function () { + return [+this, +this]; + }; + function cubism_metricOperator(name, operate) { + + function cubism_metricOperator(left, right) { + if (!(right instanceof cubism_metric)) right = new cubism_metricConstant(left.context, right); + else if (left.context !== right.context) throw new Error("mismatch context"); + cubism_metric.call(this, left.context); + this.left = left; + this.right = right; + this.toString = function () { + return left + " " + name + " " + right; + }; + } + + var cubism_metricOperatorPrototype = cubism_metricOperator.prototype = Object.create(cubism_metric.prototype); + + cubism_metricOperatorPrototype.valueAt = function (i) { + return operate(this.left.valueAt(i), this.right.valueAt(i)); + }; + + cubism_metricOperatorPrototype.shift = function (offset) { + return new cubism_metricOperator(this.left.shift(offset), this.right.shift(offset)); + }; + + cubism_metricOperatorPrototype.on = function (type, listener) { + if (arguments.length < 2) return this.left.on(type); + this.left.on(type, listener); + this.right.on(type, listener); + return this; + }; + + return function (right) { + return new cubism_metricOperator(this, right); + }; + } + + cubism_metricPrototype.add = cubism_metricOperator("+", function (left, right) { + return left + right; + }); + + cubism_metricPrototype.subtract = cubism_metricOperator("-", function (left, right) { + return left - right; + }); + + cubism_metricPrototype.multiply = cubism_metricOperator("*", function (left, right) { + return left * right; + }); + + cubism_metricPrototype.divide = cubism_metricOperator("/", function (left, right) { + return left / right; + }); + cubism_contextPrototype.horizon = function () { + var context = this, + mode = "offset", + buffer = document.createElement("canvas"), + width = buffer.width = context.size(), + height = buffer.height = 30, + scale = d3.scale.linear().interpolate(d3.interpolateRound), + metric = cubism_identity, + extent = null, + title = cubism_identity, + format = d3.format(".2s"), + colors = ["#08519c", "#3182bd", "#6baed6", "#bdd7e7", "#bae4b3", "#74c476", "#31a354", "#006d2c"]; + + function horizon(selection) { + + selection + .on("mousemove.horizon", function () { + context.focus(d3.mouse(this)[0]); + }) + .on("mouseout.horizon", function () { + context.focus(null); + }); + + selection.append("canvas") + .attr("width", width) + .attr("height", height); + + selection.append("span") + .attr("class", "title") + .text(title); + + selection.append("span") + .attr("class", "value"); + + selection.each(function (d, i) { + var that = this, + id = ++cubism_id, + metric_ = typeof metric === "function" ? metric.call(that, d, i) : metric, + colors_ = typeof colors === "function" ? colors.call(that, d, i) : colors, + extent_ = typeof extent === "function" ? extent.call(that, d, i) : extent, + start = -Infinity, + step = context.step(), + canvas = d3.select(that).select("canvas"), + span = d3.select(that).select(".value"), + max_, + m = colors_.length >> 1, + ready; + + canvas.datum({id:id, metric:metric_}); + canvas = canvas.node().getContext("2d"); + + function change(start1, stop) { + canvas.save(); + + // compute the new extent and ready flag + var extent = metric_.extent(); + ready = extent.every(isFinite); + if (extent_ != null) extent = extent_; + + // if this is an update (with no extent change), copy old values! + var i0 = 0, max = Math.max(-extent[0], extent[1]); + if (this === context) { + if (max == max_) { + i0 = width - cubism_metricOverlap; + var dx = (start1 - start) / step; + if (dx < width) { + var canvas0 = buffer.getContext("2d"); + canvas0.clearRect(0, 0, width, height); + canvas0.drawImage(canvas.canvas, dx, 0, width - dx, height, 0, 0, width - dx, height); + canvas.clearRect(0, 0, width, height); + canvas.drawImage(canvas0.canvas, 0, 0); + } + } + start = start1; + } + + // update the domain + scale.domain([0, max_ = max]); + + // clear for the new data + canvas.clearRect(i0, 0, width - i0, height); + + // record whether there are negative values to display + var negative; + + // positive bands + for (var j = 0; j < m; ++j) { + canvas.fillStyle = colors_[m + j]; + + // Adjust the range based on the current band index. + var y0 = (j - m + 1) * height; + scale.range([m * height + y0, y0]); + y0 = scale(0); + + for (var i = i0, n = width, y1; i < n; ++i) { + y1 = metric_.valueAt(i); + if (y1 <= 0) { + negative = true; + continue; + } + canvas.fillRect(i, y1 = scale(y1), 1, y0 - y1); + } + } + + if (negative) { + // enable offset mode + if (mode === "offset") { + canvas.translate(0, height); + canvas.scale(1, -1); + } + + // negative bands + for (var j = 0; j < m; ++j) { + canvas.fillStyle = colors_[m - 1 - j]; + + // Adjust the range based on the current band index. + var y0 = (j - m + 1) * height; + scale.range([m * height + y0, y0]); + y0 = scale(0); + + for (var i = i0, n = width, y1; i < n; ++i) { + y1 = metric_.valueAt(i); + if (y1 >= 0) continue; + canvas.fillRect(i, scale(-y1), 1, y0 - scale(-y1)); + } + } + } + + canvas.restore(); + } + + function focus(i) { + if (i == null) i = width - 1; + var value = metric_.valueAt(i); + span.datum(value).text(isNaN(value) ? null : format); + } + + // Update the chart when the context changes. + context.on("change.horizon-" + id, change); + context.on("focus.horizon-" + id, focus); + + // Display the first metric change immediately, + // but defer subsequent updates to the canvas change. + // Note that someone still needs to listen to the metric, + // so that it continues to update automatically. + metric_.on("change.horizon-" + id, function (start, stop) { + change(start, stop), focus(); + if (ready) metric_.on("change.horizon-" + id, cubism_identity); + }); + }); + } + + horizon.remove = function (selection) { + + selection + .on("mousemove.horizon", null) + .on("mouseout.horizon", null); + + selection.selectAll("canvas") + .each(remove) + .remove(); + + selection.selectAll(".title,.value") + .remove(); + + function remove(d) { + d.metric.on("change.horizon-" + d.id, null); + context.on("change.horizon-" + d.id, null); + context.on("focus.horizon-" + d.id, null); + } + }; + + horizon.mode = function (_) { + if (!arguments.length) return mode; + mode = _ + ""; + return horizon; + }; + + horizon.height = function (_) { + if (!arguments.length) return height; + buffer.height = height = +_; + return horizon; + }; + + horizon.metric = function (_) { + if (!arguments.length) return metric; + metric = _; + return horizon; + }; + + horizon.scale = function (_) { + if (!arguments.length) return scale; + scale = _; + return horizon; + }; + + horizon.extent = function (_) { + if (!arguments.length) return extent; + extent = _; + return horizon; + }; + + horizon.title = function (_) { + if (!arguments.length) return title; + title = _; + return horizon; + }; + + horizon.format = function (_) { + if (!arguments.length) return format; + format = _; + return horizon; + }; + + horizon.colors = function (_) { + if (!arguments.length) return colors; + colors = _; + return horizon; + }; + + return horizon; + }; + cubism_contextPrototype.comparison = function () { + var context = this, + width = context.size(), + height = 120, + scale = d3.scale.linear().interpolate(d3.interpolateRound), + primary = function (d) { + return d[0]; + }, + secondary = function (d) { + return d[1]; + }, + extent = null, + title = cubism_identity, + formatPrimary = cubism_comparisonPrimaryFormat, + formatChange = cubism_comparisonChangeFormat, + colors = ["#9ecae1", "#225b84", "#a1d99b", "#22723a"], + strokeWidth = 1.5; + + function comparison(selection) { + + selection + .on("mousemove.comparison", function () { + context.focus(d3.mouse(this)[0]); + }) + .on("mouseout.comparison", function () { + context.focus(null); + }); + + selection.append("canvas") + .attr("width", width) + .attr("height", height); + + selection.append("span") + .attr("class", "title") + .text(title); + + selection.append("span") + .attr("class", "value primary"); + + selection.append("span") + .attr("class", "value change"); + + selection.each(function (d, i) { + var that = this, + id = ++cubism_id, + primary_ = typeof primary === "function" ? primary.call(that, d, i) : primary, + secondary_ = typeof secondary === "function" ? secondary.call(that, d, i) : secondary, + extent_ = typeof extent === "function" ? extent.call(that, d, i) : extent, + div = d3.select(that), + canvas = div.select("canvas"), + spanPrimary = div.select(".value.primary"), + spanChange = div.select(".value.change"), + ready; + + canvas.datum({id:id, primary:primary_, secondary:secondary_}); + canvas = canvas.node().getContext("2d"); + + function change(start, stop) { + canvas.save(); + canvas.clearRect(0, 0, width, height); + + // update the scale + var primaryExtent = primary_.extent(), + secondaryExtent = secondary_.extent(), + extent = extent_ == null ? primaryExtent : extent_; + scale.domain(extent).range([height, 0]); + ready = primaryExtent.concat(secondaryExtent).every(isFinite); + + // consistent overplotting + var round = start / context.step() & 1 + ? cubism_comparisonRoundOdd + : cubism_comparisonRoundEven; + + // positive changes + canvas.fillStyle = colors[2]; + for (var i = 0, n = width; i < n; ++i) { + var y0 = scale(primary_.valueAt(i)), + y1 = scale(secondary_.valueAt(i)); + if (y0 < y1) canvas.fillRect(round(i), y0, 1, y1 - y0); + } + + // negative changes + canvas.fillStyle = colors[0]; + for (i = 0; i < n; ++i) { + var y0 = scale(primary_.valueAt(i)), + y1 = scale(secondary_.valueAt(i)); + if (y0 > y1) canvas.fillRect(round(i), y1, 1, y0 - y1); + } + + // positive values + canvas.fillStyle = colors[3]; + for (i = 0; i < n; ++i) { + var y0 = scale(primary_.valueAt(i)), + y1 = scale(secondary_.valueAt(i)); + if (y0 <= y1) canvas.fillRect(round(i), y0, 1, strokeWidth); + } + + // negative values + canvas.fillStyle = colors[1]; + for (i = 0; i < n; ++i) { + var y0 = scale(primary_.valueAt(i)), + y1 = scale(secondary_.valueAt(i)); + if (y0 > y1) canvas.fillRect(round(i), y0 - strokeWidth, 1, strokeWidth); + } + + canvas.restore(); + } + + function focus(i) { + if (i == null) i = width - 1; + var valuePrimary = primary_.valueAt(i), + valueSecondary = secondary_.valueAt(i), + valueChange = (valuePrimary - valueSecondary) / valueSecondary; + + spanPrimary + .datum(valuePrimary) + .text(isNaN(valuePrimary) ? null : formatPrimary); + + spanChange + .datum(valueChange) + .text(isNaN(valueChange) ? null : formatChange) + .attr("class", "value change " + (valueChange > 0 ? "positive" : valueChange < 0 ? "negative" : "")); + } + + // Display the first primary change immediately, + // but defer subsequent updates to the context change. + // Note that someone still needs to listen to the metric, + // so that it continues to update automatically. + primary_.on("change.comparison-" + id, firstChange); + secondary_.on("change.comparison-" + id, firstChange); + function firstChange(start, stop) { + change(start, stop), focus(); + if (ready) { + primary_.on("change.comparison-" + id, cubism_identity); + secondary_.on("change.comparison-" + id, cubism_identity); + } + } + + // Update the chart when the context changes. + context.on("change.comparison-" + id, change); + context.on("focus.comparison-" + id, focus); + }); + } + + comparison.remove = function (selection) { + + selection + .on("mousemove.comparison", null) + .on("mouseout.comparison", null); + + selection.selectAll("canvas") + .each(remove) + .remove(); + + selection.selectAll(".title,.value") + .remove(); + + function remove(d) { + d.primary.on("change.comparison-" + d.id, null); + d.secondary.on("change.comparison-" + d.id, null); + context.on("change.comparison-" + d.id, null); + context.on("focus.comparison-" + d.id, null); + } + }; + + comparison.height = function (_) { + if (!arguments.length) return height; + height = +_; + return comparison; + }; + + comparison.primary = function (_) { + if (!arguments.length) return primary; + primary = _; + return comparison; + }; + + comparison.secondary = function (_) { + if (!arguments.length) return secondary; + secondary = _; + return comparison; + }; + + comparison.scale = function (_) { + if (!arguments.length) return scale; + scale = _; + return comparison; + }; + + comparison.extent = function (_) { + if (!arguments.length) return extent; + extent = _; + return comparison; + }; + + comparison.title = function (_) { + if (!arguments.length) return title; + title = _; + return comparison; + }; + + comparison.formatPrimary = function (_) { + if (!arguments.length) return formatPrimary; + formatPrimary = _; + return comparison; + }; + + comparison.formatChange = function (_) { + if (!arguments.length) return formatChange; + formatChange = _; + return comparison; + }; + + comparison.colors = function (_) { + if (!arguments.length) return colors; + colors = _; + return comparison; + }; + + comparison.strokeWidth = function (_) { + if (!arguments.length) return strokeWidth; + strokeWidth = _; + return comparison; + }; + + return comparison; + }; + + var cubism_comparisonPrimaryFormat = d3.format(".2s"), + cubism_comparisonChangeFormat = d3.format("+.0%"); + + function cubism_comparisonRoundEven(i) { + return i & 0xfffffe; + } + + function cubism_comparisonRoundOdd(i) { + return ((i + 1) & 0xfffffe) - 1; + } + + cubism_contextPrototype.axis = function () { + var context = this, + scale = context.scale, + axis_ = d3.svg.axis().scale(scale); + + var format = context.step() < 6e4 ? cubism_axisFormatSeconds + : context.step() < 864e5 ? cubism_axisFormatMinutes + : cubism_axisFormatDays; + + function axis(selection) { + var id = ++cubism_id, + tick; + + var g = selection.append("svg") + .datum({id:id}) + .attr("width", context.size()) + .attr("height", Math.max(28, -axis.tickSize())) + .append("g") + .attr("transform", "translate(0," + (axis_.orient() === "top" ? 27 : 4) + ")") + .call(axis_); + + context.on("change.axis-" + id, function () { + g.call(axis_); + if (!tick) tick = d3.select(g.node().appendChild(g.selectAll("text").node().cloneNode(true))) + .style("display", "none") + .text(null); + }); + + context.on("focus.axis-" + id, function (i) { + if (tick) { + if (i == null) { + tick.style("display", "none"); + g.selectAll("text").style("fill-opacity", null); + } else { + tick.style("display", null).attr("x", i).text(format(scale.invert(i))); + var dx = tick.node().getComputedTextLength() + 6; + g.selectAll("text").style("fill-opacity", function (d) { + return Math.abs(scale(d) - i) < dx ? 0 : 1; + }); + } + } + }); + } + + axis.remove = function (selection) { + + selection.selectAll("svg") + .each(remove) + .remove(); + + function remove(d) { + context.on("change.axis-" + d.id, null); + context.on("focus.axis-" + d.id, null); + } + }; + + return d3.rebind(axis, axis_, + "orient", + "ticks", + "tickSubdivide", + "tickSize", + "tickPadding", + "tickFormat"); + }; + + var cubism_axisFormatSeconds = d3.time.format("%I:%M:%S %p"), + cubism_axisFormatMinutes = d3.time.format("%I:%M %p"), + cubism_axisFormatDays = d3.time.format("%B %d"); + cubism_contextPrototype.rule = function () { + var context = this, + metric = cubism_identity; + + function rule(selection) { + var id = ++cubism_id; + + var line = selection.append("div") + .datum({id:id}) + .attr("class", "line") + .call(cubism_ruleStyle); + + selection.each(function (d, i) { + var that = this, + id = ++cubism_id, + metric_ = typeof metric === "function" ? metric.call(that, d, i) : metric; + + if (!metric_) return; + + function change(start, stop) { + var values = []; + + for (var i = 0, n = context.size(); i < n; ++i) { + if (metric_.valueAt(i)) { + values.push(i); + } + } + + var lines = selection.selectAll(".metric").data(values); + lines.exit().remove(); + lines.enter().append("div").attr("class", "metric line").call(cubism_ruleStyle); + lines.style("left", cubism_ruleLeft); + } + + context.on("change.rule-" + id, change); + metric_.on("change.rule-" + id, change); + }); + + context.on("focus.rule-" + id, function (i) { + line.datum(i) + .style("display", i == null ? "none" : null) + .style("left", cubism_ruleLeft); + }); + } + + rule.remove = function (selection) { + + selection.selectAll(".line") + .each(remove) + .remove(); + + function remove(d) { + context.on("focus.rule-" + d.id, null); + } + }; + + rule.metric = function (_) { + if (!arguments.length) return metric; + metric = _; + return rule; + }; + + return rule; + }; + + function cubism_ruleStyle(line) { + line + .style("position", "absolute") + .style("top", 0) + .style("bottom", 0) + .style("width", "1px") + .style("pointer-events", "none"); + } + + function cubism_ruleLeft(i) { + return i + "px"; + } +})(this);
