keigan 0.0.0 → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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);