PageRenderTime 60ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/scripts/jahova/Utilities/Graph/bluff-src.js

https://bitbucket.org/GTL/surveyjs-builder
JavaScript | 1489 lines | 940 code | 259 blank | 290 comment | 117 complexity | 135e8d7a346537a3015247c872a534ff MD5 | raw file
  1. /**
  2. * Bluff - beautiful graphs in JavaScript
  3. * ======================================
  4. *
  5. * Get the latest version and docs at http://bluff.jcoglan.com
  6. * Based on Gruff by Geoffrey Grosenbach: http://github.com/topfunky/gruff
  7. *
  8. * Copyright (C) 2008-2010 James Coglan
  9. *
  10. * Released under the MIT license and the GPL v2.
  11. * http://www.opensource.org/licenses/mit-license.php
  12. * http://www.gnu.org/licenses/gpl-2.0.txt
  13. **/
  14. Bluff = {
  15. // This is the version of Bluff you are using.
  16. VERSION: '0.3.6',
  17. array: function(list) {
  18. if (list.length === undefined) return [list];
  19. var ary = [], i = list.length;
  20. while (i--) ary[i] = list[i];
  21. return ary;
  22. },
  23. array_new: function(length, filler) {
  24. var ary = [];
  25. while (length--) ary.push(filler);
  26. return ary;
  27. },
  28. each: function(list, block, context) {
  29. for (var i = 0, n = list.length; i < n; i++) {
  30. block.call(context || null, list[i], i);
  31. }
  32. },
  33. index: function(list, needle) {
  34. for (var i = 0, n = list.length; i < n; i++) {
  35. if (list[i] === needle) return i;
  36. }
  37. return -1;
  38. },
  39. keys: function(object) {
  40. var ary = [], key;
  41. for (key in object) ary.push(key);
  42. return ary;
  43. },
  44. map: function(list, block, context) {
  45. var results = [];
  46. this.each(list, function(item) {
  47. results.push(block.call(context || null, item));
  48. });
  49. return results;
  50. },
  51. reverse_each: function(list, block, context) {
  52. var i = list.length;
  53. while (i--) block.call(context || null, list[i], i);
  54. },
  55. sum: function(list) {
  56. var sum = 0, i = list.length;
  57. while (i--) sum += list[i];
  58. return sum;
  59. },
  60. Mini: {}
  61. };
  62. Bluff.Base = new JS.Class({
  63. extend: {
  64. // Draw extra lines showing where the margins and text centers are
  65. DEBUG: false,
  66. // Used for navigating the array of data to plot
  67. DATA_LABEL_INDEX: 0,
  68. DATA_VALUES_INDEX: 1,
  69. DATA_COLOR_INDEX: 2,
  70. // Space around text elements. Mostly used for vertical spacing
  71. LEGEND_MARGIN: 20,
  72. TITLE_MARGIN: 20,
  73. LABEL_MARGIN: 10,
  74. DEFAULT_MARGIN: 20,
  75. DEFAULT_TARGET_WIDTH: 800,
  76. THOUSAND_SEPARATOR: ','
  77. },
  78. // Blank space above the graph
  79. top_margin: null,
  80. // Blank space below the graph
  81. bottom_margin: null,
  82. // Blank space to the right of the graph
  83. right_margin: null,
  84. // Blank space to the left of the graph
  85. left_margin: null,
  86. // Blank space below the title
  87. title_margin: null,
  88. // Blank space below the legend
  89. legend_margin: null,
  90. // A hash of names for the individual columns, where the key is the array
  91. // index for the column this label represents.
  92. //
  93. // Not all columns need to be named.
  94. //
  95. // Example: {0: 2005, 3: 2006, 5: 2007, 7: 2008}
  96. labels: null,
  97. // Used internally for spacing.
  98. //
  99. // By default, labels are centered over the point they represent.
  100. center_labels_over_point: null,
  101. // Used internally for horizontal graph types.
  102. has_left_labels: null,
  103. // A label for the bottom of the graph
  104. x_axis_label: null,
  105. // A label for the left side of the graph
  106. y_axis_label: null,
  107. // x_axis_increment: null,
  108. // Manually set increment of the horizontal marking lines
  109. y_axis_increment: null,
  110. // Get or set the list of colors that will be used to draw the bars or lines.
  111. colors: null,
  112. // The large title of the graph displayed at the top
  113. title: null,
  114. // Font used for titles, labels, etc.
  115. font: null,
  116. font_color: null,
  117. // Prevent drawing of line markers
  118. hide_line_markers: null,
  119. // Prevent drawing of the legend
  120. hide_legend: null,
  121. // Prevent drawing of the title
  122. hide_title: null,
  123. // Prevent drawing of line numbers
  124. hide_line_numbers: null,
  125. // Message shown when there is no data. Fits up to 20 characters. Defaults
  126. // to "No Data."
  127. no_data_message: null,
  128. // The font size of the large title at the top of the graph
  129. title_font_size: null,
  130. // Optionally set the size of the font. Based on an 800x600px graph.
  131. // Default is 20.
  132. //
  133. // Will be scaled down if graph is smaller than 800px wide.
  134. legend_font_size: null,
  135. // The font size of the labels around the graph
  136. marker_font_size: null,
  137. // The color of the auxiliary lines
  138. marker_color: null,
  139. // The number of horizontal lines shown for reference
  140. marker_count: null,
  141. // You can manually set a minimum value instead of having the values
  142. // guessed for you.
  143. //
  144. // Set it after you have given all your data to the graph object.
  145. minimum_value: null,
  146. // You can manually set a maximum value, such as a percentage-based graph
  147. // that always goes to 100.
  148. //
  149. // If you use this, you must set it after you have given all your data to
  150. // the graph object.
  151. maximum_value: null,
  152. // Set to false if you don't want the data to be sorted with largest avg
  153. // values at the back.
  154. sort: null,
  155. // Experimental
  156. additional_line_values: null,
  157. // Experimental
  158. stacked: null,
  159. // Optionally set the size of the colored box by each item in the legend.
  160. // Default is 20.0
  161. //
  162. // Will be scaled down if graph is smaller than 800px wide.
  163. legend_box_size: null,
  164. // Set to true to enable tooltip displays
  165. tooltips: false,
  166. // If one numerical argument is given, the graph is drawn at 4/3 ratio
  167. // according to the given width (800 results in 800x600, 400 gives 400x300,
  168. // etc.).
  169. //
  170. // Or, send a geometry string for other ratios ('800x400', '400x225').
  171. initialize: function(renderer, target_width) {
  172. this._d = new Bluff.Renderer(renderer);
  173. target_width = target_width || this.klass.DEFAULT_TARGET_WIDTH;
  174. var geo;
  175. if (typeof target_width !== 'number') {
  176. geo = target_width.split('x');
  177. this._columns = parseFloat(geo[0]);
  178. this._rows = parseFloat(geo[1]);
  179. } else {
  180. this._columns = parseFloat(target_width);
  181. this._rows = this._columns * 0.75;
  182. }
  183. this.initialize_ivars();
  184. this._reset_themes();
  185. this.theme_keynote();
  186. this._listeners = {};
  187. },
  188. // Set instance variables for this object.
  189. //
  190. // Subclasses can override this, call super, then set values separately.
  191. //
  192. // This makes it possible to set defaults in a subclass but still allow
  193. // developers to change this values in their program.
  194. initialize_ivars: function() {
  195. // Internal for calculations
  196. this._raw_columns = 800;
  197. this._raw_rows = 800 * (this._rows/this._columns);
  198. this._column_count = 0;
  199. this.marker_count = null;
  200. this.maximum_value = this.minimum_value = null;
  201. this._has_data = false;
  202. this._data = [];
  203. this.labels = {};
  204. this._labels_seen = {};
  205. this.sort = true;
  206. this.title = null;
  207. this._scale = this._columns / this._raw_columns;
  208. this.marker_font_size = 21.0;
  209. this.legend_font_size = 20.0;
  210. this.title_font_size = 36.0;
  211. this.top_margin = this.bottom_margin =
  212. this.left_margin = this.right_margin = this.klass.DEFAULT_MARGIN;
  213. this.legend_margin = this.klass.LEGEND_MARGIN;
  214. this.title_margin = this.klass.TITLE_MARGIN;
  215. this.legend_box_size = 20.0;
  216. this.no_data_message = "No Data";
  217. this.hide_line_markers = this.hide_legend = this.hide_title = this.hide_line_numbers = false;
  218. this.center_labels_over_point = true;
  219. this.has_left_labels = false;
  220. this.additional_line_values = [];
  221. this._additional_line_colors = [];
  222. this._theme_options = {};
  223. this.x_axis_label = this.y_axis_label = null;
  224. this.y_axis_increment = null;
  225. this.stacked = null;
  226. this._norm_data = null;
  227. },
  228. // Sets the top, bottom, left and right margins to +margin+.
  229. set_margins: function(margin) {
  230. this.top_margin = this.left_margin = this.right_margin = this.bottom_margin = margin;
  231. },
  232. // Sets the font for graph text to the font at +font_path+.
  233. set_font: function(font_path) {
  234. this.font = font_path;
  235. this._d.font = this.font;
  236. },
  237. // Add a color to the list of available colors for lines.
  238. //
  239. // Example:
  240. // add_color('#c0e9d3')
  241. add_color: function(colorname) {
  242. this.colors.push(colorname);
  243. },
  244. // Replace the entire color list with a new array of colors. Also
  245. // aliased as the colors= setter method.
  246. //
  247. // If you specify fewer colors than the number of datasets you intend
  248. // to draw, 'increment_color' will cycle through the array, reusing
  249. // colors as needed.
  250. //
  251. // Note that (as with the 'set_theme' method), you should set up the color
  252. // list before you send your data (via the 'data' method). Calls to the
  253. // 'data' method made prior to this call will use whatever color scheme
  254. // was in place at the time data was called.
  255. //
  256. // Example:
  257. // replace_colors ['#cc99cc', '#d9e043', '#34d8a2']
  258. replace_colors: function(color_list) {
  259. this.colors = color_list || [];
  260. this._color_index = 0;
  261. },
  262. // You can set a theme manually. Assign a hash to this method before you
  263. // send your data.
  264. //
  265. // graph.set_theme({
  266. // colors: ['orange', 'purple', 'green', 'white', 'red'],
  267. // marker_color: 'blue',
  268. // background_colors: ['black', 'grey']
  269. // })
  270. //
  271. // background_image: 'squirrel.png' is also possible.
  272. //
  273. // (Or hopefully something better looking than that.)
  274. //
  275. set_theme: function(options) {
  276. this._reset_themes();
  277. this._theme_options = {
  278. colors: ['black', 'white'],
  279. additional_line_colors: [],
  280. marker_color: 'white',
  281. font_color: 'black',
  282. background_colors: null,
  283. background_image: null
  284. };
  285. for (var key in options) this._theme_options[key] = options[key];
  286. this.colors = this._theme_options.colors;
  287. this.marker_color = this._theme_options.marker_color;
  288. this.font_color = this._theme_options.font_color || this.marker_color;
  289. this._additional_line_colors = this._theme_options.additional_line_colors;
  290. this._render_background();
  291. },
  292. // Set just the background colors
  293. set_background: function(options) {
  294. if (options.colors)
  295. this._theme_options.background_colors = options.colors;
  296. if (options.image)
  297. this._theme_options.background_image = options.image;
  298. this._render_background();
  299. },
  300. // A color scheme similar to the popular presentation software.
  301. theme_keynote: function() {
  302. // Colors
  303. this._blue = '#6886B4';
  304. this._yellow = '#FDD84E';
  305. this._green = '#72AE6E';
  306. this._red = '#D1695E';
  307. this._purple = '#8A6EAF';
  308. this._orange = '#EFAA43';
  309. this._white = 'white';
  310. this.colors = [this._yellow, this._blue, this._green, this._red, this._purple, this._orange, this._white];
  311. this.set_theme({
  312. colors: this.colors,
  313. marker_color: 'white',
  314. font_color: 'white',
  315. background_colors: ['black', '#4a465a']
  316. });
  317. },
  318. // A color scheme plucked from the colors on the popular usability blog.
  319. theme_37signals: function() {
  320. // Colors
  321. this._green = '#339933';
  322. this._purple = '#cc99cc';
  323. this._blue = '#336699';
  324. this._yellow = '#FFF804';
  325. this._red = '#ff0000';
  326. this._orange = '#cf5910';
  327. this._black = 'black';
  328. this.colors = [this._yellow, this._blue, this._green, this._red, this._purple, this._orange, this._black];
  329. this.set_theme({
  330. colors: this.colors,
  331. marker_color: 'black',
  332. font_color: 'black',
  333. background_colors: ['#d1edf5', 'white']
  334. });
  335. },
  336. // A color scheme from the colors used on the 2005 Rails keynote
  337. // presentation at RubyConf.
  338. theme_rails_keynote: function() {
  339. // Colors
  340. this._green = '#00ff00';
  341. this._grey = '#333333';
  342. this._orange = '#ff5d00';
  343. this._red = '#f61100';
  344. this._white = 'white';
  345. this._light_grey = '#999999';
  346. this._black = 'black';
  347. this.colors = [this._green, this._grey, this._orange, this._red, this._white, this._light_grey, this._black];
  348. this.set_theme({
  349. colors: this.colors,
  350. marker_color: 'white',
  351. font_color: 'white',
  352. background_colors: ['#0083a3', '#0083a3']
  353. });
  354. },
  355. // A color scheme similar to that used on the popular podcast site.
  356. theme_odeo: function() {
  357. // Colors
  358. this._grey = '#202020';
  359. this._white = 'white';
  360. this._dark_pink = '#a21764';
  361. this._green = '#8ab438';
  362. this._light_grey = '#999999';
  363. this._dark_blue = '#3a5b87';
  364. this._black = 'black';
  365. this.colors = [this._grey, this._white, this._dark_blue, this._dark_pink, this._green, this._light_grey, this._black];
  366. this.set_theme({
  367. colors: this.colors,
  368. marker_color: 'white',
  369. font_color: 'white',
  370. background_colors: ['#ff47a4', '#ff1f81']
  371. });
  372. },
  373. // A pastel theme
  374. theme_pastel: function() {
  375. // Colors
  376. this.colors = [
  377. '#a9dada', // blue
  378. '#aedaa9', // green
  379. '#daaea9', // peach
  380. '#dadaa9', // yellow
  381. '#a9a9da', // dk purple
  382. '#daaeda', // purple
  383. '#dadada' // grey
  384. ];
  385. this.set_theme({
  386. colors: this.colors,
  387. marker_color: '#aea9a9', // Grey
  388. font_color: 'black',
  389. background_colors: 'white'
  390. });
  391. },
  392. // A greyscale theme
  393. theme_greyscale: function() {
  394. // Colors
  395. this.colors = [
  396. '#282828', //
  397. '#383838', //
  398. '#686868', //
  399. '#989898', //
  400. '#c8c8c8', //
  401. '#e8e8e8' //
  402. ];
  403. this.set_theme({
  404. colors: this.colors,
  405. marker_color: '#aea9a9', // Grey
  406. font_color: 'black',
  407. background_colors: 'white'
  408. });
  409. },
  410. // Parameters are an array where the first element is the name of the dataset
  411. // and the value is an array of values to plot.
  412. //
  413. // Can be called multiple times with different datasets for a multi-valued
  414. // graph.
  415. //
  416. // If the color argument is nil, the next color from the default theme will
  417. // be used.
  418. //
  419. // NOTE: If you want to use a preset theme, you must set it before calling
  420. // data().
  421. //
  422. // Example:
  423. // data("Bart S.", [95, 45, 78, 89, 88, 76], '#ffcc00')
  424. data: function(name, data_points, color) {
  425. data_points = (data_points === undefined) ? [] : data_points;
  426. color = color || null;
  427. data_points = Bluff.array(data_points); // make sure it's an array
  428. this._data.push([name, data_points, (color || this._increment_color())]);
  429. // Set column count if this is larger than previous counts
  430. this._column_count = (data_points.length > this._column_count) ? data_points.length : this._column_count;
  431. // Pre-normalize
  432. Bluff.each(data_points, function(data_point, index) {
  433. if (data_point === undefined) return;
  434. // Setup max/min so spread starts at the low end of the data points
  435. if (this.maximum_value === null && this.minimum_value === null)
  436. this.maximum_value = this.minimum_value = data_point;
  437. // TODO Doesn't work with stacked bar graphs
  438. // Original: @maximum_value = _larger_than_max?(data_point, index) ? max(data_point, index) : @maximum_value
  439. this.maximum_value = this._larger_than_max(data_point) ? data_point : this.maximum_value;
  440. if (this.maximum_value >= 0) this._has_data = true;
  441. this.minimum_value = this._less_than_min(data_point) ? data_point : this.minimum_value;
  442. if (this.minimum_value < 0) this._has_data = true;
  443. }, this);
  444. },
  445. // Overridden by subclasses to do the actual plotting of the graph.
  446. //
  447. // Subclasses should start by calling super() for this method.
  448. draw: function() {
  449. if (this.stacked) this._make_stacked();
  450. this._setup_drawing();
  451. this._debug(function() {
  452. // Outer margin
  453. this._d.rectangle(this.left_margin, this.top_margin,
  454. this._raw_columns - this.right_margin, this._raw_rows - this.bottom_margin);
  455. // Graph area box
  456. this._d.rectangle(this._graph_left, this._graph_top, this._graph_right, this._graph_bottom);
  457. });
  458. },
  459. clear: function() {
  460. this._render_background();
  461. },
  462. on: function(eventType, callback, context) {
  463. var list = this._listeners[eventType] = this._listeners[eventType] || [];
  464. list.push([callback, context]);
  465. },
  466. trigger: function(eventType, data) {
  467. var list = this._listeners[eventType];
  468. if (!list) return;
  469. Bluff.each(list, function(listener) {
  470. listener[0].call(listener[1], data);
  471. });
  472. },
  473. // Calculates size of drawable area and draws the decorations.
  474. //
  475. // * line markers
  476. // * legend
  477. // * title
  478. _setup_drawing: function() {
  479. // Maybe should be done in one of the following functions for more granularity.
  480. if (!this._has_data) return this._draw_no_data();
  481. this._normalize();
  482. this._setup_graph_measurements();
  483. if (this.sort) this._sort_norm_data();
  484. this._draw_legend();
  485. this._draw_line_markers();
  486. this._draw_axis_labels();
  487. this._draw_title();
  488. },
  489. // Make copy of data with values scaled between 0-100
  490. _normalize: function(force) {
  491. if (this._norm_data === null || force === true) {
  492. this._norm_data = [];
  493. if (!this._has_data) return;
  494. this._calculate_spread();
  495. Bluff.each(this._data, function(data_row) {
  496. var norm_data_points = [];
  497. Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point) {
  498. if (data_point === null || data_point === undefined)
  499. norm_data_points.push(null);
  500. else
  501. norm_data_points.push((data_point - this.minimum_value) / this._spread);
  502. }, this);
  503. this._norm_data.push([data_row[this.klass.DATA_LABEL_INDEX], norm_data_points, data_row[this.klass.DATA_COLOR_INDEX]]);
  504. }, this);
  505. }
  506. },
  507. _calculate_spread: function() {
  508. this._spread = this.maximum_value - this.minimum_value;
  509. this._spread = this._spread > 0 ? this._spread : 1;
  510. var power = Math.round(Math.LOG10E*Math.log(this._spread));
  511. this._significant_digits = Math.pow(10, 3 - power);
  512. },
  513. // Calculates size of drawable area, general font dimensions, etc.
  514. _setup_graph_measurements: function() {
  515. this._marker_caps_height = this.hide_line_markers ? 0 :
  516. this._calculate_caps_height(this.marker_font_size);
  517. this._title_caps_height = this.hide_title ? 0 :
  518. this._calculate_caps_height(this.title_font_size);
  519. this._legend_caps_height = this.hide_legend ? 0 :
  520. this._calculate_caps_height(this.legend_font_size);
  521. var longest_label,
  522. longest_left_label_width,
  523. line_number_width,
  524. last_label,
  525. extra_room_for_long_label,
  526. x_axis_label_height,
  527. key;
  528. if (this.hide_line_markers) {
  529. this._graph_left = this.left_margin;
  530. this._graph_right_margin = this.right_margin;
  531. this._graph_bottom_margin = this.bottom_margin;
  532. } else {
  533. longest_left_label_width = 0;
  534. if (this.has_left_labels) {
  535. longest_label = '';
  536. for (key in this.labels) {
  537. longest_label = longest_label.length > this.labels[key].length
  538. ? longest_label
  539. : this.labels[key];
  540. }
  541. longest_left_label_width = this._calculate_width(this.marker_font_size, longest_label) * 1.25;
  542. } else {
  543. longest_left_label_width = this._calculate_width(this.marker_font_size, this._label(this.maximum_value));
  544. }
  545. // Shift graph if left line numbers are hidden
  546. line_number_width = this.hide_line_numbers && !this.has_left_labels ?
  547. 0.0 :
  548. longest_left_label_width + this.klass.LABEL_MARGIN * 2;
  549. this._graph_left = this.left_margin +
  550. line_number_width +
  551. (this.y_axis_label === null ? 0.0 : this._marker_caps_height + this.klass.LABEL_MARGIN * 2);
  552. // Make space for half the width of the rightmost column label.
  553. // Might be greater than the number of columns if between-style bar markers are used.
  554. last_label = -Infinity;
  555. for (key in this.labels)
  556. last_label = last_label > Number(key) ? last_label : Number(key);
  557. last_label = Math.round(last_label);
  558. extra_room_for_long_label = (last_label >= (this._column_count-1) && this.center_labels_over_point) ?
  559. this._calculate_width(this.marker_font_size, this.labels[last_label]) / 2 :
  560. 0;
  561. this._graph_right_margin = this.right_margin + extra_room_for_long_label;
  562. this._graph_bottom_margin = this.bottom_margin +
  563. this._marker_caps_height + this.klass.LABEL_MARGIN;
  564. }
  565. this._graph_right = this._raw_columns - this._graph_right_margin;
  566. this._graph_width = this._raw_columns - this._graph_left - this._graph_right_margin;
  567. // When hide_title, leave a title_margin space for aesthetics.
  568. // Same with hide_legend
  569. this._graph_top = this.top_margin +
  570. (this.hide_title ? this.title_margin : this._title_caps_height + this.title_margin ) +
  571. (this.hide_legend ? this.legend_margin : this._legend_caps_height + this.legend_margin);
  572. x_axis_label_height = (this.x_axis_label === null) ? 0.0 :
  573. this._marker_caps_height + this.klass.LABEL_MARGIN;
  574. this._graph_bottom = this._raw_rows - this._graph_bottom_margin - x_axis_label_height;
  575. this._graph_height = this._graph_bottom - this._graph_top;
  576. },
  577. // Draw the optional labels for the x axis and y axis.
  578. _draw_axis_labels: function() {
  579. if (this.x_axis_label) {
  580. // X Axis
  581. // Centered vertically and horizontally by setting the
  582. // height to 1.0 and the width to the width of the graph.
  583. var x_axis_label_y_coordinate = this._graph_bottom + this.klass.LABEL_MARGIN * 2 + this._marker_caps_height;
  584. // TODO Center between graph area
  585. this._d.fill = this.font_color;
  586. if (this.font) this._d.font = this.font;
  587. this._d.stroke = 'transparent';
  588. this._d.pointsize = this._scale_fontsize(this.marker_font_size);
  589. this._d.gravity = 'north';
  590. this._d.annotate_scaled(
  591. this._raw_columns, 1.0,
  592. 0.0, x_axis_label_y_coordinate,
  593. this.x_axis_label, this._scale);
  594. this._debug(function() {
  595. this._d.line(0.0, x_axis_label_y_coordinate, this._raw_columns, x_axis_label_y_coordinate);
  596. });
  597. }
  598. // TODO Y label (not generally possible in browsers)
  599. },
  600. // Draws horizontal background lines and labels
  601. _draw_line_markers: function() {
  602. if (this.hide_line_markers) return;
  603. if (this.y_axis_increment === null) {
  604. // Try to use a number of horizontal lines that will come out even.
  605. //
  606. // TODO Do the same for larger numbers...100, 75, 50, 25
  607. if (this.marker_count === null) {
  608. Bluff.each([3,4,5,6,7], function(lines) {
  609. if (!this.marker_count && this._spread % lines === 0)
  610. this.marker_count = lines;
  611. }, this);
  612. this.marker_count = this.marker_count || 4;
  613. }
  614. this._increment = (this._spread > 0) ? this._significant(this._spread / this.marker_count) : 1;
  615. } else {
  616. // TODO Make this work for negative values
  617. this.maximum_value = Math.max(Math.ceil(this.maximum_value), this.y_axis_increment);
  618. this.minimum_value = Math.floor(this.minimum_value);
  619. this._calculate_spread();
  620. this._normalize(true);
  621. this.marker_count = Math.round(this._spread / this.y_axis_increment);
  622. this._increment = this.y_axis_increment;
  623. }
  624. this._increment_scaled = this._graph_height / (this._spread / this._increment);
  625. // Draw horizontal line markers and annotate with numbers
  626. var index, n, y, marker_label;
  627. for (index = 0, n = this.marker_count; index <= n; index++) {
  628. y = this._graph_top + this._graph_height - index * this._increment_scaled;
  629. this._d.stroke = this.marker_color;
  630. this._d.stroke_width = 1;
  631. this._d.line(this._graph_left, y, this._graph_right, y);
  632. marker_label = index * this._increment + this.minimum_value;
  633. if (!this.hide_line_numbers) {
  634. this._d.fill = this.font_color;
  635. if (this.font) this._d.font = this.font;
  636. this._d.font_weight = 'normal';
  637. this._d.stroke = 'transparent';
  638. this._d.pointsize = this._scale_fontsize(this.marker_font_size);
  639. this._d.gravity = 'east';
  640. // Vertically center with 1.0 for the height
  641. this._d.annotate_scaled(this._graph_left - this.klass.LABEL_MARGIN,
  642. 1.0, 0.0, y,
  643. this._label(marker_label), this._scale);
  644. }
  645. }
  646. },
  647. _center: function(size) {
  648. return (this._raw_columns - size) / 2;
  649. },
  650. // Draws a legend with the names of the datasets matched to the colors used
  651. // to draw them.
  652. _draw_legend: function() {
  653. if (this.hide_legend) return;
  654. this._legend_labels = Bluff.map(this._data, function(item) {
  655. return item[this.klass.DATA_LABEL_INDEX];
  656. }, this);
  657. var legend_square_width = this.legend_box_size; // small square with color of this item
  658. // May fix legend drawing problem at small sizes
  659. if (this.font) this._d.font = this.font;
  660. this._d.pointsize = this.legend_font_size;
  661. var label_widths = [[]]; // Used to calculate line wrap
  662. Bluff.each(this._legend_labels, function(label) {
  663. var last = label_widths.length - 1;
  664. var metrics = this._d.get_type_metrics(label);
  665. var label_width = metrics.width + legend_square_width * 2.7;
  666. label_widths[last].push(label_width);
  667. if (Bluff.sum(label_widths[last]) > (this._raw_columns * 0.9))
  668. label_widths.push([label_widths[last].pop()]);
  669. }, this);
  670. var current_x_offset = this._center(Bluff.sum(label_widths[0]));
  671. var current_y_offset = this.hide_title ?
  672. this.top_margin + this.title_margin :
  673. this.top_margin + this.title_margin + this._title_caps_height;
  674. this._debug(function() {
  675. this._d.stroke_width = 1;
  676. this._d.line(0, current_y_offset, this._raw_columns, current_y_offset);
  677. });
  678. Bluff.each(this._legend_labels, function(legend_label, index) {
  679. // Draw label
  680. this._d.fill = this.font_color;
  681. if (this.font) this._d.font = this.font;
  682. this._d.pointsize = this._scale_fontsize(this.legend_font_size);
  683. this._d.stroke = 'transparent';
  684. this._d.font_weight = 'normal';
  685. this._d.gravity = 'west';
  686. this._d.annotate_scaled(this._raw_columns, 1.0,
  687. current_x_offset + (legend_square_width * 1.7), current_y_offset,
  688. legend_label, this._scale);
  689. // Now draw box with color of this dataset
  690. this._d.stroke = 'transparent';
  691. this._d.fill = this._data[index][this.klass.DATA_COLOR_INDEX];
  692. this._d.rectangle(current_x_offset,
  693. current_y_offset - legend_square_width / 2.0,
  694. current_x_offset + legend_square_width,
  695. current_y_offset + legend_square_width / 2.0);
  696. this._d.pointsize = this.legend_font_size;
  697. var metrics = this._d.get_type_metrics(legend_label);
  698. var current_string_offset = metrics.width + (legend_square_width * 2.7),
  699. line_height;
  700. // Handle wrapping
  701. label_widths[0].shift();
  702. if (label_widths[0].length == 0) {
  703. this._debug(function() {
  704. this._d.line(0.0, current_y_offset, this._raw_columns, current_y_offset);
  705. });
  706. label_widths.shift();
  707. if (label_widths.length > 0) current_x_offset = this._center(Bluff.sum(label_widths[0]));
  708. line_height = Math.max(this._legend_caps_height, legend_square_width) + this.legend_margin;
  709. if (label_widths.length > 0) {
  710. // Wrap to next line and shrink available graph dimensions
  711. current_y_offset += line_height;
  712. this._graph_top += line_height;
  713. this._graph_height = this._graph_bottom - this._graph_top;
  714. }
  715. } else {
  716. current_x_offset += current_string_offset;
  717. }
  718. }, this);
  719. this._color_index = 0;
  720. },
  721. // Draws a title on the graph.
  722. _draw_title: function() {
  723. if (this.hide_title || !this.title) return;
  724. this._d.fill = this.font_color;
  725. if (this.font) this._d.font = this.font;
  726. this._d.pointsize = this._scale_fontsize(this.title_font_size);
  727. this._d.font_weight = 'bold';
  728. this._d.gravity = 'north';
  729. this._d.annotate_scaled(this._raw_columns, 1.0,
  730. 0, this.top_margin,
  731. this.title, this._scale);
  732. },
  733. // Draws column labels below graph, centered over x_offset
  734. //--
  735. // TODO Allow WestGravity as an option
  736. _draw_label: function(x_offset, index) {
  737. if (this.hide_line_markers) return;
  738. var y_offset;
  739. if (this.labels[index] && !this._labels_seen[index]) {
  740. y_offset = this._graph_bottom + this.klass.LABEL_MARGIN;
  741. this._d.fill = this.font_color;
  742. if (this.font) this._d.font = this.font;
  743. this._d.stroke = 'transparent';
  744. this._d.font_weight = 'normal';
  745. this._d.pointsize = this._scale_fontsize(this.marker_font_size);
  746. this._d.gravity = 'north';
  747. this._d.annotate_scaled(1.0, 1.0,
  748. x_offset, y_offset,
  749. this.labels[index], this._scale);
  750. this._labels_seen[index] = true;
  751. this._debug(function() {
  752. this._d.stroke_width = 1;
  753. this._d.line(0.0, y_offset, this._raw_columns, y_offset);
  754. });
  755. }
  756. },
  757. // Creates a mouse hover target rectangle for tooltip displays
  758. _draw_tooltip: function(left, top, width, height, name, color, data, index) {
  759. if (!this.tooltips) return;
  760. var node = this._d.tooltip(left, top, width, height, name, color, data);
  761. Bluff.Event.observe(node, 'click', function() {
  762. var point = {
  763. series: name,
  764. label: this.labels[index],
  765. value: data,
  766. color: color
  767. };
  768. this.trigger('click:datapoint', point);
  769. }, this);
  770. },
  771. // Shows an error message because you have no data.
  772. _draw_no_data: function() {
  773. this._d.fill = this.font_color;
  774. if (this.font) this._d.font = this.font;
  775. this._d.stroke = 'transparent';
  776. this._d.font_weight = 'normal';
  777. this._d.pointsize = this._scale_fontsize(80);
  778. this._d.gravity = 'center';
  779. this._d.annotate_scaled(this._raw_columns, this._raw_rows/2,
  780. 0, 10,
  781. this.no_data_message, this._scale);
  782. },
  783. // Finds the best background to render based on the provided theme options.
  784. _render_background: function() {
  785. var colors = this._theme_options.background_colors;
  786. switch (true) {
  787. case colors instanceof Array:
  788. this._render_gradiated_background.apply(this, colors);
  789. break;
  790. case typeof colors === 'string':
  791. this._render_solid_background(colors);
  792. break;
  793. default:
  794. this._render_image_background(this._theme_options.background_image);
  795. break;
  796. }
  797. },
  798. // Make a new image at the current size with a solid +color+.
  799. _render_solid_background: function(color) {
  800. this._d.render_solid_background(this._columns, this._rows, color);
  801. },
  802. // Use with a theme definition method to draw a gradiated background.
  803. _render_gradiated_background: function(top_color, bottom_color) {
  804. this._d.render_gradiated_background(this._columns, this._rows, top_color, bottom_color);
  805. },
  806. // Use with a theme to use an image (800x600 original) background.
  807. _render_image_background: function(image_path) {
  808. // TODO
  809. },
  810. // Resets everything to defaults (except data).
  811. _reset_themes: function() {
  812. this._color_index = 0;
  813. this._labels_seen = {};
  814. this._theme_options = {};
  815. this._d.scale(this._scale, this._scale);
  816. },
  817. _scale_value: function(value) {
  818. return this._scale * value;
  819. },
  820. // Return a comparable fontsize for the current graph.
  821. _scale_fontsize: function(value) {
  822. var new_fontsize = value * this._scale;
  823. return new_fontsize;
  824. },
  825. _clip_value_if_greater_than: function(value, max_value) {
  826. return (value > max_value) ? max_value : value;
  827. },
  828. // Overridden by subclasses such as stacked bar.
  829. _larger_than_max: function(data_point, index) {
  830. return data_point > this.maximum_value;
  831. },
  832. _less_than_min: function(data_point, index) {
  833. return data_point < this.minimum_value;
  834. },
  835. // Overridden by subclasses that need it.
  836. _max: function(data_point, index) {
  837. return data_point;
  838. },
  839. // Overridden by subclasses that need it.
  840. _min: function(data_point, index) {
  841. return data_point;
  842. },
  843. _significant: function(inc) {
  844. if (inc == 0) return 1.0;
  845. var factor = 1.0;
  846. while (inc < 10) {
  847. inc *= 10;
  848. factor /= 10;
  849. }
  850. while (inc > 100) {
  851. inc /= 10;
  852. factor *= 10;
  853. }
  854. return Math.floor(inc) * factor;
  855. },
  856. // Sort with largest overall summed value at front of array so it shows up
  857. // correctly in the drawn graph.
  858. _sort_norm_data: function() {
  859. var sums = this._sums, index = this.klass.DATA_VALUES_INDEX;
  860. this._norm_data.sort(function(a,b) {
  861. return sums(b[index]) - sums(a[index]);
  862. });
  863. this._data.sort(function(a,b) {
  864. return sums(b[index]) - sums(a[index]);
  865. });
  866. },
  867. _sums: function(data_set) {
  868. var total_sum = 0;
  869. Bluff.each(data_set, function(num) { total_sum += (num || 0) });
  870. return total_sum;
  871. },
  872. _make_stacked: function() {
  873. var stacked_values = [], i = this._column_count;
  874. while (i--) stacked_values[i] = 0;
  875. Bluff.each(this._data, function(value_set) {
  876. Bluff.each(value_set[this.klass.DATA_VALUES_INDEX], function(value, index) {
  877. stacked_values[index] += value;
  878. }, this);
  879. value_set[this.klass.DATA_VALUES_INDEX] = Bluff.array(stacked_values);
  880. }, this);
  881. },
  882. // Takes a block and draws it if DEBUG is true.
  883. //
  884. // Example:
  885. // debug { @d.rectangle x1, y1, x2, y2 }
  886. _debug: function(block) {
  887. if (this.klass.DEBUG) {
  888. this._d.fill = 'transparent';
  889. this._d.stroke = 'turquoise';
  890. block.call(this);
  891. }
  892. },
  893. // Returns the next color in your color list.
  894. _increment_color: function() {
  895. var offset = this._color_index;
  896. this._color_index = (this._color_index + 1) % this.colors.length;
  897. return this.colors[offset];
  898. },
  899. // Return a formatted string representing a number value that should be
  900. // printed as a label.
  901. _label: function(value) {
  902. var sep = this.klass.THOUSAND_SEPARATOR,
  903. label = (this._spread % this.marker_count == 0 || this.y_axis_increment !== null)
  904. ? String(Math.round(value))
  905. : String(Math.floor(value * this._significant_digits)/this._significant_digits);
  906. var parts = label.split('.');
  907. parts[0] = parts[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1' + sep);
  908. return parts.join('.');
  909. },
  910. // Returns the height of the capital letter 'X' for the current font and
  911. // size.
  912. //
  913. // Not scaled since it deals with dimensions that the regular scaling will
  914. // handle.
  915. _calculate_caps_height: function(font_size) {
  916. return this._d.caps_height(font_size);
  917. },
  918. // Returns the width of a string at this pointsize.
  919. //
  920. // Not scaled since it deals with dimensions that the regular
  921. // scaling will handle.
  922. _calculate_width: function(font_size, text) {
  923. return this._d.text_width(font_size, text);
  924. }
  925. });
  926. Bluff.Area = new JS.Class(Bluff.Base, {
  927. draw: function() {
  928. this.callSuper();
  929. if (!this._has_data) return;
  930. this._x_increment = this._graph_width / (this._column_count - 1);
  931. this._d.stroke = 'transparent';
  932. Bluff.each(this._norm_data, function(data_row) {
  933. var poly_points = [],
  934. prev_x = 0.0,
  935. prev_y = 0.0;
  936. Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, index) {
  937. // Use incremented x and scaled y
  938. var new_x = this._graph_left + (this._x_increment * index);
  939. var new_y = this._graph_top + (this._graph_height - data_point * this._graph_height);
  940. if (prev_x > 0 && prev_y > 0) {
  941. poly_points.push(new_x);
  942. poly_points.push(new_y);
  943. // this._d.polyline(prev_x, prev_y, new_x, new_y);
  944. } else {
  945. poly_points.push(this._graph_left);
  946. poly_points.push(this._graph_bottom - 1);
  947. poly_points.push(new_x);
  948. poly_points.push(new_y);
  949. // this._d.polyline(this._graph_left, this._graph_bottom, new_x, new_y);
  950. }
  951. this._draw_label(new_x, index);
  952. prev_x = new_x;
  953. prev_y = new_y;
  954. }, this);
  955. // Add closing points, draw polygon
  956. poly_points.push(this._graph_right);
  957. poly_points.push(this._graph_bottom - 1);
  958. poly_points.push(this._graph_left);
  959. poly_points.push(this._graph_bottom - 1);
  960. this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
  961. this._d.polyline(poly_points);
  962. }, this);
  963. }
  964. });
  965. // This class perfoms the y coordinats conversion for the bar class.
  966. //
  967. // There are three cases:
  968. //
  969. // 1. Bars all go from zero in positive direction
  970. // 2. Bars all go from zero to negative direction
  971. // 3. Bars either go from zero to positive or from zero to negative
  972. //
  973. Bluff.BarConversion = new JS.Class({
  974. mode: null,
  975. zero: null,
  976. graph_top: null,
  977. graph_height: null,
  978. minimum_value: null,
  979. spread: null,
  980. getLeftYRightYscaled: function(data_point, result) {
  981. var val;
  982. switch (this.mode) {
  983. case 1: // Case one
  984. // minimum value >= 0 ( only positiv values )
  985. result[0] = this.graph_top + this.graph_height*(1 - data_point) + 1;
  986. result[1] = this.graph_top + this.graph_height - 1;
  987. break;
  988. case 2: // Case two
  989. // only negativ values
  990. result[0] = this.graph_top + 1;
  991. result[1] = this.graph_top + this.graph_height*(1 - data_point) - 1;
  992. break;
  993. case 3: // Case three
  994. // positiv and negativ values
  995. val = data_point-this.minimum_value/this.spread;
  996. if ( data_point >= this.zero ) {
  997. result[0] = this.graph_top + this.graph_height*(1 - (val-this.zero)) + 1;
  998. result[1] = this.graph_top + this.graph_height*(1 - this.zero) - 1;
  999. } else {
  1000. result[0] = this.graph_top + this.graph_height*(1 - (val-this.zero)) + 1;
  1001. result[1] = this.graph_top + this.graph_height*(1 - this.zero) - 1;
  1002. }
  1003. break;
  1004. default:
  1005. result[0] = 0.0;
  1006. result[1] = 0.0;
  1007. }
  1008. }
  1009. });
  1010. Bluff.Bar = new JS.Class(Bluff.Base, {
  1011. // Spacing factor applied between bars
  1012. bar_spacing: 0.9,
  1013. draw: function() {
  1014. // Labels will be centered over the left of the bar if
  1015. // there are more labels than columns. This is basically the same
  1016. // as where it would be for a line graph.
  1017. this.center_labels_over_point = (Bluff.keys(this.labels).length > this._column_count);
  1018. this.callSuper();
  1019. if (!this._has_data) return;
  1020. this._draw_bars();
  1021. },
  1022. _draw_bars: function() {
  1023. this._bar_width = this._graph_width / (this._column_count * this._data.length);
  1024. var padding = (this._bar_width * (1 - this.bar_spacing)) / 2;
  1025. this._d.stroke_opacity = 0.0;
  1026. // Setup the BarConversion Object
  1027. var conversion = new Bluff.BarConversion();
  1028. conversion.graph_height = this._graph_height;
  1029. conversion.graph_top = this._graph_top;
  1030. // Set up the right mode [1,2,3] see BarConversion for further explanation
  1031. if (this.minimum_value >= 0) {
  1032. // all bars go from zero to positiv
  1033. conversion.mode = 1;
  1034. } else {
  1035. // all bars go from 0 to negativ
  1036. if (this.maximum_value <= 0) {
  1037. conversion.mode = 2;
  1038. } else {
  1039. // bars either go from zero to negativ or to positiv
  1040. conversion.mode = 3;
  1041. conversion.spread = this._spread;
  1042. conversion.minimum_value = this.minimum_value;
  1043. conversion.zero = -this.minimum_value/this._spread;
  1044. }
  1045. }
  1046. // iterate over all normalised data
  1047. Bluff.each(this._norm_data, function(data_row, row_index) {
  1048. var raw_data = this._data[row_index][this.klass.DATA_VALUES_INDEX];
  1049. Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, point_index) {
  1050. // Use incremented x and scaled y
  1051. // x
  1052. var left_x = this._graph_left + (this._bar_width * (row_index + point_index + ((this._data.length - 1) * point_index))) + padding;
  1053. var right_x = left_x + this._bar_width * this.bar_spacing;
  1054. // y
  1055. var conv = [];
  1056. conversion.getLeftYRightYscaled(data_point, conv);
  1057. // create new bar
  1058. this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
  1059. this._d.rectangle(left_x, conv[0], right_x, conv[1]);
  1060. // create tooltip target
  1061. this._draw_tooltip(left_x, conv[0],
  1062. right_x - left_x, conv[1] - conv[0],
  1063. data_row[this.klass.DATA_LABEL_INDEX],
  1064. data_row[this.klass.DATA_COLOR_INDEX],
  1065. raw_data[point_index], point_index);
  1066. // Calculate center based on bar_width and current row
  1067. var label_center = this._graph_left +
  1068. (this._data.length * this._bar_width * point_index) +
  1069. (this._data.length * this._bar_width / 2.0);
  1070. // Subtract half a bar width to center left if requested
  1071. this._draw_label(label_center - (this.center_labels_over_point ? this._bar_width / 2.0 : 0.0), point_index);
  1072. }, this);
  1073. }, this);
  1074. // Draw the last label if requested
  1075. if (this.center_labels_over_point) this._draw_label(this._graph_right, this._column_count);
  1076. }
  1077. });
  1078. // Here's how to make a Line graph:
  1079. //
  1080. // g = new Bluff.Line('canvasId');
  1081. // g.title = "A Line Graph";
  1082. // g.data('Fries', [20, 23, 19, 8]);
  1083. // g.data('Hamburgers', [50, 19, 99, 29]);
  1084. // g.draw();
  1085. //
  1086. // There are also other options described below, such as #baseline_value, #baseline_color, #hide_dots, and #hide_lines.
  1087. Bluff.Line = new JS.Class(Bluff.Base, {
  1088. // Draw a dashed line at the given value
  1089. baseline_value: null,
  1090. // Color of the baseline
  1091. baseline_color: null,
  1092. // Dimensions of lines and dots; calculated based on dataset size if left unspecified
  1093. line_width: null,
  1094. dot_radius: null,
  1095. // Hide parts of the graph to fit more datapoints, or for a different appearance.
  1096. hide_dots: null,
  1097. hide_lines: null,
  1098. // Call with target pixel width of graph (800, 400, 300), and/or 'false' to omit lines (points only).
  1099. //
  1100. // g = new Bluff.Line('canvasId', 400) // 400px wide with lines
  1101. //
  1102. // g = new Bluff.Line('canvasId', 400, false) // 400px wide, no lines (for backwards compatibility)
  1103. //
  1104. // g = new Bluff.Line('canvasId', false) // Defaults to 800px wide, no lines (for backwards compatibility)
  1105. //
  1106. // The preferred way is to call hide_dots or hide_lines instead.
  1107. initialize: function(renderer) {
  1108. if (arguments.length > 3) throw 'Wrong number of arguments';
  1109. if (arguments.length === 1 || (typeof arguments[1] !== 'number' && typeof arguments[1] !== 'string'))
  1110. this.callSuper(renderer, null);
  1111. else
  1112. this.callSuper();
  1113. this.hide_dots = this.hide_lines = false;
  1114. this.baseline_color = 'red';
  1115. this.baseline_value = null;
  1116. },
  1117. draw: function() {
  1118. this.callSuper();
  1119. if (!this._has_data) return;
  1120. // Check to see if more than one datapoint was given. NaN can result otherwise.
  1121. this.x_increment = (this._column_count > 1) ? (this._graph_width / (this._column_count - 1)) : this._graph_width;
  1122. var level;
  1123. if (this._norm_baseline !== undefined) {
  1124. level = this._graph_top + (this._graph_height - this._norm_baseline * this._graph_height);
  1125. this._d.push();
  1126. this._d.stroke = this.baseline_color;
  1127. this._d.fill_opacity = 0.0;
  1128. // this._d.stroke_dasharray(10, 20);
  1129. this._d.stroke_width = 3.0;
  1130. this._d.line(this._graph_left, level, this._graph_left + this._graph_width, level);
  1131. this._d.pop();
  1132. }
  1133. Bluff.each(this._norm_data, function(data_row, row_index) {
  1134. var prev_x = null, prev_y = null;
  1135. var raw_data = this._data[row_index][this.klass.DATA_VALUES_INDEX];
  1136. this._one_point = this._contains_one_point_only(data_row);
  1137. Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, index) {
  1138. var new_x = this._graph_left + (this.x_increment * index);
  1139. if (typeof data_point !== 'number') return;
  1140. this._draw_label(new_x, index);
  1141. var new_y = this._graph_top + (this._graph_height - data_point * this._graph_height);
  1142. // Reset each time to avoid thin-line errors
  1143. this._d.stroke = data_row[this.klass.DATA_COLOR_INDEX];
  1144. this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
  1145. this._d.stroke_opacity = 1.0;
  1146. this._d.stroke_width = this.line_width ||
  1147. this._clip_value_if_greater_than(this._columns / (this._norm_data[0][this.klass.DATA_VALUES_INDEX].length * 6), 3.0);
  1148. var circle_radius = this.dot_radius ||
  1149. this._clip_value_if_greater_than(this._columns / (this._norm_data[0][this.klass.DATA_VALUES_INDEX].length * 2), 7.0);
  1150. if (!this.hide_lines && prev_x !== null && prev_y !== null) {
  1151. this._d.line(prev_x, prev_y, new_x, new_y);
  1152. } else if (this._one_point) {
  1153. // Show a circle if there's just one point
  1154. this._d.circle(new_x, new_y, new_x - circle_radius, new_y);
  1155. }
  1156. if (!this.hide_dots) this._d.circle(new_x, new_y, new_x - circle_radius, new_y);
  1157. this._draw_tooltip(new_x - circle_radius, new_y - circle_radius,
  1158. 2 * circle_radius, 2 *circle_radius,
  1159. data_row[this.klass.DATA_LABEL_INDEX],
  1160. data_row[this.klass.DATA_COLOR_INDEX],
  1161. raw_data[index], index);
  1162. prev_x = new_x;
  1163. prev_y = new_y;
  1164. }, this);
  1165. }, this);
  1166. },
  1167. _normalize: function() {
  1168. this.maximum_value = Math.max(this.maximum_value, this.baseline_value);
  1169. this.callSuper();
  1170. if (this.baseline_value !== null) this._norm_baseline = this.baseline_value / this.maximum_value;
  1171. },
  1172. _contains_one_point_only: function(data_row) {
  1173. // Spin through data to determine if there is just one value present.
  1174. var count = 0;
  1175. Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point) {
  1176. if (data_point !== undefined) count += 1;
  1177. });
  1178. return count === 1;
  1179. }
  1180. });
  1181. // Graph with dots and labels along a vertical access
  1182. // see: 'Creating More Effective Graphs' by Robbins
  1183. Bluff.Dot = new JS.Class(Bluff.Base, {
  1184. draw: function() {
  1185. this.has_left_labels = true;
  1186. this.callSuper();
  1187. if (!this._has_data) return;
  1188. // Setup spacing.
  1189. //
  1190. var spacing_factor = 1.0;
  1191. this._items_width = this._graph_height / this._column_count;
  1192. this._item_width = this._items_width * spacing_factor / this._norm_data.length;
  1193. this._d.stroke_opacity = 0.0;
  1194. var height = Bluff.array_new(this._column_count, 0),
  1195. length = Bluff.array_new(this._column_count, this._graph_left),
  1196. padding = (this._items_width * (1 - spacing_factor)) / 2;
  1197. Bluff.each(this._norm_data, function(data_row, row_index) {
  1198. Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, point_index) {
  1199. var x_pos = this._graph_left + (data_point * this._graph_width) - Math.round(this._item_width/6.0);
  1200. var y_pos = this._graph_top + (this._items_width * point_index) + padding + Math.round(this._item_width/2.0);
  1201. if (row_index === 0) {
  1202. this._d.stroke = this.marker_color;
  1203. this._d.stroke_width = 1.0;
  1204. this._d.opacity = 0.1;
  1205. this._d.line(this._graph_left, y_pos, this._graph_left + this._graph_width, y_pos);
  1206. }
  1207. this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
  1208. this._d.stroke = 'transparent';
  1209. this._d.circle(x_pos, y_pos, x_pos + Math.round(this._item_width/3.0), y_pos);
  1210. // Calculate center based on item_width and current row
  1211. var label_center = this._graph_top + (this._items_width * point_index + this._items_width / 2) + padding;
  1212. this._draw_label(label_center, point_index);
  1213. }, this);
  1214. }, this);
  1215. },
  1216. // Instead of base class version, draws vertical background lines and label
  1217. _draw_line_markers: function() {
  1218. if (this.hide_line_markers) return;
  1219. this._d.stroke_antialias = false;
  1220. // Draw horizontal line markers and annotate with numbers
  1221. this._d.stroke_width = 1;
  1222. var number_of_lines = 5;
  1223. // TODO Round maximum marker value to a round number like 100, 0.1, 0.5, etc.
  1224. var increment = this._significant(this.maximum_value / number_of_lines);
  1225. for (var index = 0; index <= number_of_lines; index++) {
  1226. var line_diff = (this._graph_right - this._graph_left) / number_of_lines,
  1227. x = this._graph_right - (line_diff * index) - 1,
  1228. diff = index - number_of_lines,
  1229. marker_label = Math.abs(diff) * increment;
  1230. this._d.stroke = this.marker_color