1 
  2 /**
  3  * @fileoverview A simple charts grid implementation.
  4  * @version 1.0.0
  5  * @see https://google.github.io/styleguide/jsguide.html
  6  * @see https://github.com/google/closure-compiler/wiki
  7  */
  8 
  9 
 10 
 11 /**
 12  * Grid Constructor.
 13  * @param {string|Element} container The HTML container.
 14  * @requires formatters.NumberFormatter
 15  * @constructor
 16  */
 17 charts.Grid = function(container) {
 18 
 19   /**
 20    * Draws chart grid.
 21    * @param {Object=} opt_options A optional grid configuration options.
 22    */
 23   this.draw = function(opt_options) {
 24     options_ = self_.getOptions(opt_options);
 25     if (options_ && 'null' != options_['grid']) {
 26       /** @type {number} */ var width = container_.offsetWidth || 200;
 27       /** @type {number} */ var height = container_.offsetHeight || width;
 28 
 29       height -= options_['grid']['border'] * 2;
 30       drawLines_(drawCanvas_(width, height), width, height);
 31     }
 32   };
 33 
 34   /**
 35    * Gets grid options merged with defaults grid options.
 36    * @param {Object=} opt_options A optional grid configuration options.
 37    * @return {!Object.<string, *>} A map of name/value pairs.
 38    * @example
 39    * options: {
 40    *   'grid': {border': 1, 'color': '#ccc', 'lines': 5},
 41    *   'font': {'size': 11, 'family': 'Arial'},
 42    *   'hAxis': true,
 43    *   'vAxis': true
 44    * }
 45    */
 46   this.getOptions = function(opt_options) {
 47     opt_options = opt_options || {};
 48     opt_options['grid'] = opt_options['grid'] || {};
 49     opt_options['grid']['border'] = opt_options['grid']['border'] || 1;
 50     opt_options['grid']['color'] = opt_options['grid']['color'] || '#ccc';
 51     opt_options['grid']['lines'] = opt_options['grid']['lines'] || 5;
 52     opt_options['font'] = opt_options['font'] || {};
 53     opt_options['font']['size'] = opt_options['font']['size'] || 11;
 54     opt_options['font']['family'] = opt_options['font']['family'] || 'Arial';
 55     opt_options['hAxis'] = 'hAxis' in opt_options ? opt_options['hAxis'] : true;
 56     opt_options['vAxis'] = 'vAxis' in opt_options ? opt_options['vAxis'] : true;
 57     opt_options['grid']['direction'] =
 58         opt_options['grid']['direction'] || charts.Grid.DIRECTION.BOTTOM_TO_TOP;
 59     opt_options['scale'] =
 60         'scale' in opt_options ? opt_options['scale'] : false;
 61     return opt_options;
 62   };
 63 
 64   /**
 65    * @param {Node} canvas The canvas element.
 66    * @param {number} width The canvas width.
 67    * @param {number} height The canvas height.
 68    * @private
 69    */
 70   function drawLines_(canvas, width, height) {
 71     setTimeout(function() {container_.style.overflow = 'visible';}, 1);
 72     if (options_['scale']) scaledLines_(canvas, width, height);
 73     else simpleLines_(canvas, width, height);
 74 
 75     if (options_['hAxis'] && options_['data']['columns']) {
 76       drawColumnsLabels_(canvas, width, height);
 77     }
 78   }
 79 
 80   /**
 81    * @param {Node} canvas The canvas element.
 82    * @param {number} width The canvas width.
 83    * @param {number} height The canvas height.
 84    * @private
 85    */
 86   function scaledLines_(canvas, width, height) {
 87     /** @type {number} */ var lines = options_['grid']['lines'];
 88     /** @type {string} */ var color = options_['grid']['color'];
 89     /** @type {number} */ var padding = options_['padding'] || 0;
 90     /** @type {number} */ var border = options_['grid']['border'];
 91     /** @type {number} */ var minValue = options_['data']['min'];
 92     /** @type {number} */ var maxValue = options_['data']['max'];
 93     /** @type {number} */ var delta = maxValue - minValue;
 94 
 95     maxValue = maxValue <= 0 ? 1 : maxValue;
 96     delta = delta <= 0 ? 1 : delta;
 97 
 98     /** @type {number} */
 99     var logMinValue = Math.log(minValue) > 0 ? Math.log(minValue) : 0;
100     /** @type {number} */
101     var logMaxValue = Math.log(maxValue) > 0 ? Math.log(maxValue) : 0;
102     /** @type {number} */ var logDelta = Math.log(delta);
103     /** @type {number} */
104     var yPadding = options_['radius'] * 2 + (options_['grid']['lines'] - 1) / 2;
105     /** @type {number} */
106     var lineHeight = (height - padding * 2 - border) / (lines - 1) - border;
107     /** @type {number} */
108     var minY = Math.ceil((logDelta - (logMaxValue - logMinValue)) *
109         (height - yPadding * 2) / logDelta + yPadding);
110     /** @type {number} */
111     var maxY = Math.ceil(logDelta * (height - yPadding * 2) /
112         logDelta + yPadding);
113     /** @type {number} */ var deltaY = maxY - minY;
114     /** @type {number} */ var y = height - yPadding;
115 
116     for (/** @type {number} */ var i = 0; i < lines + 1; i++) {
117       /** @type {Node} */ var line = createElement_(canvas);
118       line.style.overflow = 'hidden';
119       if (i && !(!padding && (i == lines || i == 1))) {
120         line.style.borderTop = 'solid ' + border + 'px ' + color;
121       }
122       if (i > 0 && i <= lines) {
123         line.style.height = lineHeight + 'px';
124         line.style.paddingLeft = '1px';
125         if (options_['vAxis']) {
126           /** @type {number} */
127           var dy = deltaY + minY - ((y - yPadding) * deltaY) /
128               (height - yPadding * 2);
129           /** @type {number} */
130           var row = logDelta + logMinValue - ((dy - yPadding) *
131               logDelta) / (height - yPadding * 2);
132           /** @type {number} */ var value = Math.exp(row);
133           if (row < 1 && minValue <= 0) value = 0;
134           setRowLabel_(line, value);
135           y -= lineHeight;
136         }
137       } else {
138         line.style.height = padding + 'px';
139       }
140     }
141   }
142 
143   /**
144    * @param {Node} canvas The canvas element.
145    * @param {number} width The canvas width.
146    * @param {number} height The canvas height.
147    * @private
148    */
149   function simpleLines_(canvas, width, height) {
150     /** @type {number} */ var size = getRowSize_(width, height);
151     /** @type {number} */ var length = options_['grid']['lines'] - 1;
152     /** @type {number} */
153     var delta = options_['data']['max'] / length;
154     /** @type {string} */
155     var direction =
156         charts.Grid.DIRECTION.TOP_TO_BOTTOM == options_['direction'] ||
157         charts.Grid.DIRECTION.BOTTOM_TO_TOP == options_['direction'] ?
158         'Top' : 'Left';
159     for (/** @type {number} */ var index = 0; index <= length; index++) {
160       /** @type {Node} */ var line = createLine_(canvas, width, height);
161       line.style[direction.toLowerCase()] = (size * index) + 'px';
162       if (index && index < length) {
163         // Don't set border for first and last line.
164         line.style['border' + direction] = 'solid ' +
165             options_['grid']['border'] + 'px ' + options_['grid']['color'];
166       }
167 
168       if (options_['vAxis']) {
169         //setRowLabel_(line, delta / length * (length - index));
170         setRowLabel_(line, delta * (length - index));
171       }
172     }
173   }
174 
175   /**
176    * @param {Node} canvas The canvas element.
177    * @param {number} width The canvas width.
178    * @param {number} height The canvas height.
179    * @return {Node} Returns create grid line element.
180    * @private
181    */
182   function createLine_(canvas, width, height) {
183     /** @type {number} */ var direction = options_['direction'];
184     /** @type {number} */ var border = options_['grid']['border'];
185     /** @type {number} */ var size = getRowSize_(width, height);
186     /** @type {Node} */ var line;
187 
188     if (charts.Grid.DIRECTION.TOP_TO_BOTTOM == direction ||
189         charts.Grid.DIRECTION.BOTTOM_TO_TOP == direction) {
190       line = createElement_(canvas, width - border, size);
191     } else if (charts.Grid.DIRECTION.LEFT_TO_RIGHT == direction ||
192                charts.Grid.DIRECTION.RIGHT_TO_LEFT == direction) {
193       line = createElement_(canvas, size - border, height);
194     }
195 
196     return line;
197   }
198 
199   /**
200    * @param {number} width The canvas width.
201    * @param {number} height The canvas height.
202    * @return {number} Returns line size.
203    * @private
204    */
205   function getRowSize_(width, height) {
206     /** @type {number} */ var direction = options_['direction'];
207     /** @type {number} */ var border = options_['grid']['border'];
208     /** @type {number} */ var size = (
209         charts.Grid.DIRECTION.TOP_TO_BOTTOM == direction ||
210         charts.Grid.DIRECTION.BOTTOM_TO_TOP == direction) ? height : width;
211 
212     return (size - border) / (options_['grid']['lines'] - 1);
213   }
214 
215   /**
216    * @param {Node} canvas The canvas element.
217    * @param {number} width The canvas width.
218    * @param {number} height The canvas height.
219    * @private
220    */
221   function drawColumnsLabels_(canvas, width, height) {
222     /** @type {Array.<string>} */ var columns = options_['data']['columns'];
223     /** @type {number} */ var length = columns.length;
224     /** @type {number} */ var padding = (options_['padding'] || 1) * 2;
225     /** @type {number} */ var x = (width - padding * 2) / (length - 2);
226     /** @type {number} */ var fontSize = options_['font']['size'] || 13;
227     /** @type {string} */
228     var label = getColumnLabel_(columns[columns.length - 1]);
229     /** @type {number} */
230     var symbolWidth = label ? label.length * (fontSize - fontSize / 3) : 1;
231     /** @type {number} */
232     var maxAmount = Math.round((width - padding * 2) / symbolWidth);
233 
234     for (/** @type {number} */ var i = 1; i < length;) {
235       /** @type {Node} */ var div;
236       if (length > 2) {
237         div = createElement_(canvas, null, null, height + 5,
238             (padding + x * (i - 1) - symbolWidth / 2));
239       } else {
240         div = createElement_(canvas, null, null, height + 5,
241             (width / 2 - padding * 2));
242       }
243 
244       setColumnLabel_(div, columns[i]);
245       i += Math.round(Math.max(length / Math.round(maxAmount), 1));
246     }
247   }
248 
249   /**
250    * @param {*} column The column data.
251    * @return {string} Returns formatted column label.
252    * @private
253    */
254   function getColumnLabel_(column) {
255     if (column instanceof Date) {
256       column = formatters.DateFormatter.formatDate(
257           /** @type {Date} */(column), 'YYYY-MM-dd');
258     }
259     return '' + column;
260   }
261 
262   /**
263    * @param {Node} column The column element.
264    * @param {*} label The column label text.
265    * @see setRowLabel_(row, label)
266    * @private
267    */
268   function setColumnLabel_(column, label) {
269     column.style.position = 'absolute';
270     column.style.whiteSpace = 'nowrap';
271     column.innerHTML = getColumnLabel_(label);
272   }
273 
274   /**
275    * @param {Node} row The row element.
276    * @param {number} label The row label text.
277    * @private
278    */
279   function setRowLabel_(row, label) {
280     /** @type {number} */ var width = row.offsetWidth;
281     /** @type {number} */ var direction = options_['direction'];
282     /** @type {number} */ var font = options_['font']['size'];
283     /** @type {string} */ var css = 'overflow:hidden;position:absolute;';
284 
285     if (charts.Grid.DIRECTION.TOP_TO_BOTTOM == direction ||
286         charts.Grid.DIRECTION.LEFT_TO_RIGHT == direction) {
287       label = options_['data']['max'] - label;
288     } else if (charts.Grid.DIRECTION.BOTTOM_TO_TOP == direction ||
289         charts.Grid.DIRECTION.RIGHT_TO_LEFT == direction) {
290       label = label;
291     }
292 
293     if (charts.Grid.DIRECTION.LEFT_TO_RIGHT == direction ||
294         charts.Grid.DIRECTION.RIGHT_TO_LEFT == direction) {
295       css += 'margin-left:-' + (width / 2) + 'px;bottom:0;' +
296           'width:' + width + 'px;' +
297           'text-align:center;margin-bottom:-' + (font + font / 2) + 'px;';
298     } else {
299       width = 50;
300       css += 'left:-' + (width + 5) + 'px;width:' + width + 'px;' +
301           'text-align:right;margin-top:-' + (font / 2) + 'px;';
302     }
303     row.innerHTML = '<div style="' + css + '">' +
304                     formatter_.roundNumber(label) + '</div>';
305   }
306 
307   /**
308    * @param {number} width The canvas width.
309    * @param {number} height The canvas height.
310    * @return {Node} Returns canvas element.
311    * @private
312    */
313   function drawCanvas_(width, height) {
314     /** @type {number} */ var border = options_['grid']['border'];
315     /** @type {string} */ var color = options_['grid']['color'];
316     /** @type {Node} */
317     var canvas = createElement_(container_, width - border * 2, height);
318     canvas.style.border = 'solid ' + border + 'px ' + color;
319     canvas.style.fontFamily = options_['font']['family'];
320     canvas.style.fontSize = options_['font']['size'] + 'px';
321     canvas.style.color = color;
322     return canvas;
323   }
324 
325   /**
326    * @param {Node} parent The parent element.
327    * @param {?number=} opt_width The optional width of new element.
328    * @param {?number=} opt_height The optional height of new element.
329    * @param {?number=} opt_top The optional Y of new element.
330    * @param {?number=} opt_left The optional X of new element.
331    * @return {Node} Returns created element.
332    * @private
333    */
334   function createElement_(parent, opt_width, opt_height, opt_top, opt_left) {
335     /** @type {Node} */ var node = parent.appendChild(dom.createElement('DIV'));
336     node.style.position = 'absolute';
337     if (opt_width) node.style.width = opt_width + 'px';
338     if (opt_height) node.style.height = opt_height + 'px';
339     if (opt_top) node.style.top = opt_top + 'px';
340     if (opt_left) node.style.left = opt_left + 'px';
341     return node;
342   }
343 
344   /**
345    * The reference to current class instance. Used in private methods.
346    * @type {!charts.Grid}
347    * @private
348    */
349   var self_ = this;
350 
351   /**
352    * The reference to HTML chart container.
353    * @type {Element}
354    * @private
355    */
356   var container_ = typeof container == 'string' ?
357       dom.getElementById(container) : container;
358 
359   /**
360    * @dict
361    * @private
362    */
363   var options_ = null;
364 
365   /**
366    * @type {!formatters.NumberFormatter}
367    * @private
368    */
369   var formatter_ = new formatters.NumberFormatter;
370 };
371 
372 
373 /**
374  * Enum of grid direction.
375  * @enum {number}
376  * @static
377  * @example
378  * <code>{ LEFT_TO_RIGHT, RIGHT_TO_LEFT, BOTTOM_TO_TOP, TOP_TO_BOTTOM }</code>
379  */
380 charts.Grid.DIRECTION = {
381   LEFT_TO_RIGHT: 1, // Standard bar chart.
382   RIGHT_TO_LEFT: 2, // Flipped bar chart.
383   BOTTOM_TO_TOP: 3, // Column chart.
384   TOP_TO_BOTTOM: 4  // Flipped column chart.
385 };
386