You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by sr...@apache.org on 2014/05/29 23:02:13 UTC
[4/7] AMBARI-5928. Create and populate Metrics section of a slider
app - only Slider view changes. (onechiporenko via srimanth)
http://git-wip-us.apache.org/repos/asf/ambari/blob/07556168/contrib/views/slider/src/main/resources/ui/app/views/common/chart_view.js
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/app/views/common/chart_view.js b/contrib/views/slider/src/main/resources/ui/app/views/common/chart_view.js
new file mode 100644
index 0000000..5af5241
--- /dev/null
+++ b/contrib/views/slider/src/main/resources/ui/app/views/common/chart_view.js
@@ -0,0 +1,915 @@
+/**
+ * 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.
+ */
+
+var string_utils = require('helpers/string_utils');
+
+/**
+ * @class
+ *
+ * This is a view which GETs data from a URL and shows it as a time based line
+ * graph. Time is shown on the X axis with data series shown on Y axis. It
+ * optionally also has the ability to auto refresh itself over a given time
+ * interval.
+ *
+ * This is an abstract class which is meant to be extended.
+ *
+ * Extending classes should override the following:
+ * <ul>
+ * <li>url - from where the data can be retrieved
+ * <li>title - Title to be displayed when showing the chart
+ * <li>id - which uniquely identifies this chart in any page
+ * <li>#transformToSeries(jsonData) - function to map server data into graph
+ * series
+ * </ul>
+ *
+ * Extending classes could optionally override the following:
+ * <ul>
+ * <li>#colorForSeries(series) - function to get custom colors per series
+ * </ul>
+ *
+ * @extends Ember.Object
+ * @extends Ember.View
+ */
+App.ChartView = Ember.View.extend({
+
+ templateName: 'common/chart',
+
+ /**
+ * The URL from which data can be retrieved.
+ *
+ * This property must be provided for the graph to show properly.
+ *
+ * @type String
+ * @default null
+ */
+ url: null,
+
+ /**
+ * A unique ID for this chart.
+ *
+ * @type String
+ * @default null
+ */
+ id: null,
+
+ /**
+ * Title to be shown under the chart.
+ *
+ * @type String
+ * @default null
+ */
+ title: null,
+
+ /**
+ * @private
+ *
+ * @type Rickshaw.Graph
+ * @default null
+ */
+ _graph: null,
+
+ /**
+ * Array of classnames for each series (in widget)
+ * @type Rickshaw.Graph
+ */
+ _popupGraph: null,
+
+ /**
+ * Array of classnames for each series
+ * @type Array
+ */
+ _seriesProperties: null,
+
+ /**
+ * Array of classnames for each series (in widget)
+ * @type Array
+ */
+ _seriesPropertiesWidget: null,
+
+ /**
+ * Renderer type
+ * See <code>Rickshaw.Graph.Renderer</code> for more info
+ * @type String
+ */
+ renderer: 'area',
+
+ /**
+ * Suffix used in DOM-elements selectors
+ * @type String
+ */
+ popupSuffix: '-popup',
+
+ /**
+ * Is popup for current graph open
+ * @type Boolean
+ */
+ isPopup: false,
+
+ /**
+ * Is graph ready
+ * @type Boolean
+ */
+ isReady: false,
+
+ /**
+ * Is popup-graph ready
+ * @type Boolean
+ */
+ isPopupReady: false,
+
+ /**
+ * Is data for graph available
+ * @type Boolean
+ */
+ hasData: true,
+
+ containerId: null,
+ containerClass: null,
+ yAxisId: null,
+ yAxisClass: null,
+ xAxisId: null,
+ xAxisClass: null,
+ legendId: null,
+ legendClass: null,
+ chartId: null,
+ chartClass: null,
+ titleId: null,
+ titleClass: null,
+
+ didInsertElement: function() {
+ var id = this.get('id');
+ var idTemplate = id + '-{element}';
+
+ this.set('containerId', idTemplate.replace('{element}', 'container'));
+ this.set('containerClass', 'chart-container');
+ this.set('yAxisId', idTemplate.replace('{element}', 'yaxis'));
+ this.set('yAxisClass', this.get('yAxisId'));
+ this.set('xAxisId', idTemplate.replace('{element}', 'xaxis'));
+ this.set('xAxisClass', this.get('xAxisId'));
+ this.set('legendId', idTemplate.replace('{element}', 'legend'));
+ this.set('legendClass', this.get('legendId'));
+ this.set('chartId', idTemplate.replace('{element}', 'chart'));
+ this.set('chartClass', this.get('chartId'));
+ this.set('titleId', idTemplate.replace('{element}', 'title'));
+ this.set('titleClass', this.get('titleId'));
+ this.loadData();
+ },
+
+
+ loadData: function() {
+ App.ajax.send({
+ name: this.get('ajaxIndex'),
+ sender: this,
+ data: this.getDataForAjaxRequest(),
+ success: '_refreshGraph',
+ error: 'loadDataErrorCallback'
+ });
+ },
+
+ getDataForAjaxRequest: function() {
+ return {};
+ },
+
+ loadDataErrorCallback: function(xhr, textStatus, errorThrown) {
+ this.set('isReady', true);
+ if (xhr.readyState == 4 && xhr.status) {
+ textStatus = xhr.status + " " + textStatus;
+ }
+ this._showMessage('warn', 'graphs.error.title', textStatus + ' ' + errorThrown);
+ this.set('isPopup', false);
+ this.set('hasData', false);
+ },
+
+ /**
+ * Shows a yellow warning message in place of the chart.
+ *
+ * @param type Can be any of 'warn', 'error', 'info', 'success'
+ * @param title Bolded title for the message
+ * @param message String representing the message
+ * @type: Function
+ */
+ _showMessage: function(type, title, message) {
+ var chartOverlay = '#' + this.id;
+ var chartOverlayId = chartOverlay + '-chart';
+ var chartOverlayY = chartOverlay + '-yaxis';
+ var chartOverlayX = chartOverlay + '-xaxis';
+ var chartOverlayLegend = chartOverlay + '-legend';
+ var chartOverlayTimeline = chartOverlay + '-timeline';
+ if (this.get('isPopup')) {
+ chartOverlayId += this.get('popupSuffix');
+ chartOverlayY += this.get('popupSuffix');
+ chartOverlayX += this.get('popupSuffix');
+ chartOverlayLegend += this.get('popupSuffix');
+ chartOverlayTimeline += this.get('popupSuffix');
+ }
+ var typeClass;
+ switch (type) {
+ case 'error':
+ typeClass = 'alert-error';
+ break;
+ case 'success':
+ typeClass = 'alert-success';
+ break;
+ case 'info':
+ typeClass = 'alert-info';
+ break;
+ default:
+ typeClass = '';
+ break;
+ }
+ $(chartOverlayId+', '+chartOverlayY+', '+chartOverlayX+', '+chartOverlayLegend+', '+chartOverlayTimeline).html('');
+ $(chartOverlayId).append('<div class=\"alert '+typeClass+'\"><strong>'+title+'</strong> '+message+'</div>');
+ },
+
+ /**
+ * Transforms the JSON data retrieved from the server into the series
+ * format that Rickshaw.Graph understands.
+ *
+ * The series object is generally in the following format: [ { name :
+ * "Series 1", data : [ { x : 0, y : 0 }, { x : 1, y : 1 } ] } ]
+ *
+ * Extending classes should override this method.
+ *
+ * @param seriesData
+ * Data retrieved from the server
+ * @param displayName
+ * Graph title
+ * @type: Function
+ *
+ */
+ transformData: function (seriesData, displayName) {
+ var seriesArray = [];
+ if (seriesData != null) {
+ // Is it a string?
+ if ("string" == typeof seriesData) {
+ seriesData = JSON.parse(seriesData);
+ }
+ // Is it a number?
+ if ("number" == typeof seriesData) {
+ // Same number applies to all time.
+ var number = seriesData;
+ seriesData = [];
+ seriesData.push([number, (new Date().getTime())-(60*60)]);
+ seriesData.push([number, (new Date().getTime())]);
+ }
+ // We have valid data
+ var series = {};
+ series.name = displayName;
+ series.data = [];
+ for ( var index = 0; index < seriesData.length; index++) {
+ series.data.push({
+ x: seriesData[index][1],
+ y: seriesData[index][0]
+ });
+ }
+ return series;
+ }
+ return null;
+ },
+
+ /**
+ * Provides the formatter to use in displaying Y axis.
+ *
+ * Uses the App.ChartLinearTimeView.DefaultFormatter which shows 10K,
+ * 300M etc.
+ *
+ * @type Function
+ */
+ yAxisFormatter: function(y) {
+ return App.ChartView.DefaultFormatter(y);
+ },
+
+ /**
+ * Provides the color (in any HTML color format) to use for a particular
+ * series.
+ * May be redefined in child views
+ *
+ * @param series
+ * Series for which color is being requested
+ * @return color String. Returning null allows this chart to pick a color
+ * from palette.
+ * @default null
+ * @type Function
+ */
+ colorForSeries: function (series) {
+ return null;
+ },
+
+ /**
+ * Check whether seriesData is correct data for chart drawing
+ * @param {Array} seriesData
+ * @return {Boolean}
+ */
+ checkSeries : function(seriesData) {
+ if(!seriesData || !seriesData.length) {
+ return false;
+ }
+ var result = true;
+ seriesData.forEach(function(item) {
+ if(!item.data || !item.data.length || !item.data[0] || typeof item.data[0].x === 'undefined') {
+ result = false;
+ }
+ });
+ return result;
+ },
+
+ /**
+ * @private
+ *
+ * Refreshes the graph with the latest JSON data.
+ *
+ * @type Function
+ */
+ _refreshGraph: function (jsonData) {
+ if(this.get('isDestroyed')){
+ return;
+ }
+ var seriesData = this.transformToSeries(jsonData);
+
+ //if graph opened as modal popup
+ var popup_path = $("#" + this.get('id') + "-container" + this.get('popupSuffix'));
+ var graph_container = $("#" + this.get('id') + "-container");
+ if(popup_path.length) {
+ popup_path.children().each(function () {
+ $(this).children().remove();
+ });
+ this.set('isPopup', true);
+ }
+ else {
+ graph_container.children().each(function (index, value) {
+ $(value).children().remove();
+ });
+ }
+ if (this.checkSeries(seriesData)) {
+ // Check container exists (may be not, if we go to another page and wait while graphs loading)
+ if (graph_container.length) {
+ this.draw(seriesData);
+ this.set('hasData', true);
+ //move yAxis value lower to make them fully visible
+ $("#" + this.id + "-container").find('.y_axis text').attr('y',8);
+ }
+ }
+ else {
+ this.set('isReady', true);
+ //if Axis X time interval is default(60 minutes)
+ if(this.get('timeUnitSeconds') === 3600){
+ this._showMessage('info', this.t('graphs.noData.title'), this.t('graphs.noData.message'));
+ this.set('hasData', false);
+ }
+ else {
+ this._showMessage('info', this.t('graphs.noData.title'), this.t('graphs.noDataAtTime.message'));
+ }
+ this.set('isPopup', false);
+ }
+ },
+
+ /**
+ * Returns a custom time unit, that depends on X axis interval length, for the graph's X axis.
+ * This is needed as Rickshaw's default time X axis uses UTC time, which can be confusing
+ * for users expecting locale specific time.
+ *
+ * If <code>null</code> is returned, Rickshaw's default time unit is used.
+ *
+ * @type Function
+ * @return Rickshaw.Fixtures.Time
+ */
+ localeTimeUnit: function(timeUnitSeconds) {
+ var timeUnit = new Rickshaw.Fixtures.Time();
+ switch (timeUnitSeconds){
+ case 604800:
+ timeUnit = timeUnit.unit('day');
+ break;
+ case 2592000:
+ timeUnit = timeUnit.unit('week');
+ break;
+ case 31104000:
+ timeUnit = timeUnit.unit('month');
+ break;
+ default:
+ timeUnit = {
+ name: timeUnitSeconds / 240 + ' minute',
+ seconds: timeUnitSeconds / 4,
+ formatter: function (d) {
+ return d.toLocaleString().match(/(\d+:\d+):/)[1];
+ }
+ };
+ }
+ return timeUnit;
+ },
+
+ /**
+ * temporary fix for incoming data for graph
+ * to shift data time to correct time point
+ * @param {Array} data
+ */
+ dataShiftFix: function(data) {
+ var nowTime = Math.round((new Date().getTime()) / 1000);
+ data.forEach(function(series){
+ var l = series.data.length;
+ var shiftDiff = nowTime - series.data[l - 1].x;
+ if(shiftDiff > 3600){
+ for(var i = 0;i < l;i++){
+ series.data[i].x = series.data[i].x + shiftDiff;
+ }
+ series.data.unshift({
+ x: nowTime - this.get('timeUnitSeconds'),
+ y: 0
+ });
+ }
+ }, this);
+ },
+
+ /**
+ * calculate statistic data for popup legend and set proper colors for series
+ * @param {Array} data
+ */
+ dataPreProcess: function(data) {
+ var self = this;
+ var palette = new Rickshaw.Color.Palette({ scheme: 'munin'});
+ // Format series for display
+ var series_min_length = 100000000;
+ data.forEach(function (series, index) {
+ var seriesColor = self.colorForSeries(series);
+ if (seriesColor == null) {
+ seriesColor = palette.color();
+ }
+ series.color = seriesColor;
+ series.stroke = 'rgba(0,0,0,0.3)';
+ if (this.get('isPopup')) {
+ // calculate statistic data for popup legend
+ var avg = 0;
+ var min = Number.MAX_VALUE;
+ var max = Number.MIN_VALUE;
+ for (var i = 0; i < series.data.length; i++) {
+ avg += series.data[i]['y'];
+ if (series.data[i]['y'] < min) {
+ min = series.data[i]['y'];
+ }
+ else {
+ if (series.data[i]['y'] > max) {
+ max = series.data[i]['y'];
+ }
+ }
+ }
+ series.name = string_utils.pad(series.name, 30, ' ', 2) +
+ string_utils.pad('min', 5, ' ', 3) +
+ string_utils.pad(this.get('yAxisFormatter')(min), 12, ' ', 3) +
+ string_utils.pad('avg', 5, ' ', 3) +
+ string_utils.pad(this.get('yAxisFormatter')(avg/series.data.length), 12, ' ', 3) +
+ string_utils.pad('max', 12, ' ', 3) +
+ string_utils.pad(this.get('yAxisFormatter')(max), 5, ' ', 3);
+ }
+ if (series.data.length < series_min_length) {
+ series_min_length = series.data.length;
+ }
+ }.bind(this));
+
+ // All series should have equal length
+ data.forEach(function(series, index) {
+ if (series.data.length > series_min_length) {
+ series.data.length = series_min_length;
+ }
+ });
+ },
+
+ draw: function(seriesData) {
+ var self = this;
+ var isPopup = this.get('isPopup');
+ var p = isPopup ? this.get('popupSuffix') : '';
+
+ this.dataShiftFix(seriesData);
+ this.dataPreProcess(seriesData);
+
+ var chartId = "#" + this.get('id') + "-chart" + p;
+ var chartOverlayId = "#" + this.get('id') + "-container" + p;
+ var xaxisElementId = "#" + this.get('id') + "-xaxis" + p;
+ var yaxisElementId = "#" + this.get('id') + "-yaxis" + p;
+ var legendElementId = "#" + this.get('id') + "-legend" + p;
+
+ var chartElement = document.querySelector(chartId);
+ var overlayElement = document.querySelector(chartOverlayId);
+ var xaxisElement = document.querySelector(xaxisElementId);
+ var yaxisElement = document.querySelector(yaxisElementId);
+ var legendElement = document.querySelector(legendElementId);
+
+ var height = 150;
+ var width = 400;
+ var diff = 32;
+
+ if(this.get('inWidget')) {
+ height = 105; // for widgets view
+ diff = 22;
+ }
+ if (isPopup) {
+ height = 180;
+ width = 670;
+ }
+ else {
+ // If not in popup, the width could vary.
+ // We determine width based on div's size.
+ var thisElement = this.get('element');
+ if (thisElement!=null) {
+ var calculatedWidth = $(thisElement).width();
+ if (calculatedWidth > diff) {
+ width = calculatedWidth - diff;
+ }
+ }
+ }
+
+ var _graph = new Rickshaw.Graph({
+ height: height,
+ width: width,
+ element: chartElement,
+ series: seriesData,
+ interpolation: 'step-after',
+ stroke: true,
+ renderer: this.get('renderer'),
+ strokeWidth: (this.get('renderer') != 'area' ? 2 : 1)
+ });
+
+ if (this.get('renderer') === 'area') {
+ _graph.renderer.unstack = false;
+ }
+
+ new Rickshaw.Graph.Axis.Time({
+ graph: _graph,
+ timeUnit: this.localeTimeUnit(this.get('timeUnitSeconds'))
+ });
+
+ new Rickshaw.Graph.Axis.Y({
+ tickFormat: this.yAxisFormatter,
+ element: yaxisElement,
+ orientation: (isPopup ? 'left' : 'right'),
+ graph: _graph
+ });
+
+ var legend = new Rickshaw.Graph.Legend({
+ graph: _graph,
+ element: legendElement
+ });
+
+ new Rickshaw.Graph.Behavior.Series.Toggle({
+ graph: _graph,
+ legend: legend
+ });
+
+ new Rickshaw.Graph.Behavior.Series.Order({
+ graph: _graph,
+ legend: legend
+ });
+
+ if (!isPopup) {
+ overlayElement.addEventListener('mousemove', function () {
+ $(xaxisElement).removeClass('hide');
+ $(legendElement).removeClass('hide');
+ $(chartElement).children("div").removeClass('hide');
+ });
+ overlayElement.addEventListener('mouseout', function () {
+ $(legendElement).addClass('hide');
+ });
+ _graph.onUpdate(function () {
+ $(legendElement).addClass('hide');
+ });
+ }
+
+ //show the graph when it's loaded
+ _graph.onUpdate(function() {
+ self.set('isReady', true);
+ });
+ _graph.render();
+
+ if (isPopup) {
+ new Rickshaw.Graph.HoverDetail({
+ graph: _graph,
+ yFormatter:function (y) {
+ return self.yAxisFormatter(y);
+ },
+ xFormatter:function (x) {
+ return (new Date(x * 1000)).toLocaleTimeString();
+ },
+ formatter:function (series, x, y, formattedX, formattedY, d) {
+ return formattedY + '<br />' + formattedX;
+ }
+ });
+ }
+
+ _graph = this.updateSeriesInGraph(_graph);
+ if (isPopup) {
+ //show the graph when it's loaded
+ _graph.onUpdate(function() {
+ self.set('isPopupReady', true);
+ });
+ _graph.update();
+
+ var selector = '#'+this.get('id')+'-container'+this.get('popupSuffix');
+ $(selector + ' li.line').click(function() {
+ var series = [];
+ $(selector + ' a.action').each(function(index, v) {
+ series[index] = v.parentNode.classList;
+ });
+ self.set('_seriesProperties', series);
+ });
+
+ this.set('_popupGraph', _graph);
+ }
+ else {
+ _graph.update();
+ var selector = '#'+this.get('id')+'-container';
+ $(selector + ' li.line').click(function() {
+ var series = [];
+ $(selector + ' a.action').each(function(index, v) {
+ series[index] = v.parentNode.classList;
+ });
+ self.set('_seriesPropertiesWidget', series);
+ });
+
+ this.set('_graph', _graph);
+ }
+ },
+
+ /**
+ *
+ * @param {Rickshaw.Graph} graph
+ * @returns {Rickshaw.Graph}
+ */
+ updateSeriesInGraph: function(graph) {
+ var id = this.get('id');
+ var isPopup = this.get('isPopup');
+ var popupSuffix = this.get('popupSuffix');
+ var _series = isPopup ? this.get('_seriesProperties') : this.get('_seriesPropertiesWidget');
+ graph.series.forEach(function(series, index) {
+ if (_series !== null && _series[index] !== null && _series[index] !== undefined ) {
+ if(_series[_series.length - index - 1].length > 1) {
+ var s = '#' + id + '-container' + (isPopup ? popupSuffix : '') + ' a.action:eq(' + (_series.length - index - 1) + ')';
+ $(s).parent('li').addClass('disabled');
+ series.disable();
+ }
+ }
+ });
+ return graph;
+ },
+
+ showGraphInPopup: function() {
+ if(!this.get('hasData')) {
+ return;
+ }
+
+ this.set('isPopup', true);
+ var self = this;
+
+ App.ModalPopup.show({
+ bodyClass: Em.View.extend({
+
+ containerId: null,
+ containerClass: null,
+ yAxisId: null,
+ yAxisClass: null,
+ xAxisId: null,
+ xAxisClass: null,
+ legendId: null,
+ legendClass: null,
+ chartId: null,
+ chartClass: null,
+ titleId: null,
+ titleClass: null,
+
+ isReady: function() {
+ return this.get('parentView.graph.isPopupReady');
+ }.property('parentView.graph.isPopupReady'),
+
+ didInsertElement: function() {
+ $('#modal').addClass('modal-graph-line');
+ var popupSuffix = this.get('parentView.graph.popupSuffix');
+ var id = this.get('parentView.graph.id');
+ var idTemplate = id + '-{element}' + popupSuffix;
+
+ this.set('containerId', idTemplate.replace('{element}', 'container'));
+ this.set('containerClass', 'chart-container' + popupSuffix);
+ this.set('yAxisId', idTemplate.replace('{element}', 'yaxis'));
+ this.set('yAxisClass', this.get('yAxisId').replace(popupSuffix, ''));
+ this.set('xAxisId', idTemplate.replace('{element}', 'xaxis'));
+ this.set('xAxisClass', this.get('xAxisId').replace(popupSuffix, ''));
+ this.set('legendId', idTemplate.replace('{element}', 'legend'));
+ this.set('legendClass', this.get('legendId').replace(popupSuffix, ''));
+ this.set('chartId', idTemplate.replace('{element}', 'chart'));
+ this.set('chartClass', this.get('chartId').replace(popupSuffix, ''));
+ this.set('titleId', idTemplate.replace('{element}', 'title'));
+ this.set('titleClass', this.get('titleId').replace(popupSuffix, ''));
+ },
+
+ templateName: require('templates/common/chart/linear_time'),
+ /**
+ * check is time paging feature is enable for graph
+ */
+ isTimePagingEnable: function() {
+ return !self.get('isTimePagingDisable');
+ }.property(),
+ rightArrowVisible: function() {
+ return (this.get('isReady') && (this.get('parentView.currentTimeIndex') != 0));
+ }.property('isReady', 'parentView.currentTimeIndex'),
+ leftArrowVisible: function() {
+ return (this.get('isReady') && (this.get('parentView.currentTimeIndex') != 7));
+ }.property('isReady', 'parentView.currentTimeIndex')
+ }),
+ header: this.get('title'),
+ /**
+ * App.ChartLinearTimeView
+ */
+ graph: self,
+ secondary: null,
+ onPrimary: function() {
+ this.hide();
+ self.set('isPopup', false);
+ self.set('timeUnitSeconds', 3600);
+ },
+ onClose: function() {
+ this.onPrimary();
+ },
+ /**
+ * move graph back by time
+ * @param event
+ */
+ switchTimeBack: function(event) {
+ var index = this.get('currentTimeIndex');
+ // 7 - number of last time state
+ if(index < 7) {
+ this.reloadGraphByTime(++index);
+ }
+ },
+ /**
+ * move graph forward by time
+ * @param event
+ */
+ switchTimeForward: function(event) {
+ var index = this.get('currentTimeIndex');
+ if(index > 0) {
+ this.reloadGraphByTime(--index);
+ }
+ },
+ /**
+ * reload graph depending on the time
+ * @param index
+ */
+ reloadGraphByTime: function(index) {
+ this.set('currentTimeIndex', index);
+ self.set('timeUnitSeconds', this.get('timeStates')[index].seconds);
+ self.loadData();
+ },
+ timeStates: [
+ {name: Em.I18n.t('graphs.timeRange.hour'), seconds: 3600},
+ {name: Em.I18n.t('graphs.timeRange.twoHours'), seconds: 7200},
+ {name: Em.I18n.t('graphs.timeRange.fourHours'), seconds: 14400},
+ {name: Em.I18n.t('graphs.timeRange.twelveHours'), seconds: 43200},
+ {name: Em.I18n.t('graphs.timeRange.day'), seconds: 86400},
+ {name: Em.I18n.t('graphs.timeRange.week'), seconds: 604800},
+ {name: Em.I18n.t('graphs.timeRange.month'), seconds: 2592000},
+ {name: Em.I18n.t('graphs.timeRange.year'), seconds: 31104000}
+ ],
+ currentTimeIndex: 0,
+ currentTimeState: function() {
+ return this.get('timeStates').objectAt(this.get('currentTimeIndex'));
+ }.property('currentTimeIndex')
+ });
+ Ember.run.next(function() {
+ self.loadData();
+ self.set('isPopupReady', false);
+ });
+ },
+ //60 minute interval on X axis.
+ timeUnitSeconds: 3600
+});
+
+/**
+ * A formatter which will turn a number into computer storage sizes of the
+ * format '23 GB' etc.
+ *
+ * @type {Function}
+ */
+App.ChartView.BytesFormatter = function (y) {
+ if (y == 0) return '0 B';
+ var value = Rickshaw.Fixtures.Number.formatBase1024KMGTP(y);
+ if (!y || y.length < 1) {
+ value = '0 B';
+ }
+ else {
+ if ("number" == typeof value) {
+ value = String(value);
+ }
+ if ("string" == typeof value) {
+ value = value.replace(/\.\d(\d+)/, function($0, $1){ // Remove only 1-digit after decimal part
+ return $0.replace($1, '');
+ });
+ // Either it ends with digit or ends with character
+ value = value.replace(/(\d$)/, '$1 '); // Ends with digit like '120'
+ value = value.replace(/([a-zA-Z]$)/, ' $1'); // Ends with character like
+ // '120M'
+ value = value + 'B'; // Append B to make B, MB, GB etc.
+ }
+ }
+ return value;
+};
+
+/**
+ * A formatter which will turn a number into percentage display like '42%'
+ *
+ * @type {Function}
+ */
+App.ChartView.PercentageFormatter = function (percentage) {
+ var value = percentage;
+ if (!value || value.length < 1) {
+ value = '0 %';
+ } else {
+ value = value.toFixed(3).replace(/0+$/, '').replace(/\.$/, '') + '%';
+ }
+ return value;
+};
+
+/**
+ * A formatter which will turn elapsed time into display time like '50 ms',
+ * '5s', '10 m', '3 hr' etc. Time is expected to be provided in milliseconds.
+ *
+ * @type {Function}
+ */
+App.ChartView.TimeElapsedFormatter = function (millis) {
+ var value = millis;
+ if (!value || value.length < 1) {
+ value = '0 ms';
+ } else if ("number" == typeof millis) {
+ var seconds = millis > 1000 ? Math.round(millis / 1000) : 0;
+ var minutes = seconds > 60 ? Math.round(seconds / 60) : 0;
+ var hours = minutes > 60 ? Math.round(minutes / 60) : 0;
+ var days = hours > 24 ? Math.round(hours / 24) : 0;
+ if (days > 0) {
+ value = days + ' d';
+ } else if (hours > 0) {
+ value = hours + ' hr';
+ } else if (minutes > 0) {
+ value = minutes + ' m';
+ } else if (seconds > 0) {
+ value = seconds + ' s';
+ } else if (millis > 0) {
+ value = millis.toFixed(3).replace(/0+$/, '').replace(/\.$/, '') + ' ms';
+ } else {
+ value = millis.toFixed(3).replace(/0+$/, '').replace(/\.$/, '') + ' ms';
+ }
+ }
+ return value;
+};
+
+/**
+ * The default formatter which uses Rickshaw.Fixtures.Number.formatKMBT
+ * which shows 10K, 300M etc.
+ *
+ * @type {Function}
+ */
+App.ChartView.DefaultFormatter = function(y) {
+ if(isNaN(y)){
+ return 0;
+ }
+ var value = Rickshaw.Fixtures.Number.formatKMBT(y);
+ if (value == '') return '0';
+ value = String(value);
+ var c = value[value.length - 1];
+ if (!isNaN(parseInt(c))) {
+ // c is digit
+ value = parseFloat(value).toFixed(3).replace(/0+$/, '').replace(/\.$/, '');
+ }
+ else {
+ // c in not digit
+ value = parseFloat(value.substr(0, value.length - 1)).toFixed(3).replace(/0+$/, '').replace(/\.$/, '') + c;
+ }
+ return value;
+};
+
+
+/**
+ * Creates and returns a formatter that can convert a 'value'
+ * to 'value units/s'.
+ *
+ * @param unitsPrefix Prefix which will be used in 'unitsPrefix/s'
+ * @param valueFormatter Value itself will need further processing
+ * via provided formatter. Ex: '10M requests/s'. Generally
+ * should be App.ChartLinearTimeView.DefaultFormatter.
+ * @return {Function}
+ */
+App.ChartView.CreateRateFormatter = function (unitsPrefix, valueFormatter) {
+ var suffix = " "+unitsPrefix+"/s";
+ return function (value) {
+ value = valueFormatter(value) + suffix;
+ return value;
+ };
+};
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/ambari/blob/07556168/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric2_view.js
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric2_view.js b/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric2_view.js
new file mode 100644
index 0000000..f9a6487
--- /dev/null
+++ b/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric2_view.js
@@ -0,0 +1,63 @@
+/**
+ * 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.Metric2View = App.ChartView.extend({
+ id: "service-metrics-hdfs-jvm-threads",
+ title: 'jvm Threads',
+ renderer: 'line',
+
+ ajaxIndex: 'metrics2',
+
+ transformToSeries: function (jsonData) {
+ var seriesArray = [];
+ if (jsonData && jsonData.metrics && jsonData.metrics.jvm) {
+ for ( var name in jsonData.metrics.jvm) {
+ var displayName;
+ var seriesData = jsonData.metrics.jvm[name];
+ switch (name) {
+ case "threadsBlocked":
+ displayName = 'Threads Blocked';
+ break;
+ case "threadsWaiting":
+ displayName = 'Threads Waiting';
+ break;
+ case "threadsTimedWaiting":
+ displayName = 'Threads Timed Waiting';
+ break;
+ case "threadsRunnable":
+ displayName = 'Threads Runnable';
+ break;
+ default:
+ break;
+ }
+ if (seriesData) {
+ seriesArray.push(this.transformData(seriesData, displayName));
+ }
+ }
+ }
+ return seriesArray;
+ }
+});
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/ambari/blob/07556168/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric3_view.js
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric3_view.js b/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric3_view.js
new file mode 100644
index 0000000..b47d8dc
--- /dev/null
+++ b/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;
+ }
+});
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/ambari/blob/07556168/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric4_view.js
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric4_view.js b/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric4_view.js
new file mode 100644
index 0000000..9897a30
--- /dev/null
+++ b/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;
+ }
+});
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/ambari/blob/07556168/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric_view.js
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric_view.js b/contrib/views/slider/src/main/resources/ui/app/views/slider_app/metrics/metric_view.js
new file mode 100644
index 0000000..5f6c671
--- /dev/null
+++ b/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;
+ }
+});
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/ambari/blob/07556168/contrib/views/slider/src/main/resources/ui/app/views/slider_app/summary_view.js
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/app/views/slider_app/summary_view.js b/contrib/views/slider/src/main/resources/ui/app/views/slider_app/summary_view.js
new file mode 100644
index 0000000..bad2eb9
--- /dev/null
+++ b/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/07556168/contrib/views/slider/src/main/resources/ui/config.js
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/config.js b/contrib/views/slider/src/main/resources/ui/config.js
index 8ba0087..978d984 100755
--- a/contrib/views/slider/src/main/resources/ui/config.js
+++ b/contrib/views/slider/src/main/resources/ui/config.js
@@ -28,8 +28,15 @@ exports.config = {
},
order: {
before: [
+ 'vendor/scripts/common/d3.v2.js',
+ 'vendor/scripts/common/cubism.v1.js',
+ 'vendor/scripts/common/rickshaw.js',
'vendor/scripts/common/console-polyfill.js',
'vendor/scripts/common/jquery.js',
+ 'vendor/scripts/common/jquery.ui.core.js',
+ 'vendor/scripts/common/jquery.ui.widget.js',
+ 'vendor/scripts/common/jquery.ui.mouse.js',
+ 'vendor/scripts/common/jquery.ui.sortable.js',
'vendor/scripts/common/handlebars.js',
'vendor/scripts/development/ember.js',
'vendor/scripts/production/ember-data.js',
@@ -49,6 +56,8 @@ exports.config = {
},
order: {
before: [
+ 'vendor/styles/cubism.css',
+ 'vendor/styles/rickshaw.css',
'vendor/styles/bootstrap.css',
'vendor/styles/font-awesome.css',
'vendor/styles/font-awesome-ie7.css'
@@ -75,6 +84,9 @@ exports.config = {
},
order: {
before: [
+ 'vendor/scripts/common/d3.v2.js',
+ 'vendor/scripts/common/cubism.v1.js',
+ 'vendor/scripts/common/rickshaw.js',
'vendor/scripts/common/console-polyfill.js',
'vendor/scripts/common/jquery.js',
'vendor/scripts/common/handlebars.js',
http://git-wip-us.apache.org/repos/asf/ambari/blob/07556168/contrib/views/slider/src/main/resources/ui/package.json
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/package.json b/contrib/views/slider/src/main/resources/ui/package.json
index 2d2ea16..daf0943 100755
--- a/contrib/views/slider/src/main/resources/ui/package.json
+++ b/contrib/views/slider/src/main/resources/ui/package.json
@@ -26,7 +26,6 @@
"stylus-brunch": "~1.7.0",
"uglify-js-brunch": "~1.7.0",
"clean-css-brunch": "~1.7.0",
- "auto-reload-brunch": "~1.7.0",
"ember-handlebars-brunch": "git://github.com/fuseelements/ember-handlebars-brunch#fix/ember-1.3.0"
},
"devDependencies": {
http://git-wip-us.apache.org/repos/asf/ambari/blob/07556168/contrib/views/slider/src/main/resources/ui/vendor/scripts/common/cubism.v1.js
----------------------------------------------------------------------
diff --git a/contrib/views/slider/src/main/resources/ui/vendor/scripts/common/cubism.v1.js b/contrib/views/slider/src/main/resources/ui/vendor/scripts/common/cubism.v1.js
new file mode 100644
index 0000000..be9ff4d
--- /dev/null
+++ b/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);