highcharts-rails 3.0.2 → 3.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,576 @@
1
+ /**
2
+ * @license Map plugin v0.1 for Highcharts
3
+ *
4
+ * (c) 2011-2013 Torstein Hønsi
5
+ *
6
+ * License: www.highcharts.com/license
7
+ */
8
+
9
+ /*
10
+ * See www.highcharts.com/studies/world-map.htm for use case.
11
+ *
12
+ * To do:
13
+ * - Optimize long variable names and alias adapter methods and Highcharts namespace variables
14
+ * - Zoom and pan GUI
15
+ */
16
+ (function (Highcharts) {
17
+ var UNDEFINED,
18
+ Axis = Highcharts.Axis,
19
+ each = Highcharts.each,
20
+ extend = Highcharts.extend,
21
+ merge = Highcharts.merge,
22
+ pick = Highcharts.pick,
23
+ numberFormat = Highcharts.numberFormat,
24
+ plotOptions = Highcharts.getOptions().plotOptions,
25
+ Color = Highcharts.Color,
26
+ noop = function () {};
27
+
28
+ /**
29
+ * Utility for reading SVG paths directly.
30
+ *
31
+ * @todo This is moved to the Data plugin. Make sure it is deleted here.
32
+ */
33
+ Highcharts.pathToArray = function (path) {
34
+ var i;
35
+
36
+ // Move letters apart
37
+ path = path.replace(/([A-Za-z])/g, ' $1 ');
38
+ // Trim
39
+ path = path.replace(/^\s*/, "").replace(/\s*$/, "");
40
+
41
+ // Split on spaces and commas
42
+ path = path.split(/[ ,]+/);
43
+
44
+ for (i = 0; i < path.length; i++) {
45
+ if (!/[a-zA-Z]/.test(path[i])) {
46
+ path[i] = parseFloat(path[i]);
47
+ }
48
+ }
49
+ return path;
50
+ };
51
+
52
+ /**
53
+ * Extend the Axis object with methods specific to maps
54
+ */
55
+ Highcharts.wrap(Axis.prototype, 'init', function (proceed, chart, userOptions) {
56
+
57
+ if (chart.options.chart.type === 'map') {
58
+ extend(this, {
59
+
60
+ /**
61
+ * Override to use the extreme coordinates from the SVG shape, not the
62
+ * data values
63
+ */
64
+ getSeriesExtremes: function () {
65
+ var isXAxis = this.isXAxis,
66
+ dataMin = Number.MAX_VALUE,
67
+ dataMax = Number.MIN_VALUE;
68
+ each(this.series, function (series) {
69
+ dataMin = Math.min(dataMin, series[isXAxis ? 'minX' : 'minY']);
70
+ dataMax = Math.max(dataMax, series[isXAxis ? 'maxX' : 'maxY']);
71
+ });
72
+ this.dataMin = dataMin;
73
+ this.dataMax = dataMax;
74
+ },
75
+
76
+ /**
77
+ * Override axis translation to make sure the aspect ratio is always kept
78
+ */
79
+ setAxisTranslation: function () {
80
+ var chart = this.chart,
81
+ mapRatio,
82
+ plotRatio = chart.plotWidth / chart.plotHeight,
83
+ isXAxis = this.isXAxis,
84
+ adjustedAxisLength,
85
+ xAxis = chart.xAxis[0],
86
+ padAxis;
87
+
88
+ // Run the parent method
89
+ Axis.prototype.setAxisTranslation.call(this);
90
+
91
+ // On Y axis, handle both
92
+ if (!isXAxis && xAxis.transA !== UNDEFINED) {
93
+
94
+ // Use the same translation for both axes
95
+ this.transA = xAxis.transA = Math.min(this.transA, xAxis.transA);
96
+
97
+ mapRatio = (xAxis.max - xAxis.min) / (this.max - this.min);
98
+
99
+ // What axis to pad to put the map in the middle
100
+ padAxis = mapRatio > plotRatio ? this : xAxis;
101
+
102
+ // Pad it
103
+ adjustedAxisLength = (padAxis.max - padAxis.min) * padAxis.transA;
104
+ padAxis.minPixelPadding = (padAxis.len - adjustedAxisLength) / 2;
105
+ }
106
+
107
+ }
108
+ });
109
+ }
110
+
111
+ return proceed.call(this, chart, userOptions);
112
+ });
113
+
114
+ /**
115
+ * Extend the default options with map options
116
+ */
117
+ plotOptions.map = merge(
118
+ plotOptions.scatter,
119
+ {
120
+ animation: false, // makes the complex shapes slow
121
+ minOpacity: 0.2,
122
+ nullColor: '#F8F8F8',
123
+ borderColor: 'silver',
124
+ borderWidth: 1,
125
+ marker: null,
126
+ stickyTracking: false,
127
+ tooltip: {
128
+ followPointer: true,
129
+ headerFormat: '<span style="font-size:10px">{point.key}</span><br/>',
130
+ pointFormat: '{series.name}: {point.y}<br/>'
131
+ }
132
+ }
133
+ );
134
+
135
+ /**
136
+ * Add the series type
137
+ */
138
+ Highcharts.seriesTypes.map = Highcharts.extendClass(Highcharts.seriesTypes.scatter, {
139
+ type: 'map',
140
+ pointAttrToOptions: { // mapping between SVG attributes and the corresponding options
141
+ stroke: 'borderColor',
142
+ 'stroke-width': 'borderWidth',
143
+ fill: 'color'
144
+ },
145
+ colorKey: 'y',
146
+ trackerGroups: ['group', 'markerGroup'],
147
+ getSymbol: noop,
148
+ getExtremesFromAll: true,
149
+ init: function (chart) {
150
+ var series = this,
151
+ valueDecimals = chart.options.legend.valueDecimals,
152
+ legendItems = [],
153
+ name,
154
+ from,
155
+ to,
156
+ fromLabel,
157
+ toLabel,
158
+ colorRange,
159
+ gradientColor,
160
+ grad,
161
+ tmpLabel,
162
+ horizontal = chart.options.legend.layout === 'horizontal';
163
+
164
+
165
+ Highcharts.Series.prototype.init.apply(this, arguments);
166
+ colorRange = series.options.colorRange;
167
+
168
+ if (series.options.valueRanges) {
169
+ each(series.options.valueRanges, function (range) {
170
+ from = range.from;
171
+ to = range.to;
172
+
173
+ // Assemble the default name. This can be overridden by legend.options.labelFormatter
174
+ name = '';
175
+ if (from === UNDEFINED) {
176
+ name = '< ';
177
+ } else if (to === UNDEFINED) {
178
+ name = '> ';
179
+ }
180
+ if (from !== UNDEFINED) {
181
+ name += numberFormat(from, valueDecimals);
182
+ }
183
+ if (from !== UNDEFINED && to !== UNDEFINED) {
184
+ name += ' - ';
185
+ }
186
+ if (to !== UNDEFINED) {
187
+ name += numberFormat(to, valueDecimals);
188
+ }
189
+
190
+ // Add a mock object to the legend items
191
+ legendItems.push(Highcharts.extend({
192
+ chart: series.chart,
193
+ name: name,
194
+ options: {},
195
+ drawLegendSymbol: Highcharts.seriesTypes.area.prototype.drawLegendSymbol,
196
+ visible: true,
197
+ setState: function () {},
198
+ setVisible: function () {}
199
+ }, range));
200
+ });
201
+ series.legendItems = legendItems;
202
+
203
+ } else if (colorRange) {
204
+
205
+ from = colorRange.from;
206
+ to = colorRange.to;
207
+ fromLabel = colorRange.fromLabel;
208
+ toLabel = colorRange.toLabel;
209
+
210
+ // Flips linearGradient variables and label text.
211
+ grad = horizontal ? [0, 0, 1, 0] : [0, 1, 0, 0];
212
+ if (!horizontal) {
213
+ tmpLabel = fromLabel;
214
+ fromLabel = toLabel;
215
+ toLabel = tmpLabel;
216
+ }
217
+
218
+ // Creates color gradient.
219
+ gradientColor = {
220
+ linearGradient: { x1: grad[0], y1: grad[1], x2: grad[2], y2: grad[3] },
221
+ stops:
222
+ [
223
+ [0, from],
224
+ [1, to]
225
+ ]
226
+ };
227
+
228
+ // Add a mock object to the legend items.
229
+ legendItems = [{
230
+ chart: series.chart,
231
+ options: {},
232
+ fromLabel: fromLabel,
233
+ toLabel: toLabel,
234
+ color: gradientColor,
235
+ drawLegendSymbol: this.drawLegendSymbol,
236
+ visible: true,
237
+ setState: function () {},
238
+ setVisible: function () {}
239
+ }];
240
+
241
+ series.legendItems = legendItems;
242
+ }
243
+ },
244
+
245
+ /**
246
+ * Gets the series' symbol in the legend and extended legend with more information.
247
+ *
248
+ * @param {Object} legend The legend object
249
+ * @param {Object} item The series (this) or point
250
+ */
251
+ drawLegendSymbol: function (legend, item) {
252
+
253
+ var spacing = legend.options.symbolPadding,
254
+ padding = pick(legend.options.padding, 8),
255
+ positionY,
256
+ positionX,
257
+ gradientSize = this.chart.renderer.fontMetrics(legend.options.itemStyle.fontSize).h,
258
+ horizontal = legend.options.layout === 'horizontal',
259
+ box1,
260
+ box2,
261
+ box3,
262
+ rectangleLength = pick(legend.options.rectangleLength, 200);
263
+
264
+ // Set local variables based on option.
265
+ if (horizontal) {
266
+ positionY = -(spacing / 2);
267
+ positionX = 0;
268
+ } else {
269
+ positionY = -rectangleLength + legend.baseline - (spacing / 2);
270
+ positionX = padding + gradientSize;
271
+ }
272
+
273
+ // Creates the from text.
274
+ item.fromText = this.chart.renderer.text(
275
+ item.fromLabel, // Text.
276
+ positionX, // Lower left x.
277
+ positionY // Lower left y.
278
+ ).attr({
279
+ zIndex: 2
280
+ }).add(item.legendGroup);
281
+ box1 = item.fromText.getBBox();
282
+
283
+ // Creates legend symbol.
284
+ // Ternary changes variables based on option.
285
+ item.legendSymbol = this.chart.renderer.rect(
286
+ horizontal ? box1.x + box1.width + spacing : box1.x - gradientSize - spacing, // Upper left x.
287
+ box1.y, // Upper left y.
288
+ horizontal ? rectangleLength : gradientSize, // Width.
289
+ horizontal ? gradientSize : rectangleLength, // Height.
290
+ 2 // Corner radius.
291
+ ).attr({
292
+ zIndex: 1
293
+ }).add(item.legendGroup);
294
+ box2 = item.legendSymbol.getBBox();
295
+
296
+ // Creates the to text.
297
+ // Vertical coordinate changed based on option.
298
+ item.toText = this.chart.renderer.text(
299
+ item.toLabel,
300
+ box2.x + box2.width + spacing,
301
+ horizontal ? positionY : box2.y + box2.height - spacing
302
+ ).attr({
303
+ zIndex: 2
304
+ }).add(item.legendGroup);
305
+ box3 = item.toText.getBBox();
306
+
307
+ // Changes legend box settings based on option.
308
+ if (horizontal) {
309
+ legend.offsetWidth = box1.width + box2.width + box3.width + (spacing * 2) + padding;
310
+ legend.itemY = gradientSize + padding;
311
+ } else {
312
+ legend.offsetWidth = Math.max(box1.width, box3.width) + (spacing) + box2.width + padding;
313
+ legend.itemY = box2.height + padding;
314
+ legend.itemX = spacing;
315
+ }
316
+ },
317
+
318
+ /**
319
+ * Get the bounding box of all paths in the map combined.
320
+ */
321
+ getBox: function () {
322
+ var chart = this.chart,
323
+ maxX = -Math.pow(2, 31),
324
+ minX = Math.pow(2, 31) - 1,
325
+ maxY = -Math.pow(2, 31),
326
+ minY = Math.pow(2, 31) - 1,
327
+ xyRatio,
328
+ ratioCorrection,
329
+ plotWidth = chart.plotWidth,
330
+ plotHeight = chart.plotHeight,
331
+ pad;
332
+
333
+
334
+ // Find the bounding box
335
+ each(this.options.data, function (point) {
336
+ var path = point.path,
337
+ i = path.length,
338
+ even = false; // while loop reads from the end
339
+
340
+ while (i--) {
341
+ if (typeof path[i] === 'number') {
342
+ if (even) { // even = x
343
+ maxX = Math.max(maxX, path[i]);
344
+ minX = Math.min(minX, path[i]);
345
+ } else { // odd = Y
346
+ maxY = Math.max(maxY, path[i]);
347
+ minY = Math.min(minY, path[i]);
348
+ }
349
+ even = !even;
350
+ }
351
+ }
352
+ });
353
+ this.minY = minY;
354
+ this.maxY = maxY;
355
+ this.minX = minX;
356
+ this.maxX = maxX;
357
+
358
+ },
359
+
360
+
361
+
362
+ /**
363
+ * Translate the path so that it automatically fits into the plot area box
364
+ * @param {Object} path
365
+ */
366
+ translatePath: function (path) {
367
+
368
+ var series = this,
369
+ chart = series.chart,
370
+ even = false, // while loop reads from the end
371
+ xAxis = series.xAxis,
372
+ yAxis = series.yAxis;
373
+
374
+ // Preserve the original
375
+ path = [].concat(path);
376
+
377
+ // Do the translation
378
+ i = path.length;
379
+ while (i--) {
380
+ if (typeof path[i] === 'number') {
381
+ if (even) { // even = x
382
+ path[i] = Math.round(xAxis.translate(path[i]));
383
+ } else { // odd = Y
384
+ path[i] = Math.round(yAxis.len - yAxis.translate(path[i]));
385
+ }
386
+ even = !even;
387
+ }
388
+ }
389
+ return path;
390
+ },
391
+
392
+ setData: function () {
393
+ Highcharts.Series.prototype.setData.apply(this, arguments);
394
+ this.getBox();
395
+ },
396
+
397
+ /**
398
+ * Add the path option for data points. Find the max value for color calculation.
399
+ */
400
+ translate: function () {
401
+ var series = this,
402
+ options = series.options,
403
+ dataMin = Number.MAX_VALUE,
404
+ dataMax = Number.MIN_VALUE,
405
+ opacity,
406
+ minOpacity = options.minOpacity,
407
+ path,
408
+ color;
409
+
410
+ series.generatePoints();
411
+
412
+ each(series.data, function (point) {
413
+
414
+ point.shapeType = 'path';
415
+ point.shapeArgs = {
416
+ d: series.translatePath(point.path)
417
+ };
418
+
419
+ // TODO: do point colors in drawPoints instead of point.init
420
+ if (typeof point.y === 'number') {
421
+ if (point.y > dataMax) {
422
+ dataMax = point.y;
423
+ } else if (point.y < dataMin) {
424
+ dataMin = point.y;
425
+ }
426
+ }
427
+ });
428
+
429
+ series.translateColors(dataMin, dataMax);
430
+ },
431
+
432
+ /**
433
+ * In choropleth maps, the color is a result of the value, so this needs translation tood
434
+ */
435
+ translateColors: function (dataMin, dataMax) {
436
+
437
+ var seriesOptions = this.options,
438
+ valueRanges = seriesOptions.valueRanges,
439
+ colorRange = seriesOptions.colorRange,
440
+ colorKey = this.colorKey;
441
+
442
+ each(this.data, function (point) {
443
+ var value = point[colorKey],
444
+ rgba = [],
445
+ range,
446
+ from,
447
+ to,
448
+ i,
449
+ pos;
450
+
451
+ if (valueRanges) {
452
+ i = valueRanges.length;
453
+ while (i--) {
454
+ range = valueRanges[i];
455
+ from = range.from;
456
+ to = range.to;
457
+ if ((from === UNDEFINED || value >= from) && (to === UNDEFINED || value <= to)) {
458
+ point.options.color = range.color;
459
+ break;
460
+ }
461
+
462
+ }
463
+ } else if (colorRange && value !== undefined) {
464
+ from = Color(colorRange.from);
465
+ to = Color(colorRange.to);
466
+ pos = (dataMax - value) / (dataMax - dataMin);
467
+ i = 4;
468
+ while (i--) {
469
+ rgba[i] = Math.round(
470
+ to.rgba[i] + (from.rgba[i] - to.rgba[i]) * pos
471
+ );
472
+ }
473
+ point.options.color = 'rgba(' + rgba.join(',') + ')';
474
+ }
475
+ });
476
+ },
477
+
478
+ drawGraph: noop,
479
+
480
+ /**
481
+ * We need the points' bounding boxes in order to draw the data labels, so
482
+ * we skip it now and call if from drawPoints instead.
483
+ */
484
+ drawDataLabels: noop,
485
+
486
+ /**
487
+ * Use the drawPoints method of column, that is able to handle simple shapeArgs.
488
+ * Extend it by assigning the tooltip position.
489
+ */
490
+ drawPoints: function () {
491
+ var series = this,
492
+ chart = series.chart,
493
+ saturation,
494
+ bBox,
495
+ colorKey = series.colorKey;
496
+
497
+ // Make points pass test in drawing
498
+ each(series.data, function (point) {
499
+ point.plotY = 1; // pass null test in column.drawPoints
500
+ if (point[colorKey] === null) {
501
+ point[colorKey] = 0;
502
+ point.isNull = true;
503
+ }
504
+ });
505
+
506
+ // Draw them
507
+ Highcharts.seriesTypes.column.prototype.drawPoints.apply(series);
508
+
509
+ each(series.data, function (point) {
510
+
511
+ bBox = point.graphic.getBBox();
512
+ // for tooltip
513
+ point.tooltipPos = [
514
+ bBox.x + bBox.width / 2,
515
+ bBox.y + bBox.height / 2
516
+ ];
517
+ // for data labels
518
+ point.plotX = point.tooltipPos[0];
519
+ point.plotY = point.tooltipPos[1];
520
+
521
+ // Reset escaped null points
522
+ if (point.isNull) {
523
+ point[colorKey] = null;
524
+ }
525
+ });
526
+
527
+ // Now draw the data labels
528
+ Highcharts.Series.prototype.drawDataLabels.call(series);
529
+
530
+ }
531
+ });
532
+
533
+ /**
534
+ * A wrapper for Chart with all the default values for a Map
535
+ */
536
+ Highcharts.Map = function (options, callback) {
537
+
538
+ var hiddenAxis = {
539
+ endOnTick: false,
540
+ gridLineWidth: 0,
541
+ labels: {
542
+ enabled: false
543
+ },
544
+ lineWidth: 0,
545
+ minPadding: 0,
546
+ maxPadding: 0,
547
+ startOnTick: false,
548
+ tickWidth: 0,
549
+ title: null
550
+ };
551
+
552
+ // Don't merge the data
553
+ seriesOptions = options.series;
554
+ options.series = null;
555
+
556
+ options = merge({
557
+ chart: {
558
+ type: 'map'
559
+ },
560
+ xAxis: hiddenAxis,
561
+ yAxis: merge(hiddenAxis, { reversed: true })
562
+ },
563
+ options, // user's options
564
+
565
+ { // forced options
566
+ chart: {
567
+ inverted: false
568
+ }
569
+ });
570
+
571
+ options.series = seriesOptions;
572
+
573
+
574
+ return new Highcharts.Chart(options, callback);
575
+ };
576
+ }(Highcharts));