keigan 0.0.0 → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2990 @@
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
+
15
+ Bluff = {
16
+ // This is the version of Bluff you are using.
17
+ VERSION: '0.3.6',
18
+
19
+ array: function(list) {
20
+ if (list.length === undefined) return [list];
21
+ var ary = [], i = list.length;
22
+ while (i--) ary[i] = list[i];
23
+ return ary;
24
+ },
25
+
26
+ array_new: function(length, filler) {
27
+ var ary = [];
28
+ while (length--) ary.push(filler);
29
+ return ary;
30
+ },
31
+
32
+ each: function(list, block, context) {
33
+ for (var i = 0, n = list.length; i < n; i++) {
34
+ block.call(context || null, list[i], i);
35
+ }
36
+ },
37
+
38
+ index: function(list, needle) {
39
+ for (var i = 0, n = list.length; i < n; i++) {
40
+ if (list[i] === needle) return i;
41
+ }
42
+ return -1;
43
+ },
44
+
45
+ keys: function(object) {
46
+ var ary = [], key;
47
+ for (key in object) ary.push(key);
48
+ return ary;
49
+ },
50
+
51
+ map: function(list, block, context) {
52
+ var results = [];
53
+ this.each(list, function(item) {
54
+ results.push(block.call(context || null, item));
55
+ });
56
+ return results;
57
+ },
58
+
59
+ reverse_each: function(list, block, context) {
60
+ var i = list.length;
61
+ while (i--) block.call(context || null, list[i], i);
62
+ },
63
+
64
+ sum: function(list) {
65
+ var sum = 0, i = list.length;
66
+ while (i--) sum += list[i];
67
+ return sum;
68
+ },
69
+
70
+ Mini: {}
71
+ };
72
+
73
+ Bluff.Base = new JS.Class({
74
+ extend: {
75
+ // Draw extra lines showing where the margins and text centers are
76
+ DEBUG: false,
77
+
78
+ // Used for navigating the array of data to plot
79
+ DATA_LABEL_INDEX: 0,
80
+ DATA_VALUES_INDEX: 1,
81
+ DATA_COLOR_INDEX: 2,
82
+
83
+ // Space around text elements. Mostly used for vertical spacing
84
+ LEGEND_MARGIN: 20,
85
+ TITLE_MARGIN: 20,
86
+ LABEL_MARGIN: 10,
87
+ DEFAULT_MARGIN: 20,
88
+
89
+ DEFAULT_TARGET_WIDTH: 800,
90
+
91
+ THOUSAND_SEPARATOR: ','
92
+ },
93
+
94
+ // Blank space above the graph
95
+ top_margin: null,
96
+
97
+ // Blank space below the graph
98
+ bottom_margin: null,
99
+
100
+ // Blank space to the right of the graph
101
+ right_margin: null,
102
+
103
+ // Blank space to the left of the graph
104
+ left_margin: null,
105
+
106
+ // Blank space below the title
107
+ title_margin: null,
108
+
109
+ // Blank space below the legend
110
+ legend_margin: null,
111
+
112
+ // A hash of names for the individual columns, where the key is the array
113
+ // index for the column this label represents.
114
+ //
115
+ // Not all columns need to be named.
116
+ //
117
+ // Example: {0: 2005, 3: 2006, 5: 2007, 7: 2008}
118
+ labels: null,
119
+
120
+ // Used internally for spacing.
121
+ //
122
+ // By default, labels are centered over the point they represent.
123
+ center_labels_over_point: null,
124
+
125
+ // Used internally for horizontal graph types.
126
+ has_left_labels: null,
127
+
128
+ // A label for the bottom of the graph
129
+ x_axis_label: null,
130
+
131
+ // A label for the left side of the graph
132
+ y_axis_label: null,
133
+
134
+ // x_axis_increment: null,
135
+
136
+ // Manually set increment of the horizontal marking lines
137
+ y_axis_increment: null,
138
+
139
+ // Get or set the list of colors that will be used to draw the bars or lines.
140
+ colors: null,
141
+
142
+ // The large title of the graph displayed at the top
143
+ title: null,
144
+
145
+ // Font used for titles, labels, etc.
146
+ font: null,
147
+
148
+ font_color: null,
149
+
150
+ // Prevent drawing of line markers
151
+ hide_line_markers: null,
152
+
153
+ // Prevent drawing of the legend
154
+ hide_legend: null,
155
+
156
+ // Prevent drawing of the title
157
+ hide_title: null,
158
+
159
+ // Prevent drawing of line numbers
160
+ hide_line_numbers: null,
161
+
162
+ // Message shown when there is no data. Fits up to 20 characters. Defaults
163
+ // to "No Data."
164
+ no_data_message: null,
165
+
166
+ // The font size of the large title at the top of the graph
167
+ title_font_size: null,
168
+
169
+ // Optionally set the size of the font. Based on an 800x600px graph.
170
+ // Default is 20.
171
+ //
172
+ // Will be scaled down if graph is smaller than 800px wide.
173
+ legend_font_size: null,
174
+
175
+ // The font size of the labels around the graph
176
+ marker_font_size: null,
177
+
178
+ // The color of the auxiliary lines
179
+ marker_color: null,
180
+
181
+ // The number of horizontal lines shown for reference
182
+ marker_count: null,
183
+
184
+ // You can manually set a minimum value instead of having the values
185
+ // guessed for you.
186
+ //
187
+ // Set it after you have given all your data to the graph object.
188
+ minimum_value: null,
189
+
190
+ // You can manually set a maximum value, such as a percentage-based graph
191
+ // that always goes to 100.
192
+ //
193
+ // If you use this, you must set it after you have given all your data to
194
+ // the graph object.
195
+ maximum_value: null,
196
+
197
+ // Set to false if you don't want the data to be sorted with largest avg
198
+ // values at the back.
199
+ sort: null,
200
+
201
+ // Experimental
202
+ additional_line_values: null,
203
+
204
+ // Experimental
205
+ stacked: null,
206
+
207
+ // Optionally set the size of the colored box by each item in the legend.
208
+ // Default is 20.0
209
+ //
210
+ // Will be scaled down if graph is smaller than 800px wide.
211
+ legend_box_size: null,
212
+
213
+ // Set to true to enable tooltip displays
214
+ tooltips: false,
215
+
216
+ // If one numerical argument is given, the graph is drawn at 4/3 ratio
217
+ // according to the given width (800 results in 800x600, 400 gives 400x300,
218
+ // etc.).
219
+ //
220
+ // Or, send a geometry string for other ratios ('800x400', '400x225').
221
+ initialize: function(renderer, target_width) {
222
+ this._d = new Bluff.Renderer(renderer);
223
+ target_width = target_width || this.klass.DEFAULT_TARGET_WIDTH;
224
+
225
+ var geo;
226
+
227
+ if (typeof target_width !== 'number') {
228
+ geo = target_width.split('x');
229
+ this._columns = parseFloat(geo[0]);
230
+ this._rows = parseFloat(geo[1]);
231
+ } else {
232
+ this._columns = parseFloat(target_width);
233
+ this._rows = this._columns * 0.75;
234
+ }
235
+
236
+ this.initialize_ivars();
237
+
238
+ this._reset_themes();
239
+ this.theme_keynote();
240
+
241
+ this._listeners = {};
242
+ },
243
+
244
+ // Set instance variables for this object.
245
+ //
246
+ // Subclasses can override this, call super, then set values separately.
247
+ //
248
+ // This makes it possible to set defaults in a subclass but still allow
249
+ // developers to change this values in their program.
250
+ initialize_ivars: function() {
251
+ // Internal for calculations
252
+ this._raw_columns = 800;
253
+ this._raw_rows = 800 * (this._rows/this._columns);
254
+ this._column_count = 0;
255
+ this.marker_count = null;
256
+ this.maximum_value = this.minimum_value = null;
257
+ this._has_data = false;
258
+ this._data = [];
259
+ this.labels = {};
260
+ this._labels_seen = {};
261
+ this.sort = true;
262
+ this.title = null;
263
+
264
+ this._scale = this._columns / this._raw_columns;
265
+
266
+ this.marker_font_size = 21.0;
267
+ this.legend_font_size = 20.0;
268
+ this.title_font_size = 36.0;
269
+
270
+ this.top_margin = this.bottom_margin =
271
+ this.left_margin = this.right_margin = this.klass.DEFAULT_MARGIN;
272
+
273
+ this.legend_margin = this.klass.LEGEND_MARGIN;
274
+ this.title_margin = this.klass.TITLE_MARGIN;
275
+
276
+ this.legend_box_size = 20.0;
277
+
278
+ this.no_data_message = "No Data";
279
+
280
+ this.hide_line_markers = this.hide_legend = this.hide_title = this.hide_line_numbers = false;
281
+ this.center_labels_over_point = true;
282
+ this.has_left_labels = false;
283
+
284
+ this.additional_line_values = [];
285
+ this._additional_line_colors = [];
286
+ this._theme_options = {};
287
+
288
+ this.x_axis_label = this.y_axis_label = null;
289
+ this.y_axis_increment = null;
290
+ this.stacked = null;
291
+ this._norm_data = null;
292
+ },
293
+
294
+ // Sets the top, bottom, left and right margins to +margin+.
295
+ set_margins: function(margin) {
296
+ this.top_margin = this.left_margin = this.right_margin = this.bottom_margin = margin;
297
+ },
298
+
299
+ // Sets the font for graph text to the font at +font_path+.
300
+ set_font: function(font_path) {
301
+ this.font = font_path;
302
+ this._d.font = this.font;
303
+ },
304
+
305
+ // Add a color to the list of available colors for lines.
306
+ //
307
+ // Example:
308
+ // add_color('#c0e9d3')
309
+ add_color: function(colorname) {
310
+ this.colors.push(colorname);
311
+ },
312
+
313
+ // Replace the entire color list with a new array of colors. Also
314
+ // aliased as the colors= setter method.
315
+ //
316
+ // If you specify fewer colors than the number of datasets you intend
317
+ // to draw, 'increment_color' will cycle through the array, reusing
318
+ // colors as needed.
319
+ //
320
+ // Note that (as with the 'set_theme' method), you should set up the color
321
+ // list before you send your data (via the 'data' method). Calls to the
322
+ // 'data' method made prior to this call will use whatever color scheme
323
+ // was in place at the time data was called.
324
+ //
325
+ // Example:
326
+ // replace_colors ['#cc99cc', '#d9e043', '#34d8a2']
327
+ replace_colors: function(color_list) {
328
+ this.colors = color_list || [];
329
+ this._color_index = 0;
330
+ },
331
+
332
+ // You can set a theme manually. Assign a hash to this method before you
333
+ // send your data.
334
+ //
335
+ // graph.set_theme({
336
+ // colors: ['orange', 'purple', 'green', 'white', 'red'],
337
+ // marker_color: 'blue',
338
+ // background_colors: ['black', 'grey']
339
+ // })
340
+ //
341
+ // background_image: 'squirrel.png' is also possible.
342
+ //
343
+ // (Or hopefully something better looking than that.)
344
+ //
345
+ set_theme: function(options) {
346
+ this._reset_themes();
347
+
348
+ this._theme_options = {
349
+ colors: ['black', 'white'],
350
+ additional_line_colors: [],
351
+ marker_color: 'white',
352
+ font_color: 'black',
353
+ background_colors: null,
354
+ background_image: null
355
+ };
356
+ for (var key in options) this._theme_options[key] = options[key];
357
+
358
+ this.colors = this._theme_options.colors;
359
+ this.marker_color = this._theme_options.marker_color;
360
+ this.font_color = this._theme_options.font_color || this.marker_color;
361
+ this._additional_line_colors = this._theme_options.additional_line_colors;
362
+
363
+ this._render_background();
364
+ },
365
+
366
+ // Set just the background colors
367
+ set_background: function(options) {
368
+ if (options.colors)
369
+ this._theme_options.background_colors = options.colors;
370
+ if (options.image)
371
+ this._theme_options.background_image = options.image;
372
+ this._render_background();
373
+ },
374
+
375
+ // A color scheme similar to the popular presentation software.
376
+ theme_keynote: function() {
377
+ // Colors
378
+ this._blue = '#6886B4';
379
+ this._yellow = '#FDD84E';
380
+ this._green = '#72AE6E';
381
+ this._red = '#D1695E';
382
+ this._purple = '#8A6EAF';
383
+ this._orange = '#EFAA43';
384
+ this._white = 'white';
385
+ this.colors = [this._yellow, this._blue, this._green, this._red, this._purple, this._orange, this._white];
386
+
387
+ this.set_theme({
388
+ colors: this.colors,
389
+ marker_color: 'white',
390
+ font_color: 'white',
391
+ background_colors: ['black', '#4a465a']
392
+ });
393
+ },
394
+
395
+ // A color scheme plucked from the colors on the popular usability blog.
396
+ theme_37signals: function() {
397
+ // Colors
398
+ this._green = '#339933';
399
+ this._purple = '#cc99cc';
400
+ this._blue = '#336699';
401
+ this._yellow = '#FFF804';
402
+ this._red = '#ff0000';
403
+ this._orange = '#cf5910';
404
+ this._black = 'black';
405
+ this.colors = [this._yellow, this._blue, this._green, this._red, this._purple, this._orange, this._black];
406
+
407
+ this.set_theme({
408
+ colors: this.colors,
409
+ marker_color: 'black',
410
+ font_color: 'black',
411
+ background_colors: ['#d1edf5', 'white']
412
+ });
413
+ },
414
+
415
+ // A color scheme from the colors used on the 2005 Rails keynote
416
+ // presentation at RubyConf.
417
+ theme_rails_keynote: function() {
418
+ // Colors
419
+ this._green = '#00ff00';
420
+ this._grey = '#333333';
421
+ this._orange = '#ff5d00';
422
+ this._red = '#f61100';
423
+ this._white = 'white';
424
+ this._light_grey = '#999999';
425
+ this._black = 'black';
426
+ this.colors = [this._green, this._grey, this._orange, this._red, this._white, this._light_grey, this._black];
427
+
428
+ this.set_theme({
429
+ colors: this.colors,
430
+ marker_color: 'white',
431
+ font_color: 'white',
432
+ background_colors: ['#0083a3', '#0083a3']
433
+ });
434
+ },
435
+
436
+ // A color scheme similar to that used on the popular podcast site.
437
+ theme_odeo: function() {
438
+ // Colors
439
+ this._grey = '#202020';
440
+ this._white = 'white';
441
+ this._dark_pink = '#a21764';
442
+ this._green = '#8ab438';
443
+ this._light_grey = '#999999';
444
+ this._dark_blue = '#3a5b87';
445
+ this._black = 'black';
446
+ this.colors = [this._grey, this._white, this._dark_blue, this._dark_pink, this._green, this._light_grey, this._black];
447
+
448
+ this.set_theme({
449
+ colors: this.colors,
450
+ marker_color: 'white',
451
+ font_color: 'white',
452
+ background_colors: ['#ff47a4', '#ff1f81']
453
+ });
454
+ },
455
+
456
+ // A pastel theme
457
+ theme_pastel: function() {
458
+ // Colors
459
+ this.colors = [
460
+ '#a9dada', // blue
461
+ '#aedaa9', // green
462
+ '#daaea9', // peach
463
+ '#dadaa9', // yellow
464
+ '#a9a9da', // dk purple
465
+ '#daaeda', // purple
466
+ '#dadada' // grey
467
+ ];
468
+
469
+ this.set_theme({
470
+ colors: this.colors,
471
+ marker_color: '#aea9a9', // Grey
472
+ font_color: 'black',
473
+ background_colors: 'white'
474
+ });
475
+ },
476
+
477
+ // A greyscale theme
478
+ theme_greyscale: function() {
479
+ // Colors
480
+ this.colors = [
481
+ '#282828', //
482
+ '#383838', //
483
+ '#686868', //
484
+ '#989898', //
485
+ '#c8c8c8', //
486
+ '#e8e8e8' //
487
+ ];
488
+
489
+ this.set_theme({
490
+ colors: this.colors,
491
+ marker_color: '#aea9a9', // Grey
492
+ font_color: 'black',
493
+ background_colors: 'white'
494
+ });
495
+ },
496
+
497
+ // Parameters are an array where the first element is the name of the dataset
498
+ // and the value is an array of values to plot.
499
+ //
500
+ // Can be called multiple times with different datasets for a multi-valued
501
+ // graph.
502
+ //
503
+ // If the color argument is nil, the next color from the default theme will
504
+ // be used.
505
+ //
506
+ // NOTE: If you want to use a preset theme, you must set it before calling
507
+ // data().
508
+ //
509
+ // Example:
510
+ // data("Bart S.", [95, 45, 78, 89, 88, 76], '#ffcc00')
511
+ data: function(name, data_points, color) {
512
+ data_points = (data_points === undefined) ? [] : data_points;
513
+ color = color || null;
514
+
515
+ data_points = Bluff.array(data_points); // make sure it's an array
516
+ this._data.push([name, data_points, (color || this._increment_color())]);
517
+ // Set column count if this is larger than previous counts
518
+ this._column_count = (data_points.length > this._column_count) ? data_points.length : this._column_count;
519
+
520
+ // Pre-normalize
521
+ Bluff.each(data_points, function(data_point, index) {
522
+ if (data_point === undefined) return;
523
+
524
+ // Setup max/min so spread starts at the low end of the data points
525
+ if (this.maximum_value === null && this.minimum_value === null)
526
+ this.maximum_value = this.minimum_value = data_point;
527
+
528
+ // TODO Doesn't work with stacked bar graphs
529
+ // Original: @maximum_value = _larger_than_max?(data_point, index) ? max(data_point, index) : @maximum_value
530
+ this.maximum_value = this._larger_than_max(data_point) ? data_point : this.maximum_value;
531
+ if (this.maximum_value >= 0) this._has_data = true;
532
+
533
+ this.minimum_value = this._less_than_min(data_point) ? data_point : this.minimum_value;
534
+ if (this.minimum_value < 0) this._has_data = true;
535
+ }, this);
536
+ },
537
+
538
+ // Overridden by subclasses to do the actual plotting of the graph.
539
+ //
540
+ // Subclasses should start by calling super() for this method.
541
+ draw: function() {
542
+ if (this.stacked) this._make_stacked();
543
+ this._setup_drawing();
544
+
545
+ this._debug(function() {
546
+ // Outer margin
547
+ this._d.rectangle(this.left_margin, this.top_margin,
548
+ this._raw_columns - this.right_margin, this._raw_rows - this.bottom_margin);
549
+ // Graph area box
550
+ this._d.rectangle(this._graph_left, this._graph_top, this._graph_right, this._graph_bottom);
551
+ });
552
+ },
553
+
554
+ clear: function() {
555
+ this._render_background();
556
+ },
557
+
558
+ on: function(eventType, callback, context) {
559
+ var list = this._listeners[eventType] = this._listeners[eventType] || [];
560
+ list.push([callback, context]);
561
+ },
562
+
563
+ trigger: function(eventType, data) {
564
+ var list = this._listeners[eventType];
565
+ if (!list) return;
566
+ Bluff.each(list, function(listener) {
567
+ listener[0].call(listener[1], data);
568
+ });
569
+ },
570
+
571
+ // Calculates size of drawable area and draws the decorations.
572
+ //
573
+ // * line markers
574
+ // * legend
575
+ // * title
576
+ _setup_drawing: function() {
577
+ // Maybe should be done in one of the following functions for more granularity.
578
+ if (!this._has_data) return this._draw_no_data();
579
+
580
+ this._normalize();
581
+ this._setup_graph_measurements();
582
+ if (this.sort) this._sort_norm_data();
583
+
584
+ this._draw_legend();
585
+ this._draw_line_markers();
586
+ this._draw_axis_labels();
587
+ this._draw_title();
588
+ },
589
+
590
+ // Make copy of data with values scaled between 0-100
591
+ _normalize: function(force) {
592
+ if (this._norm_data === null || force === true) {
593
+ this._norm_data = [];
594
+ if (!this._has_data) return;
595
+
596
+ this._calculate_spread();
597
+
598
+ Bluff.each(this._data, function(data_row) {
599
+ var norm_data_points = [];
600
+ Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point) {
601
+ if (data_point === null || data_point === undefined)
602
+ norm_data_points.push(null);
603
+ else
604
+ norm_data_points.push((data_point - this.minimum_value) / this._spread);
605
+ }, this);
606
+ this._norm_data.push([data_row[this.klass.DATA_LABEL_INDEX], norm_data_points, data_row[this.klass.DATA_COLOR_INDEX]]);
607
+ }, this);
608
+ }
609
+ },
610
+
611
+ _calculate_spread: function() {
612
+ this._spread = this.maximum_value - this.minimum_value;
613
+ this._spread = this._spread > 0 ? this._spread : 1;
614
+
615
+ var power = Math.round(Math.LOG10E*Math.log(this._spread));
616
+ this._significant_digits = Math.pow(10, 3 - power);
617
+ },
618
+
619
+ // Calculates size of drawable area, general font dimensions, etc.
620
+ _setup_graph_measurements: function() {
621
+ this._marker_caps_height = this.hide_line_markers ? 0 :
622
+ this._calculate_caps_height(this.marker_font_size);
623
+ this._title_caps_height = this.hide_title ? 0 :
624
+ this._calculate_caps_height(this.title_font_size);
625
+ this._legend_caps_height = this.hide_legend ? 0 :
626
+ this._calculate_caps_height(this.legend_font_size);
627
+
628
+ var longest_label,
629
+ longest_left_label_width,
630
+ line_number_width,
631
+ last_label,
632
+ extra_room_for_long_label,
633
+ x_axis_label_height,
634
+ key;
635
+
636
+ if (this.hide_line_markers) {
637
+ this._graph_left = this.left_margin;
638
+ this._graph_right_margin = this.right_margin;
639
+ this._graph_bottom_margin = this.bottom_margin;
640
+ } else {
641
+ longest_left_label_width = 0;
642
+ if (this.has_left_labels) {
643
+ longest_label = '';
644
+ for (key in this.labels) {
645
+ longest_label = longest_label.length > this.labels[key].length
646
+ ? longest_label
647
+ : this.labels[key];
648
+ }
649
+ longest_left_label_width = this._calculate_width(this.marker_font_size, longest_label) * 1.25;
650
+ } else {
651
+ longest_left_label_width = this._calculate_width(this.marker_font_size, this._label(this.maximum_value));
652
+ }
653
+
654
+ // Shift graph if left line numbers are hidden
655
+ line_number_width = this.hide_line_numbers && !this.has_left_labels ?
656
+ 0.0 :
657
+ longest_left_label_width + this.klass.LABEL_MARGIN * 2;
658
+
659
+ this._graph_left = this.left_margin +
660
+ line_number_width +
661
+ (this.y_axis_label === null ? 0.0 : this._marker_caps_height + this.klass.LABEL_MARGIN * 2);
662
+
663
+ // Make space for half the width of the rightmost column label.
664
+ // Might be greater than the number of columns if between-style bar markers are used.
665
+ last_label = -Infinity;
666
+ for (key in this.labels)
667
+ last_label = last_label > Number(key) ? last_label : Number(key);
668
+ last_label = Math.round(last_label);
669
+ extra_room_for_long_label = (last_label >= (this._column_count-1) && this.center_labels_over_point) ?
670
+ this._calculate_width(this.marker_font_size, this.labels[last_label]) / 2 :
671
+ 0;
672
+ this._graph_right_margin = this.right_margin + extra_room_for_long_label;
673
+
674
+ this._graph_bottom_margin = this.bottom_margin +
675
+ this._marker_caps_height + this.klass.LABEL_MARGIN;
676
+ }
677
+
678
+ this._graph_right = this._raw_columns - this._graph_right_margin;
679
+ this._graph_width = this._raw_columns - this._graph_left - this._graph_right_margin;
680
+
681
+ // When hide_title, leave a title_margin space for aesthetics.
682
+ // Same with hide_legend
683
+ this._graph_top = this.top_margin +
684
+ (this.hide_title ? this.title_margin : this._title_caps_height + this.title_margin ) +
685
+ (this.hide_legend ? this.legend_margin : this._legend_caps_height + this.legend_margin);
686
+
687
+ x_axis_label_height = (this.x_axis_label === null) ? 0.0 :
688
+ this._marker_caps_height + this.klass.LABEL_MARGIN;
689
+ this._graph_bottom = this._raw_rows - this._graph_bottom_margin - x_axis_label_height;
690
+ this._graph_height = this._graph_bottom - this._graph_top;
691
+ },
692
+
693
+ // Draw the optional labels for the x axis and y axis.
694
+ _draw_axis_labels: function() {
695
+ if (this.x_axis_label) {
696
+ // X Axis
697
+ // Centered vertically and horizontally by setting the
698
+ // height to 1.0 and the width to the width of the graph.
699
+ var x_axis_label_y_coordinate = this._graph_bottom + this.klass.LABEL_MARGIN * 2 + this._marker_caps_height;
700
+
701
+ // TODO Center between graph area
702
+ this._d.fill = this.font_color;
703
+ if (this.font) this._d.font = this.font;
704
+ this._d.stroke = 'transparent';
705
+ this._d.pointsize = this._scale_fontsize(this.marker_font_size);
706
+ this._d.gravity = 'north';
707
+ this._d.annotate_scaled(
708
+ this._raw_columns, 1.0,
709
+ 0.0, x_axis_label_y_coordinate,
710
+ this.x_axis_label, this._scale);
711
+ this._debug(function() {
712
+ this._d.line(0.0, x_axis_label_y_coordinate, this._raw_columns, x_axis_label_y_coordinate);
713
+ });
714
+ }
715
+
716
+ // TODO Y label (not generally possible in browsers)
717
+ },
718
+
719
+ // Draws horizontal background lines and labels
720
+ _draw_line_markers: function() {
721
+ if (this.hide_line_markers) return;
722
+
723
+ if (this.y_axis_increment === null) {
724
+ // Try to use a number of horizontal lines that will come out even.
725
+ //
726
+ // TODO Do the same for larger numbers...100, 75, 50, 25
727
+ if (this.marker_count === null) {
728
+ Bluff.each([3,4,5,6,7], function(lines) {
729
+ if (!this.marker_count && this._spread % lines === 0)
730
+ this.marker_count = lines;
731
+ }, this);
732
+ this.marker_count = this.marker_count || 4;
733
+ }
734
+ this._increment = (this._spread > 0) ? this._significant(this._spread / this.marker_count) : 1;
735
+ } else {
736
+ // TODO Make this work for negative values
737
+ this.maximum_value = Math.max(Math.ceil(this.maximum_value), this.y_axis_increment);
738
+ this.minimum_value = Math.floor(this.minimum_value);
739
+ this._calculate_spread();
740
+ this._normalize(true);
741
+
742
+ this.marker_count = Math.round(this._spread / this.y_axis_increment);
743
+ this._increment = this.y_axis_increment;
744
+ }
745
+ this._increment_scaled = this._graph_height / (this._spread / this._increment);
746
+
747
+ // Draw horizontal line markers and annotate with numbers
748
+ var index, n, y, marker_label;
749
+ for (index = 0, n = this.marker_count; index <= n; index++) {
750
+ y = this._graph_top + this._graph_height - index * this._increment_scaled;
751
+
752
+ this._d.stroke = this.marker_color;
753
+ this._d.stroke_width = 1;
754
+ this._d.line(this._graph_left, y, this._graph_right, y);
755
+
756
+ marker_label = index * this._increment + this.minimum_value;
757
+
758
+ if (!this.hide_line_numbers) {
759
+ this._d.fill = this.font_color;
760
+ if (this.font) this._d.font = this.font;
761
+ this._d.font_weight = 'normal';
762
+ this._d.stroke = 'transparent';
763
+ this._d.pointsize = this._scale_fontsize(this.marker_font_size);
764
+ this._d.gravity = 'east';
765
+
766
+ // Vertically center with 1.0 for the height
767
+ this._d.annotate_scaled(this._graph_left - this.klass.LABEL_MARGIN,
768
+ 1.0, 0.0, y,
769
+ this._label(marker_label), this._scale);
770
+ }
771
+ }
772
+ },
773
+
774
+ _center: function(size) {
775
+ return (this._raw_columns - size) / 2;
776
+ },
777
+
778
+ // Draws a legend with the names of the datasets matched to the colors used
779
+ // to draw them.
780
+ _draw_legend: function() {
781
+ if (this.hide_legend) return;
782
+
783
+ this._legend_labels = Bluff.map(this._data, function(item) {
784
+ return item[this.klass.DATA_LABEL_INDEX];
785
+ }, this);
786
+
787
+ var legend_square_width = this.legend_box_size; // small square with color of this item
788
+
789
+ // May fix legend drawing problem at small sizes
790
+ if (this.font) this._d.font = this.font;
791
+ this._d.pointsize = this.legend_font_size;
792
+
793
+ var label_widths = [[]]; // Used to calculate line wrap
794
+ Bluff.each(this._legend_labels, function(label) {
795
+ var last = label_widths.length - 1;
796
+ var metrics = this._d.get_type_metrics(label);
797
+ var label_width = metrics.width + legend_square_width * 2.7;
798
+ label_widths[last].push(label_width);
799
+
800
+ if (Bluff.sum(label_widths[last]) > (this._raw_columns * 0.9))
801
+ label_widths.push([label_widths[last].pop()]);
802
+ }, this);
803
+
804
+ var current_x_offset = this._center(Bluff.sum(label_widths[0]));
805
+ var current_y_offset = this.hide_title ?
806
+ this.top_margin + this.title_margin :
807
+ this.top_margin + this.title_margin + this._title_caps_height;
808
+
809
+ this._debug(function() {
810
+ this._d.stroke_width = 1;
811
+ this._d.line(0, current_y_offset, this._raw_columns, current_y_offset);
812
+ });
813
+
814
+ Bluff.each(this._legend_labels, function(legend_label, index) {
815
+
816
+ // Draw label
817
+ this._d.fill = this.font_color;
818
+ if (this.font) this._d.font = this.font;
819
+ this._d.pointsize = this._scale_fontsize(this.legend_font_size);
820
+ this._d.stroke = 'transparent';
821
+ this._d.font_weight = 'normal';
822
+ this._d.gravity = 'west';
823
+ this._d.annotate_scaled(this._raw_columns, 1.0,
824
+ current_x_offset + (legend_square_width * 1.7), current_y_offset,
825
+ legend_label, this._scale);
826
+
827
+ // Now draw box with color of this dataset
828
+ this._d.stroke = 'transparent';
829
+ this._d.fill = this._data[index][this.klass.DATA_COLOR_INDEX];
830
+ this._d.rectangle(current_x_offset,
831
+ current_y_offset - legend_square_width / 2.0,
832
+ current_x_offset + legend_square_width,
833
+ current_y_offset + legend_square_width / 2.0);
834
+
835
+ this._d.pointsize = this.legend_font_size;
836
+ var metrics = this._d.get_type_metrics(legend_label);
837
+ var current_string_offset = metrics.width + (legend_square_width * 2.7),
838
+ line_height;
839
+
840
+ // Handle wrapping
841
+ label_widths[0].shift();
842
+ if (label_widths[0].length == 0) {
843
+ this._debug(function() {
844
+ this._d.line(0.0, current_y_offset, this._raw_columns, current_y_offset);
845
+ });
846
+
847
+ label_widths.shift();
848
+ if (label_widths.length > 0) current_x_offset = this._center(Bluff.sum(label_widths[0]));
849
+ line_height = Math.max(this._legend_caps_height, legend_square_width) + this.legend_margin;
850
+ if (label_widths.length > 0) {
851
+ // Wrap to next line and shrink available graph dimensions
852
+ current_y_offset += line_height;
853
+ this._graph_top += line_height;
854
+ this._graph_height = this._graph_bottom - this._graph_top;
855
+ }
856
+ } else {
857
+ current_x_offset += current_string_offset;
858
+ }
859
+ }, this);
860
+ this._color_index = 0;
861
+ },
862
+
863
+ // Draws a title on the graph.
864
+ _draw_title: function() {
865
+ if (this.hide_title || !this.title) return;
866
+
867
+ this._d.fill = this.font_color;
868
+ if (this.font) this._d.font = this.font;
869
+ this._d.pointsize = this._scale_fontsize(this.title_font_size);
870
+ this._d.font_weight = 'bold';
871
+ this._d.gravity = 'north';
872
+ this._d.annotate_scaled(this._raw_columns, 1.0,
873
+ 0, this.top_margin,
874
+ this.title, this._scale);
875
+ },
876
+
877
+ // Draws column labels below graph, centered over x_offset
878
+ //--
879
+ // TODO Allow WestGravity as an option
880
+ _draw_label: function(x_offset, index) {
881
+ if (this.hide_line_markers) return;
882
+
883
+ var y_offset;
884
+
885
+ if (this.labels[index] && !this._labels_seen[index]) {
886
+ y_offset = this._graph_bottom + this.klass.LABEL_MARGIN;
887
+
888
+ this._d.fill = this.font_color;
889
+ if (this.font) this._d.font = this.font;
890
+ this._d.stroke = 'transparent';
891
+ this._d.font_weight = 'normal';
892
+ this._d.pointsize = this._scale_fontsize(this.marker_font_size);
893
+ this._d.gravity = 'north';
894
+ this._d.annotate_scaled(1.0, 1.0,
895
+ x_offset, y_offset,
896
+ this.labels[index], this._scale);
897
+ this._labels_seen[index] = true;
898
+
899
+ this._debug(function() {
900
+ this._d.stroke_width = 1;
901
+ this._d.line(0.0, y_offset, this._raw_columns, y_offset);
902
+ });
903
+ }
904
+ },
905
+
906
+ // Creates a mouse hover target rectangle for tooltip displays
907
+ _draw_tooltip: function(left, top, width, height, name, color, data, index) {
908
+ if (!this.tooltips) return;
909
+ var node = this._d.tooltip(left, top, width, height, name, color, data);
910
+
911
+ Bluff.Event.observe(node, 'click', function() {
912
+ var point = {
913
+ series: name,
914
+ label: this.labels[index],
915
+ value: data,
916
+ color: color
917
+ };
918
+ this.trigger('click:datapoint', point);
919
+ }, this);
920
+ },
921
+
922
+ // Shows an error message because you have no data.
923
+ _draw_no_data: function() {
924
+ this._d.fill = this.font_color;
925
+ if (this.font) this._d.font = this.font;
926
+ this._d.stroke = 'transparent';
927
+ this._d.font_weight = 'normal';
928
+ this._d.pointsize = this._scale_fontsize(80);
929
+ this._d.gravity = 'center';
930
+ this._d.annotate_scaled(this._raw_columns, this._raw_rows/2,
931
+ 0, 10,
932
+ this.no_data_message, this._scale);
933
+ },
934
+
935
+ // Finds the best background to render based on the provided theme options.
936
+ _render_background: function() {
937
+ var colors = this._theme_options.background_colors;
938
+ switch (true) {
939
+ case colors instanceof Array:
940
+ this._render_gradiated_background.apply(this, colors);
941
+ break;
942
+ case typeof colors === 'string':
943
+ this._render_solid_background(colors);
944
+ break;
945
+ default:
946
+ this._render_image_background(this._theme_options.background_image);
947
+ break;
948
+ }
949
+ },
950
+
951
+ // Make a new image at the current size with a solid +color+.
952
+ _render_solid_background: function(color) {
953
+ this._d.render_solid_background(this._columns, this._rows, color);
954
+ },
955
+
956
+ // Use with a theme definition method to draw a gradiated background.
957
+ _render_gradiated_background: function(top_color, bottom_color) {
958
+ this._d.render_gradiated_background(this._columns, this._rows, top_color, bottom_color);
959
+ },
960
+
961
+ // Use with a theme to use an image (800x600 original) background.
962
+ _render_image_background: function(image_path) {
963
+ // TODO
964
+ },
965
+
966
+ // Resets everything to defaults (except data).
967
+ _reset_themes: function() {
968
+ this._color_index = 0;
969
+ this._labels_seen = {};
970
+ this._theme_options = {};
971
+ this._d.scale(this._scale, this._scale);
972
+ },
973
+
974
+ _scale_value: function(value) {
975
+ return this._scale * value;
976
+ },
977
+
978
+ // Return a comparable fontsize for the current graph.
979
+ _scale_fontsize: function(value) {
980
+ var new_fontsize = value * this._scale;
981
+ return new_fontsize;
982
+ },
983
+
984
+ _clip_value_if_greater_than: function(value, max_value) {
985
+ return (value > max_value) ? max_value : value;
986
+ },
987
+
988
+ // Overridden by subclasses such as stacked bar.
989
+ _larger_than_max: function(data_point, index) {
990
+ return data_point > this.maximum_value;
991
+ },
992
+
993
+ _less_than_min: function(data_point, index) {
994
+ return data_point < this.minimum_value;
995
+ },
996
+
997
+ // Overridden by subclasses that need it.
998
+ _max: function(data_point, index) {
999
+ return data_point;
1000
+ },
1001
+
1002
+ // Overridden by subclasses that need it.
1003
+ _min: function(data_point, index) {
1004
+ return data_point;
1005
+ },
1006
+
1007
+ _significant: function(inc) {
1008
+ if (inc == 0) return 1.0;
1009
+ var factor = 1.0;
1010
+ while (inc < 10) {
1011
+ inc *= 10;
1012
+ factor /= 10;
1013
+ }
1014
+
1015
+ while (inc > 100) {
1016
+ inc /= 10;
1017
+ factor *= 10;
1018
+ }
1019
+
1020
+ return Math.floor(inc) * factor;
1021
+ },
1022
+
1023
+ // Sort with largest overall summed value at front of array so it shows up
1024
+ // correctly in the drawn graph.
1025
+ _sort_norm_data: function() {
1026
+ var sums = this._sums, index = this.klass.DATA_VALUES_INDEX;
1027
+
1028
+ this._norm_data.sort(function(a,b) {
1029
+ return sums(b[index]) - sums(a[index]);
1030
+ });
1031
+
1032
+ this._data.sort(function(a,b) {
1033
+ return sums(b[index]) - sums(a[index]);
1034
+ });
1035
+ },
1036
+
1037
+ _sums: function(data_set) {
1038
+ var total_sum = 0;
1039
+ Bluff.each(data_set, function(num) { total_sum += (num || 0) });
1040
+ return total_sum;
1041
+ },
1042
+
1043
+ _make_stacked: function() {
1044
+ var stacked_values = [], i = this._column_count;
1045
+ while (i--) stacked_values[i] = 0;
1046
+ Bluff.each(this._data, function(value_set) {
1047
+ Bluff.each(value_set[this.klass.DATA_VALUES_INDEX], function(value, index) {
1048
+ stacked_values[index] += value;
1049
+ }, this);
1050
+ value_set[this.klass.DATA_VALUES_INDEX] = Bluff.array(stacked_values);
1051
+ }, this);
1052
+ },
1053
+
1054
+ // Takes a block and draws it if DEBUG is true.
1055
+ //
1056
+ // Example:
1057
+ // debug { @d.rectangle x1, y1, x2, y2 }
1058
+ _debug: function(block) {
1059
+ if (this.klass.DEBUG) {
1060
+ this._d.fill = 'transparent';
1061
+ this._d.stroke = 'turquoise';
1062
+ block.call(this);
1063
+ }
1064
+ },
1065
+
1066
+ // Returns the next color in your color list.
1067
+ _increment_color: function() {
1068
+ var offset = this._color_index;
1069
+ this._color_index = (this._color_index + 1) % this.colors.length;
1070
+ return this.colors[offset];
1071
+ },
1072
+
1073
+ // Return a formatted string representing a number value that should be
1074
+ // printed as a label.
1075
+ _label: function(value) {
1076
+ var sep = this.klass.THOUSAND_SEPARATOR,
1077
+ label = (this._spread % this.marker_count == 0 || this.y_axis_increment !== null)
1078
+ ? String(Math.round(value))
1079
+ : String(Math.floor(value * this._significant_digits)/this._significant_digits);
1080
+
1081
+ var parts = label.split('.');
1082
+ parts[0] = parts[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1' + sep);
1083
+ return parts.join('.');
1084
+ },
1085
+
1086
+ // Returns the height of the capital letter 'X' for the current font and
1087
+ // size.
1088
+ //
1089
+ // Not scaled since it deals with dimensions that the regular scaling will
1090
+ // handle.
1091
+ _calculate_caps_height: function(font_size) {
1092
+ return this._d.caps_height(font_size);
1093
+ },
1094
+
1095
+ // Returns the width of a string at this pointsize.
1096
+ //
1097
+ // Not scaled since it deals with dimensions that the regular
1098
+ // scaling will handle.
1099
+ _calculate_width: function(font_size, text) {
1100
+ return this._d.text_width(font_size, text);
1101
+ }
1102
+ });
1103
+
1104
+
1105
+ Bluff.Area = new JS.Class(Bluff.Base, {
1106
+
1107
+ draw: function() {
1108
+ this.callSuper();
1109
+
1110
+ if (!this._has_data) return;
1111
+
1112
+ this._x_increment = this._graph_width / (this._column_count - 1);
1113
+ this._d.stroke = 'transparent';
1114
+
1115
+ Bluff.each(this._norm_data, function(data_row) {
1116
+ var poly_points = [],
1117
+ prev_x = 0.0,
1118
+ prev_y = 0.0;
1119
+
1120
+ Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, index) {
1121
+ // Use incremented x and scaled y
1122
+ var new_x = this._graph_left + (this._x_increment * index);
1123
+ var new_y = this._graph_top + (this._graph_height - data_point * this._graph_height);
1124
+
1125
+ if (prev_x > 0 && prev_y > 0) {
1126
+ poly_points.push(new_x);
1127
+ poly_points.push(new_y);
1128
+
1129
+ // this._d.polyline(prev_x, prev_y, new_x, new_y);
1130
+ } else {
1131
+ poly_points.push(this._graph_left);
1132
+ poly_points.push(this._graph_bottom - 1);
1133
+ poly_points.push(new_x);
1134
+ poly_points.push(new_y);
1135
+
1136
+ // this._d.polyline(this._graph_left, this._graph_bottom, new_x, new_y);
1137
+ }
1138
+
1139
+ this._draw_label(new_x, index);
1140
+
1141
+ prev_x = new_x;
1142
+ prev_y = new_y;
1143
+ }, this);
1144
+
1145
+ // Add closing points, draw polygon
1146
+ poly_points.push(this._graph_right);
1147
+ poly_points.push(this._graph_bottom - 1);
1148
+ poly_points.push(this._graph_left);
1149
+ poly_points.push(this._graph_bottom - 1);
1150
+
1151
+ this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
1152
+ this._d.polyline(poly_points);
1153
+
1154
+ }, this);
1155
+ }
1156
+ });
1157
+
1158
+
1159
+ // This class perfoms the y coordinats conversion for the bar class.
1160
+ //
1161
+ // There are three cases:
1162
+ //
1163
+ // 1. Bars all go from zero in positive direction
1164
+ // 2. Bars all go from zero to negative direction
1165
+ // 3. Bars either go from zero to positive or from zero to negative
1166
+ //
1167
+ Bluff.BarConversion = new JS.Class({
1168
+ mode: null,
1169
+ zero: null,
1170
+ graph_top: null,
1171
+ graph_height: null,
1172
+ minimum_value: null,
1173
+ spread: null,
1174
+
1175
+ getLeftYRightYscaled: function(data_point, result) {
1176
+ var val;
1177
+ switch (this.mode) {
1178
+ case 1: // Case one
1179
+ // minimum value >= 0 ( only positiv values )
1180
+ result[0] = this.graph_top + this.graph_height*(1 - data_point) + 1;
1181
+ result[1] = this.graph_top + this.graph_height - 1;
1182
+ break;
1183
+ case 2: // Case two
1184
+ // only negativ values
1185
+ result[0] = this.graph_top + 1;
1186
+ result[1] = this.graph_top + this.graph_height*(1 - data_point) - 1;
1187
+ break;
1188
+ case 3: // Case three
1189
+ // positiv and negativ values
1190
+ val = data_point-this.minimum_value/this.spread;
1191
+ if ( data_point >= this.zero ) {
1192
+ result[0] = this.graph_top + this.graph_height*(1 - (val-this.zero)) + 1;
1193
+ result[1] = this.graph_top + this.graph_height*(1 - this.zero) - 1;
1194
+ } else {
1195
+ result[0] = this.graph_top + this.graph_height*(1 - (val-this.zero)) + 1;
1196
+ result[1] = this.graph_top + this.graph_height*(1 - this.zero) - 1;
1197
+ }
1198
+ break;
1199
+ default:
1200
+ result[0] = 0.0;
1201
+ result[1] = 0.0;
1202
+ }
1203
+ }
1204
+
1205
+ });
1206
+
1207
+
1208
+ Bluff.Bar = new JS.Class(Bluff.Base, {
1209
+
1210
+ // Spacing factor applied between bars
1211
+ bar_spacing: 0.9,
1212
+
1213
+ draw: function() {
1214
+ // Labels will be centered over the left of the bar if
1215
+ // there are more labels than columns. This is basically the same
1216
+ // as where it would be for a line graph.
1217
+ this.center_labels_over_point = (Bluff.keys(this.labels).length > this._column_count);
1218
+
1219
+ this.callSuper();
1220
+ if (!this._has_data) return;
1221
+
1222
+ this._draw_bars();
1223
+ },
1224
+
1225
+ _draw_bars: function() {
1226
+ this._bar_width = this._graph_width / (this._column_count * this._data.length);
1227
+ var padding = (this._bar_width * (1 - this.bar_spacing)) / 2;
1228
+
1229
+ this._d.stroke_opacity = 0.0;
1230
+
1231
+ // Setup the BarConversion Object
1232
+ var conversion = new Bluff.BarConversion();
1233
+ conversion.graph_height = this._graph_height;
1234
+ conversion.graph_top = this._graph_top;
1235
+
1236
+ // Set up the right mode [1,2,3] see BarConversion for further explanation
1237
+ if (this.minimum_value >= 0) {
1238
+ // all bars go from zero to positiv
1239
+ conversion.mode = 1;
1240
+ } else {
1241
+ // all bars go from 0 to negativ
1242
+ if (this.maximum_value <= 0) {
1243
+ conversion.mode = 2;
1244
+ } else {
1245
+ // bars either go from zero to negativ or to positiv
1246
+ conversion.mode = 3;
1247
+ conversion.spread = this._spread;
1248
+ conversion.minimum_value = this.minimum_value;
1249
+ conversion.zero = -this.minimum_value/this._spread;
1250
+ }
1251
+ }
1252
+
1253
+ // iterate over all normalised data
1254
+ Bluff.each(this._norm_data, function(data_row, row_index) {
1255
+ var raw_data = this._data[row_index][this.klass.DATA_VALUES_INDEX];
1256
+
1257
+ Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, point_index) {
1258
+ // Use incremented x and scaled y
1259
+ // x
1260
+ var left_x = this._graph_left + (this._bar_width * (row_index + point_index + ((this._data.length - 1) * point_index))) + padding;
1261
+ var right_x = left_x + this._bar_width * this.bar_spacing;
1262
+ // y
1263
+ var conv = [];
1264
+ conversion.getLeftYRightYscaled(data_point, conv);
1265
+
1266
+ // create new bar
1267
+ this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
1268
+ this._d.rectangle(left_x, conv[0], right_x, conv[1]);
1269
+
1270
+ // create tooltip target
1271
+ this._draw_tooltip(left_x, conv[0],
1272
+ right_x - left_x, conv[1] - conv[0],
1273
+ data_row[this.klass.DATA_LABEL_INDEX],
1274
+ data_row[this.klass.DATA_COLOR_INDEX],
1275
+ raw_data[point_index], point_index);
1276
+
1277
+ // Calculate center based on bar_width and current row
1278
+ var label_center = this._graph_left +
1279
+ (this._data.length * this._bar_width * point_index) +
1280
+ (this._data.length * this._bar_width / 2.0);
1281
+ // Subtract half a bar width to center left if requested
1282
+ this._draw_label(label_center - (this.center_labels_over_point ? this._bar_width / 2.0 : 0.0), point_index);
1283
+ }, this);
1284
+
1285
+ }, this);
1286
+
1287
+ // Draw the last label if requested
1288
+ if (this.center_labels_over_point) this._draw_label(this._graph_right, this._column_count);
1289
+ }
1290
+ });
1291
+
1292
+
1293
+ // Here's how to make a Line graph:
1294
+ //
1295
+ // g = new Bluff.Line('canvasId');
1296
+ // g.title = "A Line Graph";
1297
+ // g.data('Fries', [20, 23, 19, 8]);
1298
+ // g.data('Hamburgers', [50, 19, 99, 29]);
1299
+ // g.draw();
1300
+ //
1301
+ // There are also other options described below, such as #baseline_value, #baseline_color, #hide_dots, and #hide_lines.
1302
+
1303
+ Bluff.Line = new JS.Class(Bluff.Base, {
1304
+ // Draw a dashed line at the given value
1305
+ baseline_value: null,
1306
+
1307
+ // Color of the baseline
1308
+ baseline_color: null,
1309
+
1310
+ // Dimensions of lines and dots; calculated based on dataset size if left unspecified
1311
+ line_width: null,
1312
+ dot_radius: null,
1313
+
1314
+ // Hide parts of the graph to fit more datapoints, or for a different appearance.
1315
+ hide_dots: null,
1316
+ hide_lines: null,
1317
+
1318
+ // Call with target pixel width of graph (800, 400, 300), and/or 'false' to omit lines (points only).
1319
+ //
1320
+ // g = new Bluff.Line('canvasId', 400) // 400px wide with lines
1321
+ //
1322
+ // g = new Bluff.Line('canvasId', 400, false) // 400px wide, no lines (for backwards compatibility)
1323
+ //
1324
+ // g = new Bluff.Line('canvasId', false) // Defaults to 800px wide, no lines (for backwards compatibility)
1325
+ //
1326
+ // The preferred way is to call hide_dots or hide_lines instead.
1327
+ initialize: function(renderer) {
1328
+ if (arguments.length > 3) throw 'Wrong number of arguments';
1329
+ if (arguments.length === 1 || (typeof arguments[1] !== 'number' && typeof arguments[1] !== 'string'))
1330
+ this.callSuper(renderer, null);
1331
+ else
1332
+ this.callSuper();
1333
+
1334
+ this.hide_dots = this.hide_lines = false;
1335
+ this.baseline_color = 'red';
1336
+ this.baseline_value = null;
1337
+ },
1338
+
1339
+ draw: function() {
1340
+ this.callSuper();
1341
+
1342
+ if (!this._has_data) return;
1343
+
1344
+ // Check to see if more than one datapoint was given. NaN can result otherwise.
1345
+ this.x_increment = (this._column_count > 1) ? (this._graph_width / (this._column_count - 1)) : this._graph_width;
1346
+
1347
+ var level;
1348
+
1349
+ if (this._norm_baseline !== undefined) {
1350
+ level = this._graph_top + (this._graph_height - this._norm_baseline * this._graph_height);
1351
+ this._d.push();
1352
+ this._d.stroke = this.baseline_color;
1353
+ this._d.fill_opacity = 0.0;
1354
+ // this._d.stroke_dasharray(10, 20);
1355
+ this._d.stroke_width = 3.0;
1356
+ this._d.line(this._graph_left, level, this._graph_left + this._graph_width, level);
1357
+ this._d.pop();
1358
+ }
1359
+
1360
+ Bluff.each(this._norm_data, function(data_row, row_index) {
1361
+ var prev_x = null, prev_y = null;
1362
+ var raw_data = this._data[row_index][this.klass.DATA_VALUES_INDEX];
1363
+
1364
+ this._one_point = this._contains_one_point_only(data_row);
1365
+
1366
+ Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, index) {
1367
+ var new_x = this._graph_left + (this.x_increment * index);
1368
+ if (typeof data_point !== 'number') return;
1369
+
1370
+ this._draw_label(new_x, index);
1371
+
1372
+ var new_y = this._graph_top + (this._graph_height - data_point * this._graph_height);
1373
+
1374
+ // Reset each time to avoid thin-line errors
1375
+ this._d.stroke = data_row[this.klass.DATA_COLOR_INDEX];
1376
+ this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
1377
+ this._d.stroke_opacity = 1.0;
1378
+ this._d.stroke_width = this.line_width ||
1379
+ this._clip_value_if_greater_than(this._columns / (this._norm_data[0][this.klass.DATA_VALUES_INDEX].length * 6), 3.0);
1380
+
1381
+ var circle_radius = this.dot_radius ||
1382
+ this._clip_value_if_greater_than(this._columns / (this._norm_data[0][this.klass.DATA_VALUES_INDEX].length * 2), 7.0);
1383
+
1384
+ if (!this.hide_lines && prev_x !== null && prev_y !== null) {
1385
+ this._d.line(prev_x, prev_y, new_x, new_y);
1386
+ } else if (this._one_point) {
1387
+ // Show a circle if there's just one point
1388
+ this._d.circle(new_x, new_y, new_x - circle_radius, new_y);
1389
+ }
1390
+
1391
+ if (!this.hide_dots) this._d.circle(new_x, new_y, new_x - circle_radius, new_y);
1392
+
1393
+ this._draw_tooltip(new_x - circle_radius, new_y - circle_radius,
1394
+ 2 * circle_radius, 2 *circle_radius,
1395
+ data_row[this.klass.DATA_LABEL_INDEX],
1396
+ data_row[this.klass.DATA_COLOR_INDEX],
1397
+ raw_data[index], index);
1398
+
1399
+ prev_x = new_x;
1400
+ prev_y = new_y;
1401
+ }, this);
1402
+ }, this);
1403
+ },
1404
+
1405
+ _normalize: function() {
1406
+ this.maximum_value = Math.max(this.maximum_value, this.baseline_value);
1407
+ this.callSuper();
1408
+ if (this.baseline_value !== null) this._norm_baseline = this.baseline_value / this.maximum_value;
1409
+ },
1410
+
1411
+ _contains_one_point_only: function(data_row) {
1412
+ // Spin through data to determine if there is just one value present.
1413
+ var count = 0;
1414
+ Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point) {
1415
+ if (data_point !== undefined) count += 1;
1416
+ });
1417
+ return count === 1;
1418
+ }
1419
+ });
1420
+
1421
+
1422
+ // Graph with dots and labels along a vertical access
1423
+ // see: 'Creating More Effective Graphs' by Robbins
1424
+
1425
+ Bluff.Dot = new JS.Class(Bluff.Base, {
1426
+
1427
+ draw: function() {
1428
+ this.has_left_labels = true;
1429
+ this.callSuper();
1430
+
1431
+ if (!this._has_data) return;
1432
+
1433
+ // Setup spacing.
1434
+ //
1435
+ var spacing_factor = 1.0;
1436
+
1437
+ this._items_width = this._graph_height / this._column_count;
1438
+ this._item_width = this._items_width * spacing_factor / this._norm_data.length;
1439
+ this._d.stroke_opacity = 0.0;
1440
+ var height = Bluff.array_new(this._column_count, 0),
1441
+ length = Bluff.array_new(this._column_count, this._graph_left),
1442
+ padding = (this._items_width * (1 - spacing_factor)) / 2;
1443
+
1444
+ Bluff.each(this._norm_data, function(data_row, row_index) {
1445
+ Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, point_index) {
1446
+
1447
+ var x_pos = this._graph_left + (data_point * this._graph_width) - Math.round(this._item_width/6.0);
1448
+ var y_pos = this._graph_top + (this._items_width * point_index) + padding + Math.round(this._item_width/2.0);
1449
+
1450
+ if (row_index === 0) {
1451
+ this._d.stroke = this.marker_color;
1452
+ this._d.stroke_width = 1.0;
1453
+ this._d.opacity = 0.1;
1454
+ this._d.line(this._graph_left, y_pos, this._graph_left + this._graph_width, y_pos);
1455
+ }
1456
+
1457
+ this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
1458
+ this._d.stroke = 'transparent';
1459
+ this._d.circle(x_pos, y_pos, x_pos + Math.round(this._item_width/3.0), y_pos);
1460
+
1461
+ // Calculate center based on item_width and current row
1462
+ var label_center = this._graph_top + (this._items_width * point_index + this._items_width / 2) + padding;
1463
+ this._draw_label(label_center, point_index);
1464
+ }, this);
1465
+
1466
+ }, this);
1467
+ },
1468
+
1469
+ // Instead of base class version, draws vertical background lines and label
1470
+ _draw_line_markers: function() {
1471
+
1472
+ if (this.hide_line_markers) return;
1473
+
1474
+ this._d.stroke_antialias = false;
1475
+
1476
+ // Draw horizontal line markers and annotate with numbers
1477
+ this._d.stroke_width = 1;
1478
+ var number_of_lines = 5;
1479
+
1480
+ // TODO Round maximum marker value to a round number like 100, 0.1, 0.5, etc.
1481
+ var increment = this._significant(this.maximum_value / number_of_lines);
1482
+ for (var index = 0; index <= number_of_lines; index++) {
1483
+
1484
+ var line_diff = (this._graph_right - this._graph_left) / number_of_lines,
1485
+ x = this._graph_right - (line_diff * index) - 1,
1486
+ diff = index - number_of_lines,
1487
+ marker_label = Math.abs(diff) * increment;
1488
+
1489
+ this._d.stroke = this.marker_color;
1490
+ this._d.line(x, this._graph_bottom, x, this._graph_bottom + 0.5 * this.klass.LABEL_MARGIN);
1491
+
1492
+ if (!this.hide_line_numbers) {
1493
+ this._d.fill = this.font_color;
1494
+ if (this.font) this._d.font = this.font;
1495
+ this._d.stroke = 'transparent';
1496
+ this._d.pointsize = this._scale_fontsize(this.marker_font_size);
1497
+ this._d.gravity = 'center';
1498
+ // TODO Center text over line
1499
+ this._d.annotate_scaled(0, 0, // Width of box to draw text in
1500
+ x, this._graph_bottom + (this.klass.LABEL_MARGIN * 2.0), // Coordinates of text
1501
+ marker_label, this._scale);
1502
+ }
1503
+ this._d.stroke_antialias = true;
1504
+ }
1505
+ },
1506
+
1507
+ // Draw on the Y axis instead of the X
1508
+ _draw_label: function(y_offset, index) {
1509
+ if (this.labels[index] && !this._labels_seen[index]) {
1510
+ this._d.fill = this.font_color;
1511
+ if (this.font) this._d.font = this.font;
1512
+ this._d.stroke = 'transparent';
1513
+ this._d.font_weight = 'normal';
1514
+ this._d.pointsize = this._scale_fontsize(this.marker_font_size);
1515
+ this._d.gravity = 'east';
1516
+ this._d.annotate_scaled(1, 1,
1517
+ this._graph_left - this.klass.LABEL_MARGIN * 2.0, y_offset,
1518
+ this.labels[index], this._scale);
1519
+ this._labels_seen[index] = true;
1520
+ }
1521
+ }
1522
+ });
1523
+
1524
+
1525
+ // Experimental!!! See also the Spider graph.
1526
+ Bluff.Net = new JS.Class(Bluff.Base, {
1527
+
1528
+ // Hide parts of the graph to fit more datapoints, or for a different appearance.
1529
+ hide_dots: null,
1530
+
1531
+ //Dimensions of lines and dots; calculated based on dataset size if left unspecified
1532
+ line_width: null,
1533
+ dot_radius: null,
1534
+
1535
+ initialize: function() {
1536
+ this.callSuper();
1537
+
1538
+ this.hide_dots = false;
1539
+ this.hide_line_numbers = true;
1540
+ },
1541
+
1542
+ draw: function() {
1543
+
1544
+ this.callSuper();
1545
+
1546
+ if (!this._has_data) return;
1547
+
1548
+ this._radius = this._graph_height / 2.0;
1549
+ this._center_x = this._graph_left + (this._graph_width / 2.0);
1550
+ this._center_y = this._graph_top + (this._graph_height / 2.0) - 10; // Move graph up a bit
1551
+
1552
+ this._x_increment = this._graph_width / (this._column_count - 1);
1553
+ var circle_radius = this.dot_radius ||
1554
+ this._clip_value_if_greater_than(this._columns / (this._norm_data[0][this.klass.DATA_VALUES_INDEX].length * 2.5), 7.0);
1555
+
1556
+ this._d.stroke_opacity = 1.0;
1557
+ this._d.stroke_width = this.line_width ||
1558
+ this._clip_value_if_greater_than(this._columns / (this._norm_data[0][this.klass.DATA_VALUES_INDEX].length * 4), 3.0);
1559
+
1560
+ var level;
1561
+
1562
+ if (this._norm_baseline !== undefined) {
1563
+ level = this._graph_top + (this._graph_height - this._norm_baseline * this._graph_height);
1564
+ this._d.push();
1565
+ this._d.stroke_color = this.baseline_color;
1566
+ this._d.fill_opacity = 0.0;
1567
+ // this._d.stroke_dasharray(10, 20);
1568
+ this._d.stroke_width = 5;
1569
+ this._d.line(this._graph_left, level, this._graph_left + this._graph_width, level);
1570
+ this._d.pop();
1571
+ }
1572
+
1573
+ Bluff.each(this._norm_data, function(data_row) {
1574
+ var prev_x = null, prev_y = null;
1575
+
1576
+ Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, index) {
1577
+ if (data_point === undefined) return;
1578
+
1579
+ var rad_pos = index * Math.PI * 2 / this._column_count,
1580
+ point_distance = data_point * this._radius,
1581
+ start_x = this._center_x + Math.sin(rad_pos) * point_distance,
1582
+ start_y = this._center_y - Math.cos(rad_pos) * point_distance,
1583
+
1584
+ next_index = (index + 1 < data_row[this.klass.DATA_VALUES_INDEX].length) ? index + 1 : 0,
1585
+
1586
+ next_rad_pos = next_index * Math.PI * 2 / this._column_count,
1587
+ next_point_distance = data_row[this.klass.DATA_VALUES_INDEX][next_index] * this._radius,
1588
+ end_x = this._center_x + Math.sin(next_rad_pos) * next_point_distance,
1589
+ end_y = this._center_y - Math.cos(next_rad_pos) * next_point_distance;
1590
+
1591
+ this._d.stroke = data_row[this.klass.DATA_COLOR_INDEX];
1592
+ this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
1593
+ this._d.line(start_x, start_y, end_x, end_y);
1594
+
1595
+ if (!this.hide_dots) this._d.circle(start_x, start_y, start_x - circle_radius, start_y);
1596
+ }, this);
1597
+
1598
+ }, this);
1599
+ },
1600
+
1601
+ // the lines connecting in the center, with the first line vertical
1602
+ _draw_line_markers: function() {
1603
+ if (this.hide_line_markers) return;
1604
+
1605
+ // have to do this here (AGAIN)... see draw() in this class
1606
+ // because this funtion is called before the @radius, @center_x and @center_y are set
1607
+ this._radius = this._graph_height / 2.0;
1608
+ this._center_x = this._graph_left + (this._graph_width / 2.0);
1609
+ this._center_y = this._graph_top + (this._graph_height / 2.0) - 10; // Move graph up a bit
1610
+
1611
+ var rad_pos, marker_label;
1612
+
1613
+ for (var index = 0, n = this._column_count; index < n; index++) {
1614
+ rad_pos = index * Math.PI * 2 / this._column_count;
1615
+
1616
+ // Draw horizontal line markers and annotate with numbers
1617
+ this._d.stroke = this.marker_color;
1618
+ this._d.stroke_width = 1;
1619
+
1620
+ this._d.line(this._center_x, this._center_y, this._center_x + Math.sin(rad_pos) * this._radius, this._center_y - Math.cos(rad_pos) * this._radius);
1621
+
1622
+ marker_label = this.labels[index] ? this.labels[index] : '000';
1623
+
1624
+ this._draw_label(this._center_x, this._center_y, rad_pos * 360 / (2 * Math.PI), this._radius, marker_label);
1625
+ }
1626
+ },
1627
+
1628
+ _draw_label: function(center_x, center_y, angle, radius, amount) {
1629
+ var r_offset = 1.1,
1630
+ x_offset = center_x, // + 15 // The label points need to be tweaked slightly
1631
+ y_offset = center_y, // + 0 // This one doesn't though
1632
+ rad_pos = angle * Math.PI / 180,
1633
+ x = x_offset + (radius * r_offset * Math.sin(rad_pos)),
1634
+ y = y_offset - (radius * r_offset * Math.cos(rad_pos));
1635
+
1636
+ // Draw label
1637
+ this._d.fill = this.marker_color;
1638
+ if (this.font) this._d.font = this.font;
1639
+ this._d.pointsize = this._scale_fontsize(20);
1640
+ this._d.stroke = 'transparent';
1641
+ this._d.font_weight = 'bold';
1642
+ this._d.gravity = 'center';
1643
+ this._d.annotate_scaled(0, 0, x, y, amount, this._scale);
1644
+ }
1645
+ });
1646
+
1647
+
1648
+ // Here's how to make a Pie graph:
1649
+ //
1650
+ // g = new Bluff.Pie('canvasId');
1651
+ // g.title = "Visual Pie Graph Test";
1652
+ // g.data('Fries', 20);
1653
+ // g.data('Hamburgers', 50);
1654
+ // g.draw();
1655
+ //
1656
+ // To control where the pie chart starts creating slices, use #zero_degree.
1657
+
1658
+ Bluff.Pie = new JS.Class(Bluff.Base, {
1659
+ extend: {
1660
+ TEXT_OFFSET_PERCENTAGE: 0.08
1661
+ },
1662
+
1663
+ // Can be used to make the pie start cutting slices at the top (-90.0)
1664
+ // or at another angle. Default is 0.0, which starts at 3 o'clock.
1665
+ zero_degreee: null,
1666
+
1667
+ // Do not show labels for slices that are less than this percent. Use 0 to always show all labels.
1668
+ hide_labels_less_than: null,
1669
+
1670
+ initialize_ivars: function() {
1671
+ this.callSuper();
1672
+ this.zero_degree = 0.0;
1673
+ this.hide_labels_less_than = 0.0;
1674
+ },
1675
+
1676
+ draw: function() {
1677
+ this.hide_line_markers = true;
1678
+
1679
+ this.callSuper();
1680
+
1681
+ if (!this._has_data) return;
1682
+
1683
+ var diameter = this._graph_height,
1684
+ radius = (Math.min(this._graph_width, this._graph_height) / 2.0) * 0.8,
1685
+ top_x = this._graph_left + (this._graph_width - diameter) / 2.0,
1686
+ center_x = this._graph_left + (this._graph_width / 2.0),
1687
+ center_y = this._graph_top + (this._graph_height / 2.0) - 10, // Move graph up a bit
1688
+ total_sum = this._sums_for_pie(),
1689
+ prev_degrees = this.zero_degree,
1690
+ index = this.klass.DATA_VALUES_INDEX;
1691
+
1692
+ // Use full data since we can easily calculate percentages
1693
+ if (this.sort) this._data.sort(function(a,b) { return a[index][0] - b[index][0]; });
1694
+ Bluff.each(this._data, function(data_row, i) {
1695
+ if (data_row[this.klass.DATA_VALUES_INDEX][0] > 0) {
1696
+ this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
1697
+
1698
+ var current_degrees = (data_row[this.klass.DATA_VALUES_INDEX][0] / total_sum) * 360;
1699
+
1700
+ // Gruff uses ellipse() here, but canvas doesn't seem to support it.
1701
+ // circle() is fine for our purposes here.
1702
+ this._d.circle(center_x, center_y,
1703
+ center_x + radius, center_y,
1704
+ prev_degrees, prev_degrees + current_degrees + 0.5); // <= +0.5 'fudge factor' gets rid of the ugly gaps
1705
+
1706
+ var half_angle = prev_degrees + ((prev_degrees + current_degrees) - prev_degrees) / 2,
1707
+ label_val = Math.round((data_row[this.klass.DATA_VALUES_INDEX][0] / total_sum) * 100.0),
1708
+ label_string;
1709
+
1710
+ if (label_val >= this.hide_labels_less_than) {
1711
+ label_string = this._label(data_row[this.klass.DATA_VALUES_INDEX][0]);
1712
+ this._draw_label(center_x, center_y, half_angle,
1713
+ radius + (radius * this.klass.TEXT_OFFSET_PERCENTAGE),
1714
+ label_string,
1715
+ data_row, i);
1716
+ }
1717
+
1718
+ prev_degrees += current_degrees;
1719
+ }
1720
+ }, this);
1721
+
1722
+ // TODO debug a circle where the text is drawn...
1723
+ },
1724
+
1725
+ // Labels are drawn around a slightly wider ellipse to give room for
1726
+ // labels on the left and right.
1727
+ _draw_label: function(center_x, center_y, angle, radius, amount, data_row, i) {
1728
+ // TODO Don't use so many hard-coded numbers
1729
+ var r_offset = 20.0, // The distance out from the center of the pie to get point
1730
+ x_offset = center_x, // + 15.0 # The label points need to be tweaked slightly
1731
+ y_offset = center_y, // This one doesn't though
1732
+ radius_offset = radius + r_offset,
1733
+ ellipse_factor = radius_offset * 0.15,
1734
+ x = x_offset + ((radius_offset + ellipse_factor) * Math.cos(angle * Math.PI/180)),
1735
+ y = y_offset + (radius_offset * Math.sin(angle * Math.PI/180));
1736
+
1737
+ // Draw label
1738
+ this._d.fill = this.font_color;
1739
+ if (this.font) this._d.font = this.font;
1740
+ this._d.pointsize = this._scale_fontsize(this.marker_font_size);
1741
+ this._d.font_weight = 'bold';
1742
+ this._d.gravity = 'center';
1743
+ this._d.annotate_scaled(0,0, x,y, amount, this._scale);
1744
+
1745
+ this._draw_tooltip(x - 20, y - 20, 40, 40,
1746
+ data_row[this.klass.DATA_LABEL_INDEX],
1747
+ data_row[this.klass.DATA_COLOR_INDEX],
1748
+ amount, i);
1749
+ },
1750
+
1751
+ _sums_for_pie: function() {
1752
+ var total_sum = 0;
1753
+ Bluff.each(this._data, function(data_row) {
1754
+ total_sum += data_row[this.klass.DATA_VALUES_INDEX][0];
1755
+ }, this);
1756
+ return total_sum;
1757
+ }
1758
+ });
1759
+
1760
+
1761
+ // Graph with individual horizontal bars instead of vertical bars.
1762
+
1763
+ Bluff.SideBar = new JS.Class(Bluff.Base, {
1764
+
1765
+ // Spacing factor applied between bars
1766
+ bar_spacing: 0.9,
1767
+
1768
+ draw: function() {
1769
+ this.has_left_labels = true;
1770
+ this.callSuper();
1771
+
1772
+ if (!this._has_data) return;
1773
+ this._draw_bars();
1774
+ },
1775
+
1776
+ _draw_bars: function() {
1777
+ this._bars_width = this._graph_height / this._column_count;
1778
+ this._bar_width = this._bars_width / this._norm_data.length;
1779
+ this._d.stroke_opacity = 0.0;
1780
+ var height = Bluff.array_new(this._column_count, 0),
1781
+ length = Bluff.array_new(this._column_count, this._graph_left),
1782
+ padding = (this._bar_width * (1 - this.bar_spacing)) / 2;
1783
+
1784
+ Bluff.each(this._norm_data, function(data_row, row_index) {
1785
+ var raw_data = this._data[row_index][this.klass.DATA_VALUES_INDEX];
1786
+ Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, point_index) {
1787
+
1788
+ // Using the original calcs from the stacked bar chart
1789
+ // to get the difference between
1790
+ // part of the bart chart we wish to stack.
1791
+ var temp1 = this._graph_left + (this._graph_width - data_point * this._graph_width - height[point_index]),
1792
+ temp2 = this._graph_left + this._graph_width - height[point_index],
1793
+ difference = temp2 - temp1,
1794
+
1795
+ left_x = length[point_index] - 1,
1796
+ left_y = this._graph_top + (this._bars_width * point_index) + (this._bar_width * row_index) + padding,
1797
+ right_x = left_x + difference,
1798
+ right_y = left_y + this._bar_width * this.bar_spacing;
1799
+
1800
+ height[point_index] += (data_point * this._graph_width);
1801
+
1802
+ this._d.stroke = 'transparent';
1803
+ this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
1804
+ this._d.rectangle(left_x, left_y, right_x, right_y);
1805
+
1806
+ this._draw_tooltip(left_x, left_y,
1807
+ right_x - left_x, right_y - left_y,
1808
+ data_row[this.klass.DATA_LABEL_INDEX],
1809
+ data_row[this.klass.DATA_COLOR_INDEX],
1810
+ raw_data[point_index], point_index);
1811
+
1812
+ // Calculate center based on bar_width and current row
1813
+ var label_center = this._graph_top + (this._bars_width * point_index + this._bars_width / 2);
1814
+ this._draw_label(label_center, point_index);
1815
+ }, this)
1816
+
1817
+ }, this);
1818
+ },
1819
+
1820
+ // Instead of base class version, draws vertical background lines and label
1821
+ _draw_line_markers: function() {
1822
+
1823
+ if (this.hide_line_markers) return;
1824
+
1825
+ this._d.stroke_antialias = false;
1826
+
1827
+ // Draw horizontal line markers and annotate with numbers
1828
+ this._d.stroke_width = 1;
1829
+ var number_of_lines = 5;
1830
+
1831
+ // TODO Round maximum marker value to a round number like 100, 0.1, 0.5, etc.
1832
+ var increment = this._significant(this._spread / number_of_lines),
1833
+ line_diff, x, diff, marker_label;
1834
+ for (var index = 0; index <= number_of_lines; index++) {
1835
+
1836
+ line_diff = (this._graph_right - this._graph_left) / number_of_lines;
1837
+ x = this._graph_right - (line_diff * index) - 1;
1838
+ diff = index - number_of_lines;
1839
+ marker_label = Math.abs(diff) * increment + this.minimum_value;
1840
+
1841
+ this._d.stroke = this.marker_color;
1842
+ this._d.line(x, this._graph_bottom, x, this._graph_top);
1843
+
1844
+ if (!this.hide_line_numbers) {
1845
+ this._d.fill = this.font_color;
1846
+ if (this.font) this._d.font = this.font;
1847
+ this._d.stroke = 'transparent';
1848
+ this._d.pointsize = this._scale_fontsize(this.marker_font_size);
1849
+ this._d.gravity = 'center';
1850
+ // TODO Center text over line
1851
+ this._d.annotate_scaled(
1852
+ 0, 0, // Width of box to draw text in
1853
+ x, this._graph_bottom + (this.klass.LABEL_MARGIN * 2.0), // Coordinates of text
1854
+ this._label(marker_label), this._scale);
1855
+ }
1856
+ }
1857
+ },
1858
+
1859
+ // Draw on the Y axis instead of the X
1860
+ _draw_label: function(y_offset, index) {
1861
+ if (this.labels[index] && !this._labels_seen[index]) {
1862
+ this._d.fill = this.font_color;
1863
+ if (this.font) this._d.font = this.font;
1864
+ this._d.stroke = 'transparent';
1865
+ this._d.font_weight = 'normal';
1866
+ this._d.pointsize = this._scale_fontsize(this.marker_font_size);
1867
+ this._d.gravity = 'east';
1868
+ this._d.annotate_scaled(1, 1,
1869
+ this._graph_left - this.klass.LABEL_MARGIN * 2.0, y_offset,
1870
+ this.labels[index], this._scale);
1871
+ this._labels_seen[index] = true;
1872
+ }
1873
+ }
1874
+ });
1875
+
1876
+
1877
+ // Experimental!!! See also the Net graph.
1878
+ //
1879
+ // Submitted by Kevin Clark http://glu.ttono.us/
1880
+ Bluff.Spider = new JS.Class(Bluff.Base, {
1881
+
1882
+ // Hide all text
1883
+ hide_text: null,
1884
+ hide_axes: null,
1885
+ transparent_background: null,
1886
+
1887
+ initialize: function(renderer, max_value, target_width) {
1888
+ this.callSuper(renderer, target_width);
1889
+ this._max_value = max_value;
1890
+ this.hide_legend = true;
1891
+ },
1892
+
1893
+ draw: function() {
1894
+ this.hide_line_markers = true;
1895
+
1896
+ this.callSuper();
1897
+
1898
+ if (!this._has_data) return;
1899
+
1900
+ // Setup basic positioning
1901
+ var diameter = this._graph_height,
1902
+ radius = this._graph_height / 2.0,
1903
+ top_x = this._graph_left + (this._graph_width - diameter) / 2.0,
1904
+ center_x = this._graph_left + (this._graph_width / 2.0),
1905
+ center_y = this._graph_top + (this._graph_height / 2.0) - 25; // Move graph up a bit
1906
+
1907
+ this._unit_length = radius / this._max_value;
1908
+
1909
+ var total_sum = this._sums_for_spider(),
1910
+ prev_degrees = 0.0,
1911
+ additive_angle = (2 * Math.PI) / this._data.length,
1912
+
1913
+ current_angle = 0.0;
1914
+
1915
+ // Draw axes
1916
+ if (!this.hide_axes) this._draw_axes(center_x, center_y, radius, additive_angle);
1917
+
1918
+ // Draw polygon
1919
+ this._draw_polygon(center_x, center_y, additive_angle);
1920
+ },
1921
+
1922
+ _normalize_points: function(value) {
1923
+ return value * this._unit_length;
1924
+ },
1925
+
1926
+ _draw_label: function(center_x, center_y, angle, radius, amount) {
1927
+ var r_offset = 50, // The distance out from the center of the pie to get point
1928
+ x_offset = center_x, // The label points need to be tweaked slightly
1929
+ y_offset = center_y + 0, // This one doesn't though
1930
+ x = x_offset + ((radius + r_offset) * Math.cos(angle)),
1931
+ y = y_offset + ((radius + r_offset) * Math.sin(angle));
1932
+
1933
+ // Draw label
1934
+ this._d.fill = this.marker_color;
1935
+ if (this.font) this._d.font = this.font;
1936
+ this._d.pointsize = this._scale_fontsize(this.legend_font_size);
1937
+ this._d.stroke = 'transparent';
1938
+ this._d.font_weight = 'bold';
1939
+ this._d.gravity = 'center';
1940
+ this._d.annotate_scaled(0, 0,
1941
+ x, y,
1942
+ amount, this._scale);
1943
+ },
1944
+
1945
+ _draw_axes: function(center_x, center_y, radius, additive_angle, line_color) {
1946
+ if (this.hide_axes) return;
1947
+
1948
+ var current_angle = 0.0;
1949
+
1950
+ Bluff.each(this._data, function(data_row) {
1951
+ this._d.stroke = line_color || data_row[this.klass.DATA_COLOR_INDEX];
1952
+ this._d.stroke_width = 5.0;
1953
+
1954
+ var x_offset = radius * Math.cos(current_angle);
1955
+ var y_offset = radius * Math.sin(current_angle);
1956
+
1957
+ this._d.line(center_x, center_y,
1958
+ center_x + x_offset,
1959
+ center_y + y_offset);
1960
+
1961
+ if (!this.hide_text) this._draw_label(center_x, center_y, current_angle, radius, data_row[this.klass.DATA_LABEL_INDEX]);
1962
+
1963
+ current_angle += additive_angle;
1964
+ }, this);
1965
+ },
1966
+
1967
+ _draw_polygon: function(center_x, center_y, additive_angle, color) {
1968
+ var points = [],
1969
+ current_angle = 0.0;
1970
+ Bluff.each(this._data, function(data_row) {
1971
+ points.push(center_x + this._normalize_points(data_row[this.klass.DATA_VALUES_INDEX][0]) * Math.cos(current_angle));
1972
+ points.push(center_y + this._normalize_points(data_row[this.klass.DATA_VALUES_INDEX][0]) * Math.sin(current_angle));
1973
+ current_angle += additive_angle;
1974
+ }, this);
1975
+
1976
+ this._d.stroke_width = 1.0;
1977
+ this._d.stroke = color || this.marker_color;
1978
+ this._d.fill = color || this.marker_color;
1979
+ this._d.fill_opacity = 0.4;
1980
+ this._d.polyline(points);
1981
+ },
1982
+
1983
+ _sums_for_spider: function() {
1984
+ var sum = 0.0;
1985
+ Bluff.each(this._data, function(data_row) {
1986
+ sum += data_row[this.klass.DATA_VALUES_INDEX][0];
1987
+ }, this);
1988
+ return sum;
1989
+ }
1990
+ });
1991
+
1992
+
1993
+ // Used by StackedBar and child classes.
1994
+ Bluff.Base.StackedMixin = new JS.Module({
1995
+ // Get sum of each stack
1996
+ _get_maximum_by_stack: function() {
1997
+ var max_hash = {};
1998
+ Bluff.each(this._data, function(data_set) {
1999
+ Bluff.each(data_set[this.klass.DATA_VALUES_INDEX], function(data_point, i) {
2000
+ if (!max_hash[i]) max_hash[i] = 0.0;
2001
+ max_hash[i] += data_point;
2002
+ }, this);
2003
+ }, this);
2004
+
2005
+ // this.maximum_value = 0;
2006
+ for (var key in max_hash) {
2007
+ if (max_hash[key] > this.maximum_value) this.maximum_value = max_hash[key];
2008
+ }
2009
+ this.minimum_value = 0;
2010
+ }
2011
+ });
2012
+
2013
+
2014
+ Bluff.StackedArea = new JS.Class(Bluff.Base, {
2015
+ include: Bluff.Base.StackedMixin,
2016
+ last_series_goes_on_bottom: null,
2017
+
2018
+ draw: function() {
2019
+ this._get_maximum_by_stack();
2020
+ this.callSuper();
2021
+
2022
+ if (!this._has_data) return;
2023
+
2024
+ this._x_increment = this._graph_width / (this._column_count - 1);
2025
+ this._d.stroke = 'transparent';
2026
+
2027
+ var height = Bluff.array_new(this._column_count, 0);
2028
+
2029
+ var data_points = null;
2030
+ var iterator = this.last_series_goes_on_bottom ? 'reverse_each' : 'each';
2031
+ Bluff[iterator](this._norm_data, function(data_row) {
2032
+ var prev_data_points = data_points;
2033
+ data_points = [];
2034
+
2035
+ Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, index) {
2036
+ // Use incremented x and scaled y
2037
+ var new_x = this._graph_left + (this._x_increment * index);
2038
+ var new_y = this._graph_top + (this._graph_height - data_point * this._graph_height - height[index]);
2039
+
2040
+ height[index] += (data_point * this._graph_height);
2041
+
2042
+ data_points.push(new_x);
2043
+ data_points.push(new_y);
2044
+
2045
+ this._draw_label(new_x, index);
2046
+ }, this);
2047
+
2048
+ var poly_points, i, n;
2049
+
2050
+ if (prev_data_points) {
2051
+ poly_points = Bluff.array(data_points);
2052
+ for (i = prev_data_points.length/2 - 1; i >= 0; i--) {
2053
+ poly_points.push(prev_data_points[2*i]);
2054
+ poly_points.push(prev_data_points[2*i+1]);
2055
+ }
2056
+ poly_points.push(data_points[0]);
2057
+ poly_points.push(data_points[1]);
2058
+ } else {
2059
+ poly_points = Bluff.array(data_points);
2060
+ poly_points.push(this._graph_right);
2061
+ poly_points.push(this._graph_bottom - 1);
2062
+ poly_points.push(this._graph_left);
2063
+ poly_points.push(this._graph_bottom - 1);
2064
+ poly_points.push(data_points[0]);
2065
+ poly_points.push(data_points[1]);
2066
+ }
2067
+ this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
2068
+ this._d.polyline(poly_points);
2069
+ }, this);
2070
+ }
2071
+ });
2072
+
2073
+
2074
+ Bluff.StackedBar = new JS.Class(Bluff.Base, {
2075
+ include: Bluff.Base.StackedMixin,
2076
+
2077
+ // Spacing factor applied between bars
2078
+ bar_spacing: 0.9,
2079
+
2080
+ // Draws a bar graph, but multiple sets are stacked on top of each other.
2081
+ draw: function() {
2082
+ this._get_maximum_by_stack();
2083
+ this.callSuper();
2084
+ if (!this._has_data) return;
2085
+
2086
+ this._bar_width = this._graph_width / this._column_count;
2087
+ var padding = (this._bar_width * (1 - this.bar_spacing)) / 2;
2088
+
2089
+ this._d.stroke_opacity = 0.0;
2090
+
2091
+ var height = Bluff.array_new(this._column_count, 0);
2092
+
2093
+ Bluff.each(this._norm_data, function(data_row, row_index) {
2094
+ var raw_data = this._data[row_index][this.klass.DATA_VALUES_INDEX];
2095
+
2096
+ Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, point_index) {
2097
+ // Calculate center based on bar_width and current row
2098
+ var label_center = this._graph_left + (this._bar_width * point_index) + (this._bar_width * this.bar_spacing / 2.0);
2099
+ this._draw_label(label_center, point_index);
2100
+
2101
+ if (data_point == 0) return;
2102
+ // Use incremented x and scaled y
2103
+ var left_x = this._graph_left + (this._bar_width * point_index) + padding;
2104
+ var left_y = this._graph_top + (this._graph_height -
2105
+ data_point * this._graph_height -
2106
+ height[point_index]) + 1;
2107
+ var right_x = left_x + this._bar_width * this.bar_spacing;
2108
+ var right_y = this._graph_top + this._graph_height - height[point_index] - 1;
2109
+
2110
+ // update the total height of the current stacked bar
2111
+ height[point_index] += (data_point * this._graph_height);
2112
+
2113
+ this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
2114
+ this._d.rectangle(left_x, left_y, right_x, right_y);
2115
+
2116
+ this._draw_tooltip(left_x, left_y,
2117
+ right_x - left_x, right_y - left_y,
2118
+ data_row[this.klass.DATA_LABEL_INDEX],
2119
+ data_row[this.klass.DATA_COLOR_INDEX],
2120
+ raw_data[point_index], point_index);
2121
+ }, this);
2122
+ }, this);
2123
+ }
2124
+ });
2125
+
2126
+
2127
+ // A special bar graph that shows a single dataset as a set of
2128
+ // stacked bars. The bottom bar shows the running total and
2129
+ // the top bar shows the new value being added to the array.
2130
+
2131
+ Bluff.AccumulatorBar = new JS.Class(Bluff.StackedBar, {
2132
+
2133
+ draw: function() {
2134
+ if (this._data.length !== 1) throw 'Incorrect number of datasets';
2135
+
2136
+ var accumulator_array = [],
2137
+ index = 0,
2138
+ increment_array = [];
2139
+
2140
+ Bluff.each(this._data[0][this.klass.DATA_VALUES_INDEX], function(value) {
2141
+ var max = -Infinity;
2142
+ Bluff.each(increment_array, function(x) { max = Math.max(max, x); });
2143
+
2144
+ increment_array.push((index > 0) ? (value + max) : value);
2145
+ accumulator_array.push(increment_array[index] - value);
2146
+ index += 1;
2147
+ }, this);
2148
+
2149
+ this.data("Accumulator", accumulator_array);
2150
+
2151
+ this.callSuper();
2152
+ }
2153
+ });
2154
+
2155
+
2156
+ // New gruff graph type added to enable sideways stacking bar charts
2157
+ // (basically looks like a x/y flip of a standard stacking bar chart)
2158
+ //
2159
+ // alun.eyre@googlemail.com
2160
+
2161
+ Bluff.SideStackedBar = new JS.Class(Bluff.SideBar, {
2162
+ include: Bluff.Base.StackedMixin,
2163
+
2164
+ // Spacing factor applied between bars
2165
+ bar_spacing: 0.9,
2166
+
2167
+ draw: function() {
2168
+ this.has_left_labels = true;
2169
+ this._get_maximum_by_stack();
2170
+ this.callSuper();
2171
+ },
2172
+
2173
+ _draw_bars: function() {
2174
+ this._bar_width = this._graph_height / this._column_count;
2175
+ var height = Bluff.array_new(this._column_count, 0),
2176
+ length = Bluff.array_new(this._column_count, this._graph_left),
2177
+ padding = (this._bar_width * (1 - this.bar_spacing)) / 2;
2178
+
2179
+ Bluff.each(this._norm_data, function(data_row, row_index) {
2180
+ var raw_data = this._data[row_index][this.klass.DATA_VALUES_INDEX];
2181
+
2182
+ Bluff.each(data_row[this.klass.DATA_VALUES_INDEX], function(data_point, point_index) {
2183
+
2184
+ // using the original calcs from the stacked bar chart to get the difference between
2185
+ // part of the bart chart we wish to stack.
2186
+ var temp1 = this._graph_left + (this._graph_width -
2187
+ data_point * this._graph_width -
2188
+ height[point_index]) + 1;
2189
+ var temp2 = this._graph_left + this._graph_width - height[point_index] - 1;
2190
+ var difference = temp2 - temp1;
2191
+
2192
+ this._d.fill = data_row[this.klass.DATA_COLOR_INDEX];
2193
+
2194
+ var left_x = length[point_index], //+ 1
2195
+ left_y = this._graph_top + (this._bar_width * point_index) + padding,
2196
+ right_x = left_x + difference,
2197
+ right_y = left_y + this._bar_width * this.bar_spacing;
2198
+ length[point_index] += difference;
2199
+ height[point_index] += (data_point * this._graph_width - 2);
2200
+
2201
+ this._d.rectangle(left_x, left_y, right_x, right_y);
2202
+
2203
+ this._draw_tooltip(left_x, left_y,
2204
+ right_x - left_x, right_y - left_y,
2205
+ data_row[this.klass.DATA_LABEL_INDEX],
2206
+ data_row[this.klass.DATA_COLOR_INDEX],
2207
+ raw_data[point_index], point_index);
2208
+
2209
+ // Calculate center based on bar_width and current row
2210
+ var label_center = this._graph_top + (this._bar_width * point_index) + (this._bar_width * this.bar_spacing / 2.0);
2211
+ this._draw_label(label_center, point_index);
2212
+ }, this);
2213
+ }, this);
2214
+ },
2215
+
2216
+ _larger_than_max: function(data_point, index) {
2217
+ index = index || 0;
2218
+ return this._max(data_point, index) > this.maximum_value;
2219
+ },
2220
+
2221
+ _max: function(data_point, index) {
2222
+ var sum = 0;
2223
+ Bluff.each(this._data, function(item) {
2224
+ sum += item[this.klass.DATA_VALUES_INDEX][index];
2225
+ }, this);
2226
+ return sum;
2227
+ }
2228
+ });
2229
+
2230
+
2231
+ Bluff.Mini.Legend = new JS.Module({
2232
+
2233
+ hide_mini_legend: false,
2234
+
2235
+ // The canvas needs to be bigger so we can put the legend beneath it.
2236
+ _expand_canvas_for_vertical_legend: function() {
2237
+ if (this.hide_mini_legend) return;
2238
+
2239
+ this._legend_labels = Bluff.map(this._data, function(item) {
2240
+ return item[this.klass.DATA_LABEL_INDEX];
2241
+ }, this);
2242
+
2243
+ var legend_height = this._scale_fontsize(
2244
+ this._data.length * this._calculate_line_height() +
2245
+ this.top_margin + this.bottom_margin);
2246
+
2247
+ this._original_rows = this._raw_rows;
2248
+ this._original_columns = this._raw_columns;
2249
+
2250
+ switch (this.legend_position) {
2251
+ case 'right':
2252
+ this._rows = Math.max(this._rows, legend_height);
2253
+ this._columns += this._calculate_legend_width() + this.left_margin;
2254
+ break;
2255
+
2256
+ default:
2257
+ this._rows += legend_height;
2258
+ break;
2259
+ }
2260
+ this._render_background();
2261
+ },
2262
+
2263
+ _calculate_line_height: function() {
2264
+ return this._calculate_caps_height(this.legend_font_size) * 1.7;
2265
+ },
2266
+
2267
+ _calculate_legend_width: function() {
2268
+ var width = 0;
2269
+ Bluff.each(this._legend_labels, function(label) {
2270
+ width = Math.max(this._calculate_width(this.legend_font_size, label), width);
2271
+ }, this);
2272
+ return this._scale_fontsize(width + 40*1.7);
2273
+ },
2274
+
2275
+ // Draw the legend beneath the existing graph.
2276
+ _draw_vertical_legend: function() {
2277
+ if (this.hide_mini_legend) return;
2278
+
2279
+ var legend_square_width = 40.0, // small square with color of this item
2280
+ legend_square_margin = 10.0,
2281
+ legend_left_margin = 100.0,
2282
+ legend_top_margin = 40.0;
2283
+
2284
+ // May fix legend drawing problem at small sizes
2285
+ if (this.font) this._d.font = this.font;
2286
+ this._d.pointsize = this.legend_font_size;
2287
+
2288
+ var current_x_offset, current_y_offset;
2289
+
2290
+ switch (this.legend_position) {
2291
+ case 'right':
2292
+ current_x_offset = this._original_columns + this.left_margin;
2293
+ current_y_offset = this.top_margin + legend_top_margin;
2294
+ break;
2295
+
2296
+ default:
2297
+ current_x_offset = legend_left_margin,
2298
+ current_y_offset = this._original_rows + legend_top_margin;
2299
+ break;
2300
+ }
2301
+
2302
+ this._debug(function() {
2303
+ this._d.line(0.0, current_y_offset, this._raw_columns, current_y_offset);
2304
+ });
2305
+
2306
+ Bluff.each(this._legend_labels, function(legend_label, index) {
2307
+
2308
+ // Draw label
2309
+ this._d.fill = this.font_color;
2310
+ if (this.font) this._d.font = this.font;
2311
+ this._d.pointsize = this._scale_fontsize(this.legend_font_size);
2312
+ this._d.stroke = 'transparent';
2313
+ this._d.font_weight = 'normal';
2314
+ this._d.gravity = 'west';
2315
+ this._d.annotate_scaled(this._raw_columns, 1.0,
2316
+ current_x_offset + (legend_square_width * 1.7), current_y_offset,
2317
+ this._truncate_legend_label(legend_label), this._scale);
2318
+
2319
+ // Now draw box with color of this dataset
2320
+ this._d.stroke = 'transparent';
2321
+ this._d.fill = this._data[index][this.klass.DATA_COLOR_INDEX];
2322
+ this._d.rectangle(current_x_offset,
2323
+ current_y_offset - legend_square_width / 2.0,
2324
+ current_x_offset + legend_square_width,
2325
+ current_y_offset + legend_square_width / 2.0);
2326
+
2327
+ current_y_offset += this._calculate_line_height();
2328
+ }, this);
2329
+ this._color_index = 0;
2330
+ },
2331
+
2332
+ // Shorten long labels so they will fit on the canvas.
2333
+ _truncate_legend_label: function(label) {
2334
+ var truncated_label = String(label);
2335
+ while (this._calculate_width(this._scale_fontsize(this.legend_font_size), truncated_label) > (this._columns - this.legend_left_margin - this.right_margin) && (truncated_label.length > 1))
2336
+ truncated_label = truncated_label.substr(0, truncated_label.length-1);
2337
+ return truncated_label + (truncated_label.length < String(label).length ? "..." : '');
2338
+ }
2339
+ });
2340
+
2341
+
2342
+ // Makes a small bar graph suitable for display at 200px or even smaller.
2343
+ //
2344
+ Bluff.Mini.Bar = new JS.Class(Bluff.Bar, {
2345
+ include: Bluff.Mini.Legend,
2346
+
2347
+ initialize_ivars: function() {
2348
+ this.callSuper();
2349
+
2350
+ this.hide_legend = true;
2351
+ this.hide_title = true;
2352
+ this.hide_line_numbers = true;
2353
+
2354
+ this.marker_font_size = 50.0;
2355
+ this.minimum_value = 0.0;
2356
+ this.maximum_value = 0.0;
2357
+ this.legend_font_size = 60.0;
2358
+ },
2359
+
2360
+ draw: function() {
2361
+ this._expand_canvas_for_vertical_legend();
2362
+
2363
+ this.callSuper();
2364
+
2365
+ this._draw_vertical_legend();
2366
+ }
2367
+ });
2368
+
2369
+
2370
+ // Makes a small pie graph suitable for display at 200px or even smaller.
2371
+ //
2372
+ Bluff.Mini.Pie = new JS.Class(Bluff.Pie, {
2373
+ include: Bluff.Mini.Legend,
2374
+
2375
+ initialize_ivars: function() {
2376
+ this.callSuper();
2377
+
2378
+ this.hide_legend = true;
2379
+ this.hide_title = true;
2380
+ this.hide_line_numbers = true;
2381
+
2382
+ this.marker_font_size = 60.0;
2383
+ this.legend_font_size = 60.0;
2384
+ },
2385
+
2386
+ draw: function() {
2387
+ this._expand_canvas_for_vertical_legend();
2388
+
2389
+ this.callSuper();
2390
+
2391
+ this._draw_vertical_legend();
2392
+ }
2393
+ });
2394
+
2395
+
2396
+ // Makes a small pie graph suitable for display at 200px or even smaller.
2397
+ //
2398
+ Bluff.Mini.SideBar = new JS.Class(Bluff.SideBar, {
2399
+ include: Bluff.Mini.Legend,
2400
+
2401
+ initialize_ivars: function() {
2402
+ this.callSuper();
2403
+ this.hide_legend = true;
2404
+ this.hide_title = true;
2405
+ this.hide_line_numbers = true;
2406
+
2407
+ this.marker_font_size = 50.0;
2408
+ this.legend_font_size = 50.0;
2409
+ },
2410
+
2411
+ draw: function() {
2412
+ this._expand_canvas_for_vertical_legend();
2413
+
2414
+ this.callSuper();
2415
+
2416
+ this._draw_vertical_legend();
2417
+ }
2418
+ });
2419
+
2420
+
2421
+ Bluff.Renderer = new JS.Class({
2422
+ extend: {
2423
+ WRAPPER_CLASS: 'bluff-wrapper',
2424
+ TEXT_CLASS: 'bluff-text',
2425
+ TARGET_CLASS: 'bluff-tooltip-target'
2426
+ },
2427
+
2428
+ font: 'Arial, Helvetica, Verdana, sans-serif',
2429
+ gravity: 'north',
2430
+
2431
+ initialize: function(canvasId) {
2432
+ this._canvas = document.getElementById(canvasId);
2433
+ this._ctx = this._canvas.getContext('2d');
2434
+ },
2435
+
2436
+ scale: function(sx, sy) {
2437
+ this._sx = sx;
2438
+ this._sy = sy || sx;
2439
+ },
2440
+
2441
+ caps_height: function(font_size) {
2442
+ var X = this._sized_text(font_size, 'X'),
2443
+ height = this._element_size(X).height;
2444
+ this._remove_node(X);
2445
+ return height;
2446
+ },
2447
+
2448
+ text_width: function(font_size, text) {
2449
+ var element = this._sized_text(font_size, text);
2450
+ var width = this._element_size(element).width;
2451
+ this._remove_node(element);
2452
+ return width;
2453
+ },
2454
+
2455
+ get_type_metrics: function(text) {
2456
+ var node = this._sized_text(this.pointsize, text);
2457
+ document.body.appendChild(node);
2458
+ var size = this._element_size(node);
2459
+ this._remove_node(node);
2460
+ return size;
2461
+ },
2462
+
2463
+ clear: function(width, height) {
2464
+ this._canvas.width = width;
2465
+ this._canvas.height = height;
2466
+ this._ctx.clearRect(0, 0, width, height);
2467
+ var wrapper = this._text_container(), children = wrapper.childNodes, i = children.length;
2468
+ wrapper.style.width = width + 'px';
2469
+ wrapper.style.height = height + 'px';
2470
+ while (i--) {
2471
+ if (children[i].tagName.toLowerCase() !== 'canvas') {
2472
+ Bluff.Event.stopObserving(children[i]);
2473
+ this._remove_node(children[i]);
2474
+ }
2475
+ }
2476
+ },
2477
+
2478
+ push: function() {
2479
+ this._ctx.save();
2480
+ },
2481
+
2482
+ pop: function() {
2483
+ this._ctx.restore();
2484
+ },
2485
+
2486
+ render_gradiated_background: function(width, height, top_color, bottom_color) {
2487
+ this.clear(width, height);
2488
+ var gradient = this._ctx.createLinearGradient(0,0, 0,height);
2489
+ gradient.addColorStop(0, top_color);
2490
+ gradient.addColorStop(1, bottom_color);
2491
+ this._ctx.fillStyle = gradient;
2492
+ this._ctx.fillRect(0, 0, width, height);
2493
+ },
2494
+
2495
+ render_solid_background: function(width, height, color) {
2496
+ this.clear(width, height);
2497
+ this._ctx.fillStyle = color;
2498
+ this._ctx.fillRect(0, 0, width, height);
2499
+ },
2500
+
2501
+ annotate_scaled: function(width, height, x, y, text, scale) {
2502
+ var scaled_width = (width * scale) >= 1 ? (width * scale) : 1;
2503
+ var scaled_height = (height * scale) >= 1 ? (height * scale) : 1;
2504
+ var text = this._sized_text(this.pointsize, text);
2505
+ text.style.color = this.fill;
2506
+ text.style.cursor = 'default';
2507
+ text.style.fontWeight = this.font_weight;
2508
+ text.style.textAlign = 'center';
2509
+ text.style.left = (this._sx * x + this._left_adjustment(text, scaled_width)) + 'px';
2510
+ text.style.top = (this._sy * y + this._top_adjustment(text, scaled_height)) + 'px';
2511
+ },
2512
+
2513
+ tooltip: function(left, top, width, height, name, color, data) {
2514
+ if (width < 0) left += width;
2515
+ if (height < 0) top += height;
2516
+
2517
+ var wrapper = this._canvas.parentNode,
2518
+ target = document.createElement('div');
2519
+ target.className = this.klass.TARGET_CLASS;
2520
+ target.style.cursor = 'default';
2521
+ target.style.position = 'absolute';
2522
+ target.style.left = (this._sx * left - 3) + 'px';
2523
+ target.style.top = (this._sy * top - 3) + 'px';
2524
+ target.style.width = (this._sx * Math.abs(width) + 5) + 'px';
2525
+ target.style.height = (this._sy * Math.abs(height) + 5) + 'px';
2526
+ target.style.fontSize = 0;
2527
+ target.style.overflow = 'hidden';
2528
+
2529
+ Bluff.Event.observe(target, 'mouseover', function(node) {
2530
+ Bluff.Tooltip.show(name, color, data);
2531
+ });
2532
+ Bluff.Event.observe(target, 'mouseout', function(node) {
2533
+ Bluff.Tooltip.hide();
2534
+ });
2535
+
2536
+ wrapper.appendChild(target);
2537
+ return target;
2538
+ },
2539
+
2540
+ circle: function(origin_x, origin_y, perim_x, perim_y, arc_start, arc_end) {
2541
+ var radius = Math.sqrt(Math.pow(perim_x - origin_x, 2) + Math.pow(perim_y - origin_y, 2));
2542
+ var alpha = 0, beta = 2 * Math.PI; // radians to full circle
2543
+
2544
+ this._ctx.fillStyle = this.fill;
2545
+ this._ctx.beginPath();
2546
+
2547
+ if (arc_start !== undefined && arc_end !== undefined &&
2548
+ Math.abs(Math.floor(arc_end - arc_start)) !== 360) {
2549
+ alpha = arc_start * Math.PI/180;
2550
+ beta = arc_end * Math.PI/180;
2551
+
2552
+ this._ctx.moveTo(this._sx * (origin_x + radius * Math.cos(beta)), this._sy * (origin_y + radius * Math.sin(beta)));
2553
+ this._ctx.lineTo(this._sx * origin_x, this._sy * origin_y);
2554
+ this._ctx.lineTo(this._sx * (origin_x + radius * Math.cos(alpha)), this._sy * (origin_y + radius * Math.sin(alpha)));
2555
+ }
2556
+ this._ctx.arc(this._sx * origin_x, this._sy * origin_y, this._sx * radius, alpha, beta, false); // draw it clockwise
2557
+ this._ctx.fill();
2558
+ },
2559
+
2560
+ line: function(sx, sy, ex, ey) {
2561
+ this._ctx.strokeStyle = this.stroke;
2562
+ this._ctx.lineWidth = this.stroke_width;
2563
+ this._ctx.beginPath();
2564
+ this._ctx.moveTo(this._sx * sx, this._sy * sy);
2565
+ this._ctx.lineTo(this._sx * ex, this._sy * ey);
2566
+ this._ctx.stroke();
2567
+ },
2568
+
2569
+ polyline: function(points) {
2570
+ this._ctx.fillStyle = this.fill;
2571
+ this._ctx.globalAlpha = this.fill_opacity || 1;
2572
+ try { this._ctx.strokeStyle = this.stroke; } catch (e) {}
2573
+ var x = points.shift(), y = points.shift();
2574
+ this._ctx.beginPath();
2575
+ this._ctx.moveTo(this._sx * x, this._sy * y);
2576
+ while (points.length > 0) {
2577
+ x = points.shift(); y = points.shift();
2578
+ this._ctx.lineTo(this._sx * x, this._sy * y);
2579
+ }
2580
+ this._ctx.fill();
2581
+ },
2582
+
2583
+ rectangle: function(ax, ay, bx, by) {
2584
+ var temp;
2585
+ if (ax > bx) { temp = ax; ax = bx; bx = temp; }
2586
+ if (ay > by) { temp = ay; ay = by; by = temp; }
2587
+ try {
2588
+ this._ctx.fillStyle = this.fill;
2589
+ this._ctx.fillRect(this._sx * ax, this._sy * ay, this._sx * (bx-ax), this._sy * (by-ay));
2590
+ } catch (e) {}
2591
+ try {
2592
+ this._ctx.strokeStyle = this.stroke;
2593
+ if (this.stroke !== 'transparent')
2594
+ this._ctx.strokeRect(this._sx * ax, this._sy * ay, this._sx * (bx-ax), this._sy * (by-ay));
2595
+ } catch (e) {}
2596
+ },
2597
+
2598
+ _left_adjustment: function(node, width) {
2599
+ var w = this._element_size(node).width;
2600
+ switch (this.gravity) {
2601
+ case 'west': return 0;
2602
+ case 'east': return width - w;
2603
+ case 'north': case 'south': case 'center':
2604
+ return (width - w) / 2;
2605
+ }
2606
+ },
2607
+
2608
+ _top_adjustment: function(node, height) {
2609
+ var h = this._element_size(node).height;
2610
+ switch (this.gravity) {
2611
+ case 'north': return 0;
2612
+ case 'south': return height - h;
2613
+ case 'west': case 'east': case 'center':
2614
+ return (height - h) / 2;
2615
+ }
2616
+ },
2617
+
2618
+ _text_container: function() {
2619
+ var wrapper = this._canvas.parentNode;
2620
+ if (wrapper.className === this.klass.WRAPPER_CLASS) return wrapper;
2621
+ wrapper = document.createElement('div');
2622
+ wrapper.className = this.klass.WRAPPER_CLASS;
2623
+
2624
+ wrapper.style.position = 'relative';
2625
+ wrapper.style.border = 'none';
2626
+ wrapper.style.padding = '0 0 0 0';
2627
+
2628
+ this._canvas.parentNode.insertBefore(wrapper, this._canvas);
2629
+ wrapper.appendChild(this._canvas);
2630
+ return wrapper;
2631
+ },
2632
+
2633
+ _sized_text: function(size, content) {
2634
+ var text = this._text_node(content);
2635
+ text.style.fontFamily = this.font;
2636
+ text.style.fontSize = (typeof size === 'number') ? size + 'px' : size;
2637
+ return text;
2638
+ },
2639
+
2640
+ _text_node: function(content) {
2641
+ var div = document.createElement('div');
2642
+ div.className = this.klass.TEXT_CLASS;
2643
+ div.style.position = 'absolute';
2644
+ div.appendChild(document.createTextNode(content));
2645
+ this._text_container().appendChild(div);
2646
+ return div;
2647
+ },
2648
+
2649
+ _remove_node: function(node) {
2650
+ node.parentNode.removeChild(node);
2651
+ if (node.className === this.klass.TARGET_CLASS)
2652
+ Bluff.Event.stopObserving(node);
2653
+ },
2654
+
2655
+ _element_size: function(element) {
2656
+ var display = element.style.display;
2657
+ return (display && display !== 'none')
2658
+ ? {width: element.offsetWidth, height: element.offsetHeight}
2659
+ : {width: element.clientWidth, height: element.clientHeight};
2660
+ }
2661
+ });
2662
+
2663
+
2664
+ // DOM event module, adapted from Prototype
2665
+ // Copyright (c) 2005-2008 Sam Stephenson
2666
+
2667
+ Bluff.Event = {
2668
+ _cache: [],
2669
+
2670
+ _isIE: (window.attachEvent && navigator.userAgent.indexOf('Opera') === -1),
2671
+
2672
+ observe: function(element, eventName, callback, scope) {
2673
+ var handlers = Bluff.map(this._handlersFor(element, eventName),
2674
+ function(entry) { return entry._handler });
2675
+ if (Bluff.index(handlers, callback) !== -1) return;
2676
+
2677
+ var responder = function(event) {
2678
+ callback.call(scope || null, element, Bluff.Event._extend(event));
2679
+ };
2680
+ this._cache.push({_node: element, _name: eventName,
2681
+ _handler: callback, _responder: responder});
2682
+
2683
+ if (element.addEventListener)
2684
+ element.addEventListener(eventName, responder, false);
2685
+ else
2686
+ element.attachEvent('on' + eventName, responder);
2687
+ },
2688
+
2689
+ stopObserving: function(element) {
2690
+ var handlers = element ? this._handlersFor(element) : this._cache;
2691
+ Bluff.each(handlers, function(entry) {
2692
+ if (entry._node.removeEventListener)
2693
+ entry._node.removeEventListener(entry._name, entry._responder, false);
2694
+ else
2695
+ entry._node.detachEvent('on' + entry._name, entry._responder);
2696
+ });
2697
+ },
2698
+
2699
+ _handlersFor: function(element, eventName) {
2700
+ var results = [];
2701
+ Bluff.each(this._cache, function(entry) {
2702
+ if (element && entry._node !== element) return;
2703
+ if (eventName && entry._name !== eventName) return;
2704
+ results.push(entry);
2705
+ });
2706
+ return results;
2707
+ },
2708
+
2709
+ _extend: function(event) {
2710
+ if (!this._isIE) return event;
2711
+ if (!event) return false;
2712
+ if (event._extendedByBluff) return event;
2713
+ event._extendedByBluff = true;
2714
+
2715
+ var pointer = this._pointer(event);
2716
+ event.target = event.srcElement;
2717
+ event.pageX = pointer.x;
2718
+ event.pageY = pointer.y;
2719
+
2720
+ return event;
2721
+ },
2722
+
2723
+ _pointer: function(event) {
2724
+ var docElement = document.documentElement,
2725
+ body = document.body || { scrollLeft: 0, scrollTop: 0 };
2726
+ return {
2727
+ x: event.pageX || (event.clientX +
2728
+ (docElement.scrollLeft || body.scrollLeft) -
2729
+ (docElement.clientLeft || 0)),
2730
+ y: event.pageY || (event.clientY +
2731
+ (docElement.scrollTop || body.scrollTop) -
2732
+ (docElement.clientTop || 0))
2733
+ };
2734
+ }
2735
+ };
2736
+
2737
+ if (Bluff.Event._isIE)
2738
+ window.attachEvent('onunload', function() {
2739
+ Bluff.Event.stopObserving();
2740
+ Bluff.Event._cache = null;
2741
+ });
2742
+
2743
+ if (navigator.userAgent.indexOf('AppleWebKit/') > -1)
2744
+ window.addEventListener('unload', function() {}, false);
2745
+
2746
+
2747
+ Bluff.Tooltip = new JS.Singleton({
2748
+ LEFT_OFFSET: 20,
2749
+ TOP_OFFSET: -6,
2750
+ DATA_LENGTH: 8,
2751
+
2752
+ CLASS_NAME: 'bluff-tooltip',
2753
+
2754
+ setup: function() {
2755
+ this._tip = document.createElement('div');
2756
+ this._tip.className = this.CLASS_NAME;
2757
+ this._tip.style.position = 'absolute';
2758
+ this.hide();
2759
+ document.body.appendChild(this._tip);
2760
+
2761
+ Bluff.Event.observe(document.body, 'mousemove', function(body, event) {
2762
+ this._tip.style.left = (event.pageX + this.LEFT_OFFSET) + 'px';
2763
+ this._tip.style.top = (event.pageY + this.TOP_OFFSET) + 'px';
2764
+ }, this);
2765
+ },
2766
+
2767
+ show: function(name, color, data) {
2768
+ data = Number(String(data).substr(0, this.DATA_LENGTH));
2769
+ this._tip.innerHTML = '<span class="color" style="background: ' + color + ';">&nbsp;</span> ' +
2770
+ '<span class="label">' + name + '</span> ' +
2771
+ '<span class="data">' + data + '</span>';
2772
+ this._tip.style.display = '';
2773
+ },
2774
+
2775
+ hide: function() {
2776
+ this._tip.style.display = 'none';
2777
+ }
2778
+ });
2779
+
2780
+ Bluff.Event.observe(window, 'load', Bluff.Tooltip.method('setup'));
2781
+
2782
+
2783
+ Bluff.TableReader = new JS.Class({
2784
+
2785
+ NUMBER_FORMAT: /\-?(0|[1-9]\d*)(\.\d+)?(e[\+\-]?\d+)?/i,
2786
+
2787
+ initialize: function(table, options) {
2788
+ this._options = options || {};
2789
+ this._orientation = this._options.orientation || 'auto';
2790
+
2791
+ this._table = (typeof table === 'string')
2792
+ ? document.getElementById(table)
2793
+ : table;
2794
+ },
2795
+
2796
+ // Get array of data series from the table
2797
+ get_data: function() {
2798
+ if (!this._data) this._read();
2799
+ return this._data;
2800
+ },
2801
+
2802
+ // Get set of axis labels to use for the graph
2803
+ get_labels: function() {
2804
+ if (!this._labels) this._read();
2805
+ return this._labels;
2806
+ },
2807
+
2808
+ // Get the title from the table's caption
2809
+ get_title: function() {
2810
+ return this._title;
2811
+ },
2812
+
2813
+ // Return series number i
2814
+ get_series: function(i) {
2815
+ if (this._data[i]) return this._data[i];
2816
+ return this._data[i] = {points: []};
2817
+ },
2818
+
2819
+ // Gather data by reading from the table
2820
+ _read: function() {
2821
+ this._row = this._col = 0;
2822
+ this._row_offset = this._col_offset = 0;
2823
+ this._data = [];
2824
+ this._labels = {};
2825
+ this._row_headings = [];
2826
+ this._col_headings = [];
2827
+ this._skip_rows = [];
2828
+ this._skip_cols = [];
2829
+
2830
+ this._walk(this._table);
2831
+ this._cleanup();
2832
+ this._orient();
2833
+
2834
+ Bluff.each(this._col_headings, function(heading, i) {
2835
+ this.get_series(i - this._col_offset).name = heading;
2836
+ }, this);
2837
+
2838
+ Bluff.each(this._row_headings, function(heading, i) {
2839
+ this._labels[i - this._row_offset] = heading;
2840
+ }, this);
2841
+ },
2842
+
2843
+ // Walk the table's DOM tree
2844
+ _walk: function(node) {
2845
+ this._visit(node);
2846
+ var i, children = node.childNodes, n = children.length;
2847
+ for (i = 0; i < n; i++) this._walk(children[i]);
2848
+ },
2849
+
2850
+ // Read a single DOM node from the table
2851
+ _visit: function(node) {
2852
+ if (!node.tagName) return;
2853
+ var content = this._strip_tags(node.innerHTML), x, y;
2854
+ switch (node.tagName.toUpperCase()) {
2855
+
2856
+ case 'TR':
2857
+ if (!this._has_data) this._row_offset = this._row;
2858
+ this._row += 1;
2859
+ this._col = 0;
2860
+ break;
2861
+
2862
+ case 'TD':
2863
+ if (!this._has_data) this._col_offset = this._col;
2864
+ this._has_data = true;
2865
+ this._col += 1;
2866
+ content = content.match(this.NUMBER_FORMAT);
2867
+ if (content === null) {
2868
+ this.get_series(x).points[y] = null;
2869
+ } else {
2870
+ x = this._col - this._col_offset - 1;
2871
+ y = this._row - this._row_offset - 1;
2872
+ this.get_series(x).points[y] = parseFloat(content[0]);
2873
+ }
2874
+ break;
2875
+
2876
+ case 'TH':
2877
+ this._col += 1;
2878
+ if (this._ignore(node)) {
2879
+ this._skip_cols.push(this._col);
2880
+ this._skip_rows.push(this._row);
2881
+ }
2882
+ if (this._col === 1 && this._row === 1)
2883
+ this._row_headings[0] = this._col_headings[0] = content;
2884
+ else if (node.scope === "row" || this._col === 1)
2885
+ this._row_headings[this._row - 1] = content;
2886
+ else
2887
+ this._col_headings[this._col - 1] = content;
2888
+ break;
2889
+
2890
+ case 'CAPTION':
2891
+ this._title = content;
2892
+ break;
2893
+ }
2894
+ },
2895
+
2896
+ _ignore: function(node) {
2897
+ if (!this._options.except) return false;
2898
+
2899
+ var content = this._strip_tags(node.innerHTML),
2900
+ classes = (node.className || '').split(/\s+/),
2901
+ list = [].concat(this._options.except);
2902
+
2903
+ if (Bluff.index(list, content) >= 0) return true;
2904
+ var i = classes.length;
2905
+ while (i--) {
2906
+ if (Bluff.index(list, classes[i]) >= 0) return true;
2907
+ }
2908
+ return false;
2909
+ },
2910
+
2911
+ _cleanup: function() {
2912
+ var i = this._skip_cols.length, index;
2913
+ while (i--) {
2914
+ index = this._skip_cols[i];
2915
+ if (index <= this._col_offset) continue;
2916
+ this._col_headings.splice(index - 1, 1);
2917
+ if (index >= this._col_offset)
2918
+ this._data.splice(index - 1 - this._col_offset, 1);
2919
+ }
2920
+
2921
+ var i = this._skip_rows.length, index;
2922
+ while (i--) {
2923
+ index = this._skip_rows[i];
2924
+ if (index <= this._row_offset) continue;
2925
+ this._row_headings.splice(index - 1, 1);
2926
+ Bluff.each(this._data, function(series) {
2927
+ if (index >= this._row_offset)
2928
+ series.points.splice(index - 1 - this._row_offset, 1);
2929
+ }, this);
2930
+ }
2931
+ },
2932
+
2933
+ _orient: function() {
2934
+ switch (this._orientation) {
2935
+ case 'auto':
2936
+ if ((this._row_headings.length > 1 && this._col_headings.length === 1) ||
2937
+ this._row_headings.length < this._col_headings.length) {
2938
+ this._transpose();
2939
+ }
2940
+ break;
2941
+
2942
+ case 'rows':
2943
+ this._transpose();
2944
+ break;
2945
+ }
2946
+ },
2947
+
2948
+ // Transpose data in memory
2949
+ _transpose: function() {
2950
+ var data = this._data, tmp;
2951
+ this._data = [];
2952
+
2953
+ Bluff.each(data, function(row, i) {
2954
+ Bluff.each(row.points, function(point, p) {
2955
+ this.get_series(p).points[i] = point;
2956
+ }, this);
2957
+ }, this);
2958
+
2959
+ tmp = this._row_headings;
2960
+ this._row_headings = this._col_headings;
2961
+ this._col_headings = tmp;
2962
+
2963
+ tmp = this._row_offset;
2964
+ this._row_offset = this._col_offset;
2965
+ this._col_offset = tmp;
2966
+ },
2967
+
2968
+ // Remove HTML from a string
2969
+ _strip_tags: function(string) {
2970
+ return string.replace(/<\/?[^>]+>/gi, '');
2971
+ },
2972
+
2973
+ extend: {
2974
+ Mixin: new JS.Module({
2975
+ data_from_table: function(table, options) {
2976
+ var reader = new Bluff.TableReader(table, options),
2977
+ data_rows = reader.get_data();
2978
+
2979
+ Bluff.each(data_rows, function(row) {
2980
+ this.data(row.name, row.points);
2981
+ }, this);
2982
+
2983
+ this.labels = reader.get_labels();
2984
+ this.title = reader.get_title() || this.title;
2985
+ }
2986
+ })
2987
+ }
2988
+ });
2989
+
2990
+ Bluff.Base.include(Bluff.TableReader.Mixin);