1 |
|
2 | (function (factory) {
|
3 | if (typeof module === 'object' && module.exports) {
|
4 | module.exports = factory;
|
5 | } else {
|
6 | factory(Highcharts);
|
7 | }
|
8 | }(function (HC) {
|
9 | 'use strict';
|
10 | |
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 | var UNDEFINED = void 0,
|
20 | mathRound = Math.round,
|
21 | mathMin = Math.min,
|
22 | mathMax = Math.max,
|
23 | merge = HC.merge,
|
24 | pick = HC.pick,
|
25 | each = HC.each,
|
26 |
|
27 |
|
28 | axisProto = HC.Axis.prototype,
|
29 | tickProto = HC.Tick.prototype,
|
30 |
|
31 |
|
32 | protoAxisInit = axisProto.init,
|
33 | protoAxisRender = axisProto.render,
|
34 | protoAxisSetCategories = axisProto.setCategories,
|
35 | protoTickGetLabelSize = tickProto.getLabelSize,
|
36 | protoTickAddLabel = tickProto.addLabel,
|
37 | protoTickDestroy = tickProto.destroy,
|
38 | protoTickRender = tickProto.render;
|
39 |
|
40 | function deepClone(thing) {
|
41 | return JSON.parse(JSON.stringify(thing));
|
42 | }
|
43 |
|
44 | function Category(obj, parent) {
|
45 | this.userOptions = deepClone(obj);
|
46 | this.name = obj.name || obj;
|
47 | this.parent = parent;
|
48 |
|
49 | return this;
|
50 | }
|
51 |
|
52 | Category.prototype.toString = function () {
|
53 | var parts = [],
|
54 | cat = this;
|
55 |
|
56 | while (cat) {
|
57 | parts.push(cat.name);
|
58 | cat = cat.parent;
|
59 | }
|
60 |
|
61 | return parts.join(', ');
|
62 | };
|
63 |
|
64 |
|
65 | function sum(arr) {
|
66 | var l = arr.length,
|
67 | x = 0;
|
68 |
|
69 | while (l--) {
|
70 | x += arr[l];
|
71 | }
|
72 |
|
73 | return x;
|
74 | }
|
75 |
|
76 |
|
77 | function addLeaf(out, cat, parent) {
|
78 | out.unshift(new Category(cat, parent));
|
79 |
|
80 | while (parent) {
|
81 | parent.leaves = parent.leaves ? (parent.leaves + 1) : 1;
|
82 | parent = parent.parent;
|
83 | }
|
84 | }
|
85 |
|
86 |
|
87 | function buildTree(cats, out, options, parent, depth) {
|
88 | var len = cats.length,
|
89 | cat;
|
90 |
|
91 | depth = depth ? depth : 0;
|
92 | options.depth = options.depth ? options.depth : 0;
|
93 |
|
94 | while (len--) {
|
95 | cat = cats[len];
|
96 |
|
97 | if (cat.categories) {
|
98 | if (parent) {
|
99 | cat.parent = parent;
|
100 | }
|
101 | buildTree(cat.categories, out, options, cat, depth + 1);
|
102 | } else {
|
103 | addLeaf(out, cat, parent);
|
104 | }
|
105 | }
|
106 | options.depth = mathMax(options.depth, depth);
|
107 | }
|
108 |
|
109 |
|
110 | function addGridPart(path, d, width) {
|
111 |
|
112 | if (d[0] === d[2]) {
|
113 | d[0] = d[2] = mathRound(d[0]) - (width % 2 / 2);
|
114 | }
|
115 | if (d[1] === d[3]) {
|
116 | d[1] = d[3] = mathRound(d[1]) + (width % 2 / 2);
|
117 | }
|
118 |
|
119 | path.push(
|
120 | 'M',
|
121 | d[0], d[1],
|
122 | 'L',
|
123 | d[2], d[3]
|
124 | );
|
125 | }
|
126 |
|
127 |
|
128 | function tickPosition(tick, pos) {
|
129 | return tick.getPosition(tick.axis.horiz, pos, tick.axis.tickmarkOffset);
|
130 | }
|
131 |
|
132 | function walk(arr, key, fn) {
|
133 | var l = arr.length,
|
134 | children;
|
135 |
|
136 | while (l--) {
|
137 | children = arr[l][key];
|
138 |
|
139 | if (children) {
|
140 | walk(children, key, fn);
|
141 | }
|
142 | fn(arr[l]);
|
143 | }
|
144 | }
|
145 |
|
146 |
|
147 |
|
148 |
|
149 |
|
150 | axisProto.init = function (chart, options) {
|
151 |
|
152 | protoAxisInit.call(this, chart, options);
|
153 |
|
154 | if (typeof options === 'object' && options.categories) {
|
155 | this.setupGroups(options);
|
156 | }
|
157 | };
|
158 |
|
159 |
|
160 | axisProto.setupGroups = function (options) {
|
161 | var categories = deepClone(options.categories),
|
162 | reverseTree = [],
|
163 | stats = {},
|
164 | labelOptions = this.options.labels,
|
165 | userAttr = labelOptions.groupedOptions,
|
166 | css = labelOptions.style;
|
167 |
|
168 |
|
169 | buildTree(categories, reverseTree, stats);
|
170 |
|
171 |
|
172 | this.categoriesTree = categories;
|
173 | this.categories = reverseTree;
|
174 | this.isGrouped = stats.depth !== 0;
|
175 | this.labelsDepth = stats.depth;
|
176 | this.labelsSizes = [];
|
177 | this.labelsGridPath = [];
|
178 | this.tickLength = options.tickLength || this.tickLength || null;
|
179 |
|
180 | this.tickWidth = pick(options.tickWidth, this.isXAxis ? 1 : 0);
|
181 | this.directionFactor = [-1, 1, 1, -1][this.side];
|
182 | this.options.lineWidth = pick(options.lineWidth, 1);
|
183 |
|
184 | this.groupFontHeights = [];
|
185 | for (var i = 0; i <= stats.depth; i++) {
|
186 | var hasOptions = userAttr && userAttr[i - 1],
|
187 | mergedCSS = hasOptions && userAttr[i - 1].style ? merge(css, userAttr[i - 1].style) : css;
|
188 | this.groupFontHeights[i] = Math.round(this.chart.renderer.fontMetrics(mergedCSS ? mergedCSS.fontSize : 0).b * 0.3);
|
189 | }
|
190 | };
|
191 |
|
192 |
|
193 | axisProto.render = function () {
|
194 |
|
195 | if (this.isGrouped) {
|
196 | this.labelsGridPath = [];
|
197 | }
|
198 |
|
199 |
|
200 | if (this.originalTickLength === UNDEFINED) {
|
201 | this.originalTickLength = this.options.tickLength;
|
202 | }
|
203 |
|
204 |
|
205 |
|
206 |
|
207 | this.options.tickLength = this.isGrouped ? 0.001 : this.originalTickLength;
|
208 |
|
209 | protoAxisRender.call(this);
|
210 |
|
211 | if (!this.isGrouped) {
|
212 | if (this.labelsGrid) {
|
213 | this.labelsGrid.attr({
|
214 | visibility: 'hidden'
|
215 | });
|
216 | }
|
217 | return false;
|
218 | }
|
219 |
|
220 | var axis = this,
|
221 | options = axis.options,
|
222 | top = axis.top,
|
223 | left = axis.left,
|
224 | right = left + axis.width,
|
225 | bottom = top + axis.height,
|
226 | visible = axis.hasVisibleSeries || axis.hasData,
|
227 | depth = axis.labelsDepth,
|
228 | grid = axis.labelsGrid,
|
229 | horiz = axis.horiz,
|
230 | d = axis.labelsGridPath,
|
231 | i = options.drawHorizontalBorders === false ? (depth + 1) : 0,
|
232 | offset = axis.opposite ? (horiz ? top : right) : (horiz ? bottom : left),
|
233 | tickWidth = axis.tickWidth,
|
234 | part;
|
235 |
|
236 | if (axis.userTickLength) {
|
237 | depth -= 1;
|
238 | }
|
239 |
|
240 |
|
241 | if (!grid) {
|
242 | grid = axis.labelsGrid = axis.chart.renderer.path()
|
243 | .attr({
|
244 |
|
245 | strokeWidth: tickWidth,
|
246 | 'stroke-width': tickWidth,
|
247 | stroke: options.tickColor || ''
|
248 | })
|
249 | .add(axis.axisGroup);
|
250 |
|
251 | if (!options.tickColor) {
|
252 | grid.addClass('highcharts-tick');
|
253 | }
|
254 | }
|
255 |
|
256 |
|
257 | while (i <= depth) {
|
258 | offset += axis.groupSize(i);
|
259 |
|
260 | part = horiz ?
|
261 | [left, offset, right, offset] :
|
262 | [offset, top, offset, bottom];
|
263 |
|
264 | addGridPart(d, part, tickWidth);
|
265 | i++;
|
266 | }
|
267 |
|
268 |
|
269 | grid.attr({
|
270 | d: d,
|
271 | visibility: visible ? 'visible' : 'hidden'
|
272 | });
|
273 |
|
274 | axis.labelGroup.attr({
|
275 | visibility: visible ? 'visible' : 'hidden'
|
276 | });
|
277 |
|
278 |
|
279 | walk(axis.categoriesTree, 'categories', function (group) {
|
280 | var tick = group.tick;
|
281 |
|
282 | if (!tick) {
|
283 | return false;
|
284 | }
|
285 | if (tick.startAt + tick.leaves - 1 < axis.min || tick.startAt > axis.max) {
|
286 | tick.label.hide();
|
287 | tick.destroyed = 0;
|
288 | } else {
|
289 | tick.label.attr({
|
290 | visibility: visible ? 'visible' : 'hidden'
|
291 | });
|
292 | }
|
293 | return true;
|
294 | });
|
295 | return true;
|
296 | };
|
297 |
|
298 | axisProto.setCategories = function (newCategories, doRedraw) {
|
299 | if (this.categories) {
|
300 | this.cleanGroups();
|
301 | }
|
302 | this.setupGroups({
|
303 | categories: newCategories
|
304 | });
|
305 | this.categories = this.userOptions.categories = newCategories;
|
306 | protoAxisSetCategories.call(this, this.categories, doRedraw);
|
307 | };
|
308 |
|
309 |
|
310 | axisProto.cleanGroups = function () {
|
311 | var ticks = this.ticks,
|
312 | n;
|
313 |
|
314 | for (n in ticks) {
|
315 | if (ticks[n].parent) {
|
316 | delete ticks[n].parent;
|
317 | }
|
318 | }
|
319 | walk(this.categoriesTree, 'categories', function (group) {
|
320 | var tick = group.tick;
|
321 |
|
322 | if (!tick) {
|
323 | return false;
|
324 | }
|
325 | tick.label.destroy();
|
326 |
|
327 | each(tick, function (v, i) {
|
328 | delete tick[i];
|
329 | });
|
330 | delete group.tick;
|
331 |
|
332 | return true;
|
333 | });
|
334 | this.labelsGrid = null;
|
335 | };
|
336 |
|
337 |
|
338 | axisProto.groupSize = function (level, position) {
|
339 | var positions = this.labelsSizes,
|
340 | direction = this.directionFactor,
|
341 | groupedOptions = this.options.labels.groupedOptions ? this.options.labels.groupedOptions[level - 1] : false,
|
342 | userXY = 0;
|
343 |
|
344 | if (groupedOptions) {
|
345 | if (direction === -1) {
|
346 | userXY = groupedOptions.x ? groupedOptions.x : 0;
|
347 | } else {
|
348 | userXY = groupedOptions.y ? groupedOptions.y : 0;
|
349 | }
|
350 | }
|
351 |
|
352 | if (position !== UNDEFINED) {
|
353 | positions[level] = mathMax(positions[level] || 0, position + 10 + Math.abs(userXY));
|
354 | }
|
355 |
|
356 | if (level === true) {
|
357 | return sum(positions) * direction;
|
358 | } else if (positions[level]) {
|
359 | return positions[level] * direction;
|
360 | }
|
361 |
|
362 | return 0;
|
363 | };
|
364 |
|
365 |
|
366 |
|
367 |
|
368 |
|
369 |
|
370 | tickProto.addLabel = function () {
|
371 | var tick = this,
|
372 | axis = tick.axis,
|
373 | category;
|
374 |
|
375 | protoTickAddLabel.call(tick);
|
376 |
|
377 | if (!axis.categories || !(category = axis.categories[tick.pos])) {
|
378 | return false;
|
379 | }
|
380 |
|
381 |
|
382 | if (tick.label) {
|
383 | tick.label.attr('text', tick.axis.labelFormatter.call({
|
384 | axis: axis,
|
385 | chart: axis.chart,
|
386 | isFirst: tick.isFirst,
|
387 | isLast: tick.isLast,
|
388 | value: category.name,
|
389 | pos: tick.pos
|
390 | }));
|
391 |
|
392 |
|
393 | tick.label.textPxLength = tick.label.getBBox().width;
|
394 | }
|
395 |
|
396 |
|
397 | if (axis.isGrouped && axis.options.labels.enabled) {
|
398 | tick.addGroupedLabels(category);
|
399 | }
|
400 | return true;
|
401 | };
|
402 |
|
403 |
|
404 | tickProto.addGroupedLabels = function (category) {
|
405 | var tick = this,
|
406 | axis = this.axis,
|
407 | chart = axis.chart,
|
408 | options = axis.options.labels,
|
409 | useHTML = options.useHTML,
|
410 | css = options.style,
|
411 | userAttr = options.groupedOptions,
|
412 | attr = {
|
413 | align: 'center',
|
414 | rotation: options.rotation,
|
415 | x: 0,
|
416 | y: 0
|
417 | },
|
418 | size = axis.horiz ? 'height' : 'width',
|
419 | depth = 0,
|
420 | label;
|
421 |
|
422 |
|
423 | while (tick) {
|
424 | if (depth > 0 && !category.tick) {
|
425 |
|
426 | this.value = category.name;
|
427 | var name = options.formatter ? options.formatter.call(this, category) : category.name,
|
428 | hasOptions = userAttr && userAttr[depth - 1],
|
429 | mergedAttrs = hasOptions ? merge(attr, userAttr[depth - 1]) : attr,
|
430 | mergedCSS = hasOptions && userAttr[depth - 1].style ? merge(css, userAttr[depth - 1].style) : css;
|
431 |
|
432 |
|
433 | delete mergedAttrs.style;
|
434 |
|
435 | label = chart.renderer.text(name, 0, 0, useHTML)
|
436 | .attr(mergedAttrs)
|
437 | .add(axis.labelGroup);
|
438 |
|
439 |
|
440 | if (label && !chart.styledMode) {
|
441 | label.css(mergedCSS);
|
442 | }
|
443 |
|
444 |
|
445 | tick.startAt = this.pos;
|
446 | tick.childCount = category.categories.length;
|
447 | tick.leaves = category.leaves;
|
448 | tick.visible = this.childCount;
|
449 | tick.label = label;
|
450 | tick.labelOffsets = {
|
451 | x: mergedAttrs.x,
|
452 | y: mergedAttrs.y
|
453 | };
|
454 |
|
455 |
|
456 | category.tick = tick;
|
457 | }
|
458 |
|
459 |
|
460 | if (tick && tick.label) {
|
461 | axis.groupSize(depth, tick.label.getBBox()[size]);
|
462 | }
|
463 |
|
464 |
|
465 | category = category.parent;
|
466 |
|
467 | if (category) {
|
468 | tick = tick.parent = category.tick || {};
|
469 | } else {
|
470 | tick = null;
|
471 | }
|
472 |
|
473 | depth++;
|
474 | }
|
475 | };
|
476 |
|
477 |
|
478 | tickProto.render = function (index, old, opacity) {
|
479 | protoTickRender.call(this, index, old, opacity);
|
480 |
|
481 | var treeCat = this.axis.categories[this.pos];
|
482 |
|
483 | if (!this.axis.isGrouped || !treeCat || this.pos > this.axis.max) {
|
484 | return;
|
485 | }
|
486 |
|
487 | var tick = this,
|
488 | group = tick,
|
489 | axis = tick.axis,
|
490 | tickPos = tick.pos,
|
491 | isFirst = tick.isFirst,
|
492 | max = axis.max,
|
493 | min = axis.min,
|
494 | horiz = axis.horiz,
|
495 | grid = axis.labelsGridPath,
|
496 | size = axis.groupSize(0),
|
497 | tickWidth = axis.tickWidth,
|
498 | xy = tickPosition(tick, tickPos),
|
499 | start = horiz ? xy.y : xy.x,
|
500 | baseLine = axis.chart.renderer.fontMetrics(axis.options.labels.style ? axis.options.labels.style.fontSize : 0).b,
|
501 | depth = 1,
|
502 | reverseCrisp = ((horiz && xy.x === axis.pos + axis.len) || (!horiz && xy.y === axis.pos)) ? -1 : 0,
|
503 | gridAttrs,
|
504 | lvlSize,
|
505 | minPos,
|
506 | maxPos,
|
507 | attrs,
|
508 | bBox;
|
509 |
|
510 |
|
511 | if (isFirst) {
|
512 | gridAttrs = horiz ?
|
513 | [axis.left, xy.y, axis.left, xy.y + axis.groupSize(true)] : axis.isXAxis ?
|
514 | [xy.x, axis.top, xy.x + axis.groupSize(true), axis.top] : [xy.x, axis.top + axis.len, xy.x + axis.groupSize(true), axis.top + axis.len];
|
515 |
|
516 | addGridPart(grid, gridAttrs, tickWidth);
|
517 | }
|
518 |
|
519 | if (horiz && axis.left < xy.x) {
|
520 | addGridPart(grid, [xy.x - reverseCrisp, xy.y, xy.x - reverseCrisp, xy.y + size], tickWidth);
|
521 | } else if (!horiz && axis.top <= xy.y) {
|
522 | addGridPart(grid, [xy.x, xy.y + reverseCrisp, xy.x + size, xy.y + reverseCrisp], tickWidth);
|
523 | }
|
524 |
|
525 | size = start + size;
|
526 |
|
527 | function fixOffset(tCat) {
|
528 | var ret = 0;
|
529 | if (isFirst) {
|
530 | ret = tCat.parent.categories.indexOf(tCat.name);
|
531 | ret = ret < 0 ? 0 : ret;
|
532 | return ret;
|
533 | }
|
534 | return ret;
|
535 | }
|
536 |
|
537 |
|
538 | while (group.parent) {
|
539 | group = group.parent;
|
540 |
|
541 | var fix = fixOffset(treeCat),
|
542 | userX = group.labelOffsets.x,
|
543 | userY = group.labelOffsets.y;
|
544 |
|
545 | minPos = tickPosition(tick, mathMax(group.startAt - 1, min - 1));
|
546 | maxPos = tickPosition(tick, mathMin(group.startAt + group.leaves - 1 - fix, max));
|
547 | bBox = group.label.getBBox(true);
|
548 | lvlSize = axis.groupSize(depth);
|
549 |
|
550 | reverseCrisp = ((horiz && maxPos.x === axis.pos + axis.len) || (!horiz && maxPos.y === axis.pos)) ? -1 : 0;
|
551 |
|
552 | attrs = horiz ? {
|
553 | x: (minPos.x + maxPos.x) / 2 + userX,
|
554 | y: size + axis.groupFontHeights[depth] + lvlSize / 2 + userY / 2
|
555 | } : {
|
556 | x: size + lvlSize / 2 + userX,
|
557 | y: (minPos.y + maxPos.y - bBox.height) / 2 + baseLine + userY
|
558 | };
|
559 |
|
560 | if (!isNaN(attrs.x) && !isNaN(attrs.y)) {
|
561 | group.label.attr(attrs);
|
562 |
|
563 | if (grid) {
|
564 | if (horiz && axis.left < maxPos.x) {
|
565 | addGridPart(grid, [maxPos.x - reverseCrisp, size, maxPos.x - reverseCrisp, size + lvlSize], tickWidth);
|
566 | } else if (!horiz && axis.top <= maxPos.y) {
|
567 | addGridPart(grid, [size, maxPos.y + reverseCrisp, size + lvlSize, maxPos.y + reverseCrisp], tickWidth);
|
568 | }
|
569 | }
|
570 | }
|
571 |
|
572 | size += lvlSize;
|
573 | depth++;
|
574 | }
|
575 | };
|
576 |
|
577 | tickProto.destroy = function () {
|
578 | var group = this.parent;
|
579 |
|
580 | while (group) {
|
581 | group.destroyed = group.destroyed ? (group.destroyed + 1) : 1;
|
582 | group = group.parent;
|
583 | }
|
584 |
|
585 | protoTickDestroy.call(this);
|
586 | };
|
587 |
|
588 |
|
589 | tickProto.getLabelSize = function () {
|
590 | if (this.axis.isGrouped === true) {
|
591 |
|
592 | var size = protoTickGetLabelSize.call(this) + 10,
|
593 | topLabelSize = this.axis.labelsSizes[0];
|
594 | if (topLabelSize < size) {
|
595 | this.axis.labelsSizes[0] = size;
|
596 | }
|
597 | return sum(this.axis.labelsSizes);
|
598 | }
|
599 | return protoTickGetLabelSize.call(this);
|
600 | };
|
601 |
|
602 |
|
603 |
|
604 | HC.wrap(HC.Tick.prototype, 'replaceMovedLabel', function (proceed) {
|
605 | if (!this.axis.isGrouped) {
|
606 | proceed.apply(this, Array.prototype.slice.call(arguments, 1));
|
607 | }
|
608 | });
|
609 |
|
610 | }));
|