3b4c38a0e2183008a36cd4df1395d63b8039d5a3.svn-base 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565
  1. /**
  2. * Grouped Categories v1.0.9 (2015-09-02)
  3. *
  4. * (c) 2012-2015 Black Label
  5. *
  6. * License: Creative Commons Attribution (CC)
  7. */
  8. (function(HC, HA){
  9. /*jshint expr:true, boss:true */
  10. var UNDEFINED = void 0,
  11. mathRound = Math.round,
  12. mathMin = Math.min,
  13. mathMax = Math.max,
  14. merge = HC.merge,
  15. pick = HC.pick,
  16. // cache prototypes
  17. axisProto = HC.Axis.prototype,
  18. tickProto = HC.Tick.prototype,
  19. // cache original methods
  20. _axisInit = axisProto.init,
  21. _axisRender = axisProto.render,
  22. _axisSetCategories = axisProto.setCategories,
  23. _tickGetLabelSize = tickProto.getLabelSize,
  24. _tickAddLabel = tickProto.addLabel,
  25. _tickDestroy = tickProto.destroy,
  26. _tickRender = tickProto.render;
  27. function Category(obj, parent) {
  28. this.userOptions = deepClone(obj);
  29. this.name = obj.name || obj;
  30. this.parent = parent;
  31. return this;
  32. }
  33. Category.prototype.toString = function () {
  34. var parts = [],
  35. cat = this;
  36. while (cat) {
  37. parts.push(cat.name);
  38. cat = cat.parent;
  39. }
  40. return parts.join(', ');
  41. };
  42. // Highcharts methods
  43. function defined(obj) {
  44. return obj !== UNDEFINED && obj !== null;
  45. }
  46. // calls parseInt with radix = 10, adds 0.5 to avoid blur
  47. function pInt(n) {
  48. return parseInt(n, 10) - 0.5;
  49. }
  50. // returns sum of an array
  51. function sum(arr) {
  52. var l = arr.length,
  53. x = 0;
  54. while (l--)
  55. x += arr[l];
  56. return x;
  57. }
  58. // Builds reverse category tree
  59. function buildTree(cats, out, options, parent, depth) {
  60. var len = cats.length,
  61. cat;
  62. depth || (depth = 0);
  63. options.depth || (options.depth = 0);
  64. while (len--) {
  65. cat = cats[len];
  66. if (parent)
  67. cat.parent = parent;
  68. if (cat.categories)
  69. buildTree(cat.categories, out, options, cat, depth + 1);
  70. else
  71. addLeaf(out, cat, parent);
  72. }
  73. options.depth = mathMax(options.depth, depth);
  74. }
  75. // Adds category leaf to array
  76. function addLeaf(out, cat, parent) {
  77. out.unshift(new Category(cat, parent));
  78. while (parent) {
  79. parent.leaves++ || (parent.leaves = 1);
  80. parent = parent.parent;
  81. }
  82. }
  83. // Pushes part of grid to path
  84. function addGridPart(path, d) {
  85. path.push(
  86. 'M',
  87. pInt(d[0]), pInt(d[1]),
  88. 'L',
  89. pInt(d[2]), pInt(d[3])
  90. );
  91. }
  92. // Destroys category groups
  93. function cleanCategory(category) {
  94. var tmp;
  95. while (category) {
  96. tmp = category.parent;
  97. if (category.label)
  98. category.label.destroy();
  99. delete category.parent;
  100. delete category.label;
  101. category = tmp;
  102. }
  103. }
  104. // Returns tick position
  105. function tickPosition(tick, pos) {
  106. return tick.getPosition(tick.axis.horiz, pos, tick.axis.tickmarkOffset);
  107. }
  108. function walk(arr, key, fn) {
  109. var l = arr.length,
  110. children;
  111. while (l--) {
  112. children = arr[l][key];
  113. if (children)
  114. walk(children, key, fn);
  115. fn(arr[l]);
  116. }
  117. }
  118. function deepClone(thing) {
  119. return JSON.parse(JSON.stringify(thing));
  120. }
  121. //
  122. // Axis prototype
  123. //
  124. axisProto.init = function (chart, options) {
  125. // default behaviour
  126. _axisInit.call(this, chart, options);
  127. if (typeof options === 'object' && options.categories)
  128. this.setupGroups(options);
  129. };
  130. // setup required axis options
  131. axisProto.setupGroups = function (options) {
  132. var categories,
  133. reverseTree = [],
  134. stats = {};
  135. categories = deepClone(options.categories);;
  136. // build categories tree
  137. buildTree(categories, reverseTree, stats);
  138. // set axis properties
  139. this.categoriesTree = categories;
  140. this.categories = reverseTree;
  141. this.isGrouped = stats.depth !== 0;
  142. this.labelsDepth = stats.depth;
  143. this.labelsSizes = [];
  144. this.labelsGridPath = [];
  145. this.tickLength = options.tickLength || this.tickLength || null;
  146. this.directionFactor = [-1, 1, 1, -1][this.side];
  147. this.options.lineWidth = options.lineWidth || 1;
  148. };
  149. axisProto.render = function () {
  150. // clear grid path
  151. if (this.isGrouped)
  152. this.labelsGridPath = [];
  153. // cache original tick length
  154. if (this.originalTickLength === UNDEFINED)
  155. this.originalTickLength = this.options.tickLength;
  156. // use default tickLength for not-grouped axis
  157. // and generate grid on grouped axes,
  158. // use tiny number to force highcharts to hide tick
  159. this.options.tickLength = this.isGrouped ? 0.001 : this.originalTickLength;
  160. _axisRender.call(this);
  161. if (!this.isGrouped) {
  162. if (this.labelsGrid)
  163. this.labelsGrid.attr({visibility: 'hidden'});
  164. return;
  165. }
  166. var axis = this,
  167. options = axis.options,
  168. top = axis.top,
  169. left = axis.left,
  170. right = left + axis.width,
  171. bottom = top + axis.height,
  172. visible = axis.hasVisibleSeries || axis.hasData,
  173. depth = axis.labelsDepth,
  174. grid = axis.labelsGrid,
  175. horiz = axis.horiz,
  176. d = axis.labelsGridPath,
  177. i = options.drawHorizontalBorders === false ? depth+1 : 0,
  178. offset = axis.opposite ? (horiz ? top : right) : (horiz ? bottom : left),
  179. part,
  180. tickWidth;
  181. if (axis.userTickLength)
  182. depth -= 1;
  183. // render grid path for the first time
  184. if (!grid) {
  185. // #66: tickWidth for x axis defaults to 1, for y to 0
  186. tickWidth = pick(options.tickWidth, axis.isXAxis ? 1 : 0);
  187. grid = axis.labelsGrid = axis.chart.renderer.path()
  188. .attr({
  189. // #58: use tickWidth/tickColor instead of lineWidth/lineColor:
  190. strokeWidth: tickWidth, // < 4.0.3
  191. 'stroke-width': tickWidth, // 4.0.3+ #30
  192. stroke: options.tickColor
  193. })
  194. .add(axis.axisGroup);
  195. }
  196. // go through every level and draw horizontal grid line
  197. while (i <= depth) {
  198. offset += axis.groupSize(i);
  199. part = horiz ?
  200. [left, offset, right, offset] :
  201. [offset, top, offset, bottom];
  202. addGridPart(d, part);
  203. i++;
  204. }
  205. // draw grid path
  206. grid.attr({
  207. d: d,
  208. visibility: visible ? 'visible' : 'hidden'
  209. });
  210. axis.labelGroup.attr({
  211. visibility: visible ? 'visible' : 'hidden'
  212. });
  213. walk(axis.categoriesTree, 'categories', function (group) {
  214. var tick = group.tick;
  215. if (!tick)
  216. return;
  217. if (tick.startAt + tick.leaves - 1 < axis.min || tick.startAt > axis.max) {
  218. tick.label.hide();
  219. tick.destroyed = 0;
  220. }
  221. else
  222. tick.label.attr({
  223. visibility: visible ? 'visible' : 'hidden'
  224. });
  225. });
  226. };
  227. axisProto.setCategories = function (newCategories, doRedraw) {
  228. if (this.categories)
  229. this.cleanGroups();
  230. this.setupGroups({
  231. categories: newCategories
  232. });
  233. this.categories = this.userOptions.categories = newCategories;
  234. _axisSetCategories.call(this, this.categories, doRedraw);
  235. };
  236. // cleans old categories
  237. axisProto.cleanGroups = function () {
  238. var ticks = this.ticks,
  239. n;
  240. for (n in ticks)
  241. if (ticks[n].parent)
  242. delete ticks[n].parent;
  243. walk(this.categoriesTree, 'categories', function (group) {
  244. var tick = group.tick,
  245. n;
  246. if (!tick)
  247. return;
  248. tick.label.destroy();
  249. for (n in tick)
  250. delete tick[n];
  251. delete group.tick;
  252. });
  253. this.labelsGrid = null;
  254. };
  255. // keeps size of each categories level
  256. axisProto.groupSize = function (level, position) {
  257. var positions = this.labelsSizes,
  258. direction = this.directionFactor,
  259. groupedOptions = this.options.labels.groupedOptions ? this.options.labels.groupedOptions[level-1] : false,
  260. userXY = 0;
  261. if(groupedOptions) {
  262. if(direction == -1) {
  263. userXY = groupedOptions.x ? groupedOptions.x : 0;
  264. } else {
  265. userXY = groupedOptions.y ? groupedOptions.y : 0;
  266. }
  267. }
  268. if (position !== UNDEFINED)
  269. positions[level] = mathMax(positions[level] || 0, position + 10 + Math.abs(userXY)) ;
  270. if (level === true)
  271. return sum(positions) * direction;
  272. else if (positions[level])
  273. return positions[level] * direction;
  274. return 0;
  275. };
  276. //
  277. // Tick prototype
  278. //
  279. // Override methods prototypes
  280. tickProto.addLabel = function () {
  281. var category;
  282. _tickAddLabel.call(this);
  283. if (!this.axis.categories ||
  284. !(category = this.axis.categories[this.pos]))
  285. return;
  286. // set label text - but applied after formatter #46
  287. if (category.name && this.label)
  288. this.label.attr('text', this.axis.labelFormatter.call({
  289. axis: this.axis,
  290. chart: this.axis.chart,
  291. isFirst: this.isFirst,
  292. isLast: this.isLast,
  293. value: category.name
  294. }));
  295. // create elements for parent categories
  296. if (this.axis.isGrouped && this.axis.options.labels.enabled)
  297. this.addGroupedLabels(category);
  298. };
  299. // render ancestor label
  300. tickProto.addGroupedLabels = function (category) {
  301. var tick = this,
  302. axis = this.axis,
  303. chart = axis.chart,
  304. options = axis.options.labels,
  305. useHTML = options.useHTML,
  306. css = options.style,
  307. userAttr= options.groupedOptions,
  308. attr = { align: 'center' , rotation: options.rotation, x: 0, y: 0 },
  309. size = axis.horiz ? 'height' : 'width',
  310. depth = 0,
  311. label;
  312. while (tick) {
  313. if (depth > 0 && !category.tick) {
  314. // render label element
  315. this.value = category.name;
  316. var name = options.formatter ? options.formatter.call(this, category) : category.name,
  317. hasOptions = userAttr && userAttr[depth-1],
  318. mergedAttrs = hasOptions ? merge(attr, userAttr[depth-1] ) : attr,
  319. mergedCSS = hasOptions && userAttr[depth-1].style ? merge(css, userAttr[depth-1].style ) : css;
  320. label = chart.renderer.text(name, 0, 0, useHTML)
  321. .attr(mergedAttrs)
  322. .css(mergedCSS)
  323. .add(axis.labelGroup);
  324. // tick properties
  325. tick.startAt = this.pos;
  326. tick.childCount = category.categories.length;
  327. tick.leaves = category.leaves;
  328. tick.visible = this.childCount;
  329. tick.label = label;
  330. tick.labelOffsets = {
  331. x: mergedAttrs.x,
  332. y: mergedAttrs.y
  333. };
  334. // link tick with category
  335. category.tick = tick;
  336. }
  337. // set level size
  338. axis.groupSize(depth, tick.label.getBBox()[size]);
  339. // go up to the parent category
  340. category = category.parent;
  341. if (category)
  342. tick = tick.parent = category.tick || {};
  343. else
  344. tick = null;
  345. depth++;
  346. }
  347. };
  348. // set labels position & render categories grid
  349. tickProto.render = function (index, old, opacity) {
  350. _tickRender.call(this, index, old, opacity);
  351. var treeCat = this.axis.categories[this.pos];
  352. if (!this.axis.isGrouped || !treeCat || this.pos > this.axis.max)
  353. return;
  354. var tick = this,
  355. group = tick,
  356. axis = tick.axis,
  357. tickPos = tick.pos,
  358. isFirst = tick.isFirst,
  359. max = axis.max,
  360. min = axis.min,
  361. horiz = axis.horiz,
  362. cat = axis.categories[tickPos],
  363. grid = axis.labelsGridPath,
  364. size = axis.groupSize(0),
  365. tickLen = axis.tickLength || size,
  366. factor = axis.directionFactor,
  367. xy = tickPosition(tick, tickPos),
  368. start = horiz ? xy.y : xy.x,
  369. baseLine= axis.chart.renderer.fontMetrics(axis.options.labels.style.fontSize).b,
  370. depth = 1,
  371. gridAttrs,
  372. lvlSize,
  373. minPos,
  374. maxPos,
  375. attrs,
  376. bBox;
  377. // render grid for "normal" categories (first-level), render left grid line only for the first category
  378. if (isFirst) {
  379. gridAttrs = horiz ?
  380. [axis.left, xy.y, axis.left, xy.y + axis.groupSize(true)] :
  381. axis.isXAxis ?
  382. [xy.x, axis.top, xy.x + axis.groupSize(true), axis.top] :
  383. [xy.x, axis.top + axis.len, xy.x + axis.groupSize(true), axis.top + axis.len];
  384. addGridPart(grid, gridAttrs);
  385. }
  386. if(horiz && axis.left < xy.x) {
  387. addGridPart(grid, [xy.x, xy.y, xy.x, xy.y + size]);
  388. } else if(!horiz && axis.top < xy.y){
  389. addGridPart(grid, [xy.x, xy.y, xy.x + size, xy.y]);
  390. }
  391. size = start + size;
  392. function fixOffset(group, treeCat, tick){
  393. var ret = 0;
  394. if(isFirst) {
  395. ret = HA.inArray(treeCat.name, treeCat.parent.categories);
  396. ret = ret < 0 ? 0 : ret;
  397. return ret;
  398. }
  399. return ret;
  400. }
  401. while (group = group.parent) {
  402. var fix = fixOffset(group, treeCat, tick),
  403. userX = group.labelOffsets.x,
  404. userY = group.labelOffsets.y;
  405. minPos = tickPosition(tick, mathMax(group.startAt - 1, min - 1));
  406. maxPos = tickPosition(tick, mathMin(group.startAt + group.leaves - 1 - fix, max));
  407. bBox = group.label.getBBox(true);
  408. lvlSize = axis.groupSize(depth);
  409. attrs = horiz ? {
  410. x: (minPos.x + maxPos.x) / 2 + userX,
  411. y: size + lvlSize / 2 + baseLine - bBox.height / 2 - 4 + userY / 2
  412. } : {
  413. x: size + lvlSize / 2 + userX,
  414. y: (minPos.y + maxPos.y - bBox.height) / 2 + baseLine + userY
  415. };
  416. if(!isNaN(attrs.x) && !isNaN(attrs.y)){
  417. group.label.attr(attrs);
  418. if (grid) {
  419. if(horiz && axis.left < maxPos.x) {
  420. addGridPart(grid, [maxPos.x, size, maxPos.x, size + lvlSize]);
  421. } else if(!horiz && axis.top < maxPos.y){
  422. addGridPart(grid, [size, maxPos.y, size + lvlSize, maxPos.y]);
  423. }
  424. }
  425. }
  426. size += lvlSize;
  427. depth++;
  428. }
  429. };
  430. tickProto.destroy = function () {
  431. var group = this;
  432. while (group = group.parent)
  433. group.destroyed++ || (group.destroyed = 1);
  434. _tickDestroy.call(this);
  435. };
  436. // return size of the label (height for horizontal, width for vertical axes)
  437. tickProto.getLabelSize = function () {
  438. if (this.axis.isGrouped === true)
  439. return sum(this.axis.labelsSizes);
  440. else
  441. return _tickGetLabelSize.call(this);
  442. };
  443. }(Highcharts, HighchartsAdapter));