d3pie-0.2.1-netdata-3.js 78 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124
  1. /*!
  2. * d3pie
  3. * @author Ben Keen
  4. * @version 0.1.9
  5. * @date June 17th, 2015
  6. * @repo http://github.com/benkeen/d3pie
  7. * SPDX-License-Identifier: MIT
  8. */
  9. // UMD pattern from https://github.com/umdjs/umd/blob/master/returnExports.js
  10. (function(root, factory) {
  11. if (typeof define === 'function' && define.amd) {
  12. // AMD. Register as an anonymous module
  13. define([], factory);
  14. } else if (typeof exports === 'object') {
  15. // Node. Does not work with strict CommonJS, but only CommonJS-like environments that support module.exports,
  16. // like Node
  17. module.exports = factory();
  18. } else {
  19. // browser globals (root is window)
  20. root.d3pie = factory(root);
  21. }
  22. }(this, function() {
  23. var _scriptName = "d3pie";
  24. var _version = "0.2.1";
  25. // used to uniquely generate IDs and classes, ensuring no conflict between multiple pies on the same page
  26. var _uniqueIDCounter = 0;
  27. // this section includes all helper libs on the d3pie object. They're populated via grunt-template. Note: to keep
  28. // the syntax highlighting from getting all messed up, I commented out each line. That REQUIRES each of the files
  29. // to have an empty first line. Crumby, yes, but acceptable.
  30. //// --------- _default-settings.js -----------/**
  31. /**
  32. * Contains the out-the-box settings for the script. Any of these settings that aren't explicitly overridden for the
  33. * d3pie instance will inherit from these. This is also included on the main website for use in the generation script.
  34. */
  35. var defaultSettings = {
  36. header: {
  37. title: {
  38. text: "",
  39. color: "#333333",
  40. fontSize: 18,
  41. fontWeight: "bold",
  42. font: "arial"
  43. },
  44. subtitle: {
  45. text: "",
  46. color: "#666666",
  47. fontSize: 14,
  48. fontWeight: "bold",
  49. font: "arial"
  50. },
  51. location: "top-center",
  52. titleSubtitlePadding: 8
  53. },
  54. footer: {
  55. text: "",
  56. color: "#666666",
  57. fontSize: 14,
  58. fontWeight: "bold",
  59. font: "arial",
  60. location: "left"
  61. },
  62. size: {
  63. canvasHeight: 500,
  64. canvasWidth: 500,
  65. pieInnerRadius: "0%",
  66. pieOuterRadius: null
  67. },
  68. data: {
  69. sortOrder: "none",
  70. ignoreSmallSegments: {
  71. enabled: false,
  72. valueType: "percentage",
  73. value: null
  74. },
  75. smallSegmentGrouping: {
  76. enabled: false,
  77. value: 1,
  78. valueType: "percentage",
  79. label: "Other",
  80. color: "#cccccc"
  81. },
  82. content: []
  83. },
  84. labels: {
  85. outer: {
  86. format: "label",
  87. hideWhenLessThanPercentage: null,
  88. pieDistance: 30
  89. },
  90. inner: {
  91. format: "percentage",
  92. hideWhenLessThanPercentage: null
  93. },
  94. mainLabel: {
  95. color: "#333333",
  96. font: "arial",
  97. fontWeight: "normal",
  98. fontSize: 10
  99. },
  100. percentage: {
  101. color: "#dddddd",
  102. font: "arial",
  103. fontWeight: "bold",
  104. fontSize: 10,
  105. decimalPlaces: 0
  106. },
  107. value: {
  108. color: "#cccc44",
  109. fontWeight: "bold",
  110. font: "arial",
  111. fontSize: 10
  112. },
  113. lines: {
  114. enabled: true,
  115. style: "curved",
  116. color: "segment"
  117. },
  118. truncation: {
  119. enabled: false,
  120. truncateLength: 30
  121. },
  122. formatter: null
  123. },
  124. effects: {
  125. load: {
  126. effect: "none", // "default", commented in the code
  127. speed: 1000
  128. },
  129. pullOutSegmentOnClick: {
  130. effect: "none", // "bounce", commented in the code
  131. speed: 300,
  132. size: 10
  133. },
  134. highlightSegmentOnMouseover: false,
  135. highlightLuminosity: -0.2
  136. },
  137. tooltips: {
  138. enabled: false,
  139. type: "placeholder", // caption|placeholder
  140. string: "",
  141. placeholderParser: null,
  142. styles: {
  143. fadeInSpeed: 250,
  144. backgroundColor: "#000000",
  145. backgroundOpacity: 0.5,
  146. color: "#efefef",
  147. borderRadius: 2,
  148. font: "arial",
  149. fontWeight: "bold",
  150. fontSize: 10,
  151. padding: 4
  152. }
  153. },
  154. misc: {
  155. colors: {
  156. background: null,
  157. segments: [
  158. "#2484c1", "#65a620", "#7b6888", "#a05d56", "#961a1a", "#d8d23a", "#e98125", "#d0743c", "#635222", "#6ada6a",
  159. "#0c6197", "#7d9058", "#207f33", "#44b9b0", "#bca44a", "#e4a14b", "#a3acb2", "#8cc3e9", "#69a6f9", "#5b388f",
  160. "#546e91", "#8bde95", "#d2ab58", "#273c71", "#98bf6e", "#4daa4b", "#98abc5", "#cc1010", "#31383b", "#006391",
  161. "#c2643f", "#b0a474", "#a5a39c", "#a9c2bc", "#22af8c", "#7fcecf", "#987ac6", "#3d3b87", "#b77b1c", "#c9c2b6",
  162. "#807ece", "#8db27c", "#be66a2", "#9ed3c6", "#00644b", "#005064", "#77979f", "#77e079", "#9c73ab", "#1f79a7"
  163. ],
  164. segmentStroke: "#ffffff"
  165. },
  166. gradient: {
  167. enabled: false,
  168. percentage: 95,
  169. color: "#000000"
  170. },
  171. canvasPadding: {
  172. top: 5,
  173. right: 5,
  174. bottom: 5,
  175. left: 5
  176. },
  177. pieCenterOffset: {
  178. x: 0,
  179. y: 0
  180. },
  181. cssPrefix: null
  182. },
  183. callbacks: {
  184. onload: null,
  185. onMouseoverSegment: null,
  186. onMouseoutSegment: null,
  187. onClickSegment: null
  188. }
  189. };
  190. //// --------- validate.js -----------
  191. var validate = {
  192. // called whenever a new pie chart is created
  193. initialCheck: function(pie) {
  194. var cssPrefix = pie.cssPrefix;
  195. var element = pie.element;
  196. var options = pie.options;
  197. // confirm d3 is available [check minimum version]
  198. if (!window.d3 || !window.d3.hasOwnProperty("version")) {
  199. console.error("d3pie error: d3 is not available");
  200. return false;
  201. }
  202. // confirm element is either a DOM element or a valid string for a DOM element
  203. if (!(element instanceof HTMLElement || element instanceof SVGElement)) {
  204. console.error("d3pie error: the first d3pie() param must be a valid DOM element (not jQuery) or a ID string.");
  205. return false;
  206. }
  207. // confirm the CSS prefix is valid. It has to start with a-Z and contain nothing but a-Z0-9_-
  208. if (!(/[a-zA-Z][a-zA-Z0-9_-]*$/.test(cssPrefix))) {
  209. console.error("d3pie error: invalid options.misc.cssPrefix");
  210. return false;
  211. }
  212. // confirm some data has been supplied
  213. if (!helpers.isArray(options.data.content)) {
  214. console.error("d3pie error: invalid config structure: missing data.content property.");
  215. return false;
  216. }
  217. if (options.data.content.length === 0) {
  218. console.error("d3pie error: no data supplied.");
  219. return false;
  220. }
  221. // clear out any invalid data. Each data row needs a valid positive number and a label
  222. var data = [];
  223. for (var i=0; i<options.data.content.length; i++) {
  224. if (typeof options.data.content[i].value !== "number" || isNaN(options.data.content[i].value)) {
  225. console.log("not valid: ", options.data.content[i]);
  226. continue;
  227. }
  228. if (options.data.content[i].value <= 0) {
  229. console.log("not valid - should have positive value: ", options.data.content[i]);
  230. continue;
  231. }
  232. data.push(options.data.content[i]);
  233. }
  234. pie.options.data.content = data;
  235. // labels.outer.hideWhenLessThanPercentage - 1-100
  236. // labels.inner.hideWhenLessThanPercentage - 1-100
  237. return true;
  238. }
  239. };
  240. //// --------- helpers.js -----------
  241. var helpers = {
  242. // creates the SVG element
  243. addSVGSpace: function(pie) {
  244. var element = pie.element;
  245. var canvasWidth = pie.options.size.canvasWidth;
  246. var canvasHeight = pie.options.size.canvasHeight;
  247. var backgroundColor = pie.options.misc.colors.background;
  248. var svg = d3.select(element).append("svg:svg")
  249. .attr("width", canvasWidth)
  250. .attr("height", canvasHeight);
  251. if (backgroundColor !== "transparent") {
  252. svg.style("background-color", function() { return backgroundColor; });
  253. }
  254. return svg;
  255. },
  256. shuffleArray: function(array) {
  257. var currentIndex = array.length, tmpVal, randomIndex;
  258. while (0 !== currentIndex) {
  259. randomIndex = Math.floor(Math.random() * currentIndex);
  260. currentIndex -= 1;
  261. // and swap it with the current element
  262. tmpVal = array[currentIndex];
  263. array[currentIndex] = array[randomIndex];
  264. array[randomIndex] = tmpVal;
  265. }
  266. return array;
  267. },
  268. processObj: function(obj, is, value) {
  269. if (typeof is === 'string') {
  270. return helpers.processObj(obj, is.split('.'), value);
  271. } else if (is.length === 1 && value !== undefined) {
  272. obj[is[0]] = value;
  273. return obj[is[0]];
  274. } else if (is.length === 0) {
  275. return obj;
  276. } else {
  277. return helpers.processObj(obj[is[0]], is.slice(1), value);
  278. }
  279. },
  280. getDimensions: function(el) {
  281. if(typeof el === 'string')
  282. el = document.getElementById(el);
  283. var w = 0, h = 0;
  284. if (el) {
  285. var dimensions = el.getBBox();
  286. w = dimensions.width;
  287. h = dimensions.height;
  288. }
  289. else {
  290. console.log("error: getDimensions() " + id + " not found.");
  291. }
  292. return { w: w, h: h };
  293. },
  294. /**
  295. * This is based on the SVG coordinate system, where top-left is 0,0 and bottom right is n-n.
  296. * @param r1
  297. * @param r2
  298. * @returns {boolean}
  299. */
  300. rectIntersect: function(r1, r2) {
  301. var returnVal = (
  302. // r2.left > r1.right
  303. (r2.x > (r1.x + r1.w)) ||
  304. // r2.right < r1.left
  305. ((r2.x + r2.w) < r1.x) ||
  306. // r2.top < r1.bottom
  307. ((r2.y + r2.h) < r1.y) ||
  308. // r2.bottom > r1.top
  309. (r2.y > (r1.y + r1.h))
  310. );
  311. return !returnVal;
  312. },
  313. /**
  314. * Returns a lighter/darker shade of a hex value, based on a luminance value passed.
  315. * @param hex a hex color value such as “#abc” or “#123456″ (the hash is optional)
  316. * @param lum the luminosity factor: -0.1 is 10% darker, 0.2 is 20% lighter, etc.
  317. * @returns {string}
  318. */
  319. getColorShade: function(hex, lum) {
  320. // validate hex string
  321. hex = String(hex).replace(/[^0-9a-f]/gi, '');
  322. if (hex.length < 6) {
  323. hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];
  324. }
  325. lum = lum || 0;
  326. // convert to decimal and change luminosity
  327. var newHex = "#";
  328. for (var i=0; i<3; i++) {
  329. var c = parseInt(hex.substr(i * 2, 2), 16);
  330. c = Math.round(Math.min(Math.max(0, c + (c * lum)), 255)).toString(16);
  331. newHex += ("00" + c).substr(c.length);
  332. }
  333. return newHex;
  334. },
  335. /**
  336. * Users can choose to specify segment colors in three ways (in order of precedence):
  337. * 1. include a "color" attribute for each row in data.content
  338. * 2. include a misc.colors.segments property which contains an array of hex codes
  339. * 3. specify nothing at all and rely on this lib provide some reasonable defaults
  340. *
  341. * This function sees what's included and populates this.options.colors with whatever's required
  342. * for this pie chart.
  343. * @param data
  344. */
  345. initSegmentColors: function(pie) {
  346. var data = pie.options.data.content;
  347. var colors = pie.options.misc.colors.segments;
  348. // TODO this needs a ton of error handling
  349. var finalColors = [];
  350. for (var i=0; i<data.length; i++) {
  351. if (data[i].hasOwnProperty("color")) {
  352. finalColors.push(data[i].color);
  353. } else {
  354. finalColors.push(colors[i]);
  355. }
  356. }
  357. return finalColors;
  358. },
  359. applySmallSegmentGrouping: function(data, smallSegmentGrouping) {
  360. var totalSize;
  361. if (smallSegmentGrouping.valueType === "percentage") {
  362. totalSize = math.getTotalPieSize(data);
  363. }
  364. // loop through each data item
  365. var newData = [];
  366. var groupedData = [];
  367. var totalGroupedData = 0;
  368. for (var i=0; i<data.length; i++) {
  369. if (smallSegmentGrouping.valueType === "percentage") {
  370. var dataPercent = (data[i].value / totalSize) * 100;
  371. if (dataPercent <= smallSegmentGrouping.value) {
  372. groupedData.push(data[i]);
  373. totalGroupedData += data[i].value;
  374. continue;
  375. }
  376. data[i].isGrouped = false;
  377. newData.push(data[i]);
  378. } else {
  379. if (data[i].value <= smallSegmentGrouping.value) {
  380. groupedData.push(data[i]);
  381. totalGroupedData += data[i].value;
  382. continue;
  383. }
  384. data[i].isGrouped = false;
  385. newData.push(data[i]);
  386. }
  387. }
  388. // we're done! See if there's any small segment groups to add
  389. if (groupedData.length) {
  390. newData.push({
  391. color: smallSegmentGrouping.color,
  392. label: smallSegmentGrouping.label,
  393. value: totalGroupedData,
  394. isGrouped: true,
  395. groupedData: groupedData
  396. });
  397. }
  398. return newData;
  399. },
  400. // for debugging
  401. showPoint: function(svg, x, y) {
  402. svg.append("circle").attr("cx", x).attr("cy", y).attr("r", 2).style("fill", "black");
  403. },
  404. isFunction: function(functionToCheck) {
  405. var getType = {};
  406. return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
  407. },
  408. isArray: function(o) {
  409. return Object.prototype.toString.call(o) === '[object Array]';
  410. }
  411. };
  412. // taken from jQuery
  413. var extend = function() {
  414. var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {},
  415. i = 1,
  416. length = arguments.length,
  417. deep = false,
  418. toString = Object.prototype.toString,
  419. hasOwn = Object.prototype.hasOwnProperty,
  420. class2type = {
  421. "[object Boolean]": "boolean",
  422. "[object Number]": "number",
  423. "[object String]": "string",
  424. "[object Function]": "function",
  425. "[object Array]": "array",
  426. "[object Date]": "date",
  427. "[object RegExp]": "regexp",
  428. "[object Object]": "object"
  429. },
  430. jQuery = {
  431. isFunction: function (obj) {
  432. return jQuery.type(obj) === "function";
  433. },
  434. isArray: Array.isArray ||
  435. function (obj) {
  436. return jQuery.type(obj) === "array";
  437. },
  438. isWindow: function (obj) {
  439. return obj !== null && obj === obj.window;
  440. },
  441. isNumeric: function (obj) {
  442. return !isNaN(parseFloat(obj)) && isFinite(obj);
  443. },
  444. type: function (obj) {
  445. return obj === null ? String(obj) : class2type[toString.call(obj)] || "object";
  446. },
  447. isPlainObject: function (obj) {
  448. if (!obj || jQuery.type(obj) !== "object" || obj.nodeType) {
  449. return false;
  450. }
  451. try {
  452. if (obj.constructor && !hasOwn.call(obj, "constructor") && !hasOwn.call(obj.constructor.prototype, "isPrototypeOf")) {
  453. return false;
  454. }
  455. } catch (e) {
  456. return false;
  457. }
  458. var key;
  459. for (key in obj) {}
  460. return key === undefined || hasOwn.call(obj, key);
  461. }
  462. };
  463. if (typeof target === "boolean") {
  464. deep = target;
  465. target = arguments[1] || {};
  466. i = 2;
  467. }
  468. if (typeof target !== "object" && !jQuery.isFunction(target)) {
  469. target = {};
  470. }
  471. if (length === i) {
  472. target = this;
  473. --i;
  474. }
  475. for (i; i < length; i++) {
  476. if ((options = arguments[i]) !== null) {
  477. for (name in options) {
  478. src = target[name];
  479. copy = options[name];
  480. if (target === copy) {
  481. continue;
  482. }
  483. if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) {
  484. if (copyIsArray) {
  485. copyIsArray = false;
  486. clone = src && jQuery.isArray(src) ? src : [];
  487. } else {
  488. clone = src && jQuery.isPlainObject(src) ? src : {};
  489. }
  490. // WARNING: RECURSION
  491. target[name] = extend(deep, clone, copy);
  492. } else if (copy !== undefined) {
  493. target[name] = copy;
  494. }
  495. }
  496. }
  497. }
  498. return target;
  499. };
  500. //// --------- math.js -----------
  501. var math = {
  502. toRadians: function(degrees) {
  503. return degrees * (Math.PI / 180);
  504. },
  505. toDegrees: function(radians) {
  506. return radians * (180 / Math.PI);
  507. },
  508. computePieRadius: function(pie) {
  509. var size = pie.options.size;
  510. var canvasPadding = pie.options.misc.canvasPadding;
  511. // outer radius is either specified (e.g. through the generator), or omitted altogether
  512. // and calculated based on the canvas dimensions. Right now the estimated version isn't great - it should
  513. // be possible to calculate it to precisely generate the maximum sized pie, but it's fussy as heck. Something
  514. // for the next release.
  515. // first, calculate the default _outerRadius
  516. var w = size.canvasWidth - canvasPadding.left - canvasPadding.right;
  517. var h = size.canvasHeight - canvasPadding.top - canvasPadding.bottom;
  518. // now factor in the footer, title & subtitle
  519. if (pie.options.header.location !== "pie-center") {
  520. h -= pie.textComponents.headerHeight;
  521. }
  522. if (pie.textComponents.footer.exists) {
  523. h -= pie.textComponents.footer.h;
  524. }
  525. // for really teeny pies, h may be < 0. Adjust it back
  526. h = (h < 0) ? 0 : h;
  527. var outerRadius = ((w < h) ? w : h) / 3;
  528. var innerRadius, percent;
  529. // if the user specified something, use that instead
  530. if (size.pieOuterRadius !== null) {
  531. if (/%/.test(size.pieOuterRadius)) {
  532. percent = parseInt(size.pieOuterRadius.replace(/[\D]/, ""), 10);
  533. percent = (percent > 99) ? 99 : percent;
  534. percent = (percent < 0) ? 0 : percent;
  535. var smallestDimension = (w < h) ? w : h;
  536. // now factor in the label line size
  537. if (pie.options.labels.outer.format !== "none") {
  538. var pieDistanceSpace = parseInt(pie.options.labels.outer.pieDistance, 10) * 2;
  539. if (smallestDimension - pieDistanceSpace > 0) {
  540. smallestDimension -= pieDistanceSpace;
  541. }
  542. }
  543. outerRadius = Math.floor((smallestDimension / 100) * percent) / 2;
  544. } else {
  545. outerRadius = parseInt(size.pieOuterRadius, 10);
  546. }
  547. }
  548. // inner radius
  549. if (/%/.test(size.pieInnerRadius)) {
  550. percent = parseInt(size.pieInnerRadius.replace(/[\D]/, ""), 10);
  551. percent = (percent > 99) ? 99 : percent;
  552. percent = (percent < 0) ? 0 : percent;
  553. innerRadius = Math.floor((outerRadius / 100) * percent);
  554. } else {
  555. innerRadius = parseInt(size.pieInnerRadius, 10);
  556. }
  557. pie.innerRadius = innerRadius;
  558. pie.outerRadius = outerRadius;
  559. },
  560. getTotalPieSize: function(data) {
  561. var totalSize = 0;
  562. for (var i=0; i<data.length; i++) {
  563. totalSize += data[i].value;
  564. }
  565. return totalSize;
  566. },
  567. sortPieData: function(pie) {
  568. var data = pie.options.data.content;
  569. var sortOrder = pie.options.data.sortOrder;
  570. switch (sortOrder) {
  571. case "none":
  572. // do nothing
  573. break;
  574. case "random":
  575. data = helpers.shuffleArray(data);
  576. break;
  577. case "value-asc":
  578. data.sort(function(a, b) { return (a.value < b.value) ? -1 : 1; });
  579. break;
  580. case "value-desc":
  581. data.sort(function(a, b) { return (a.value < b.value) ? 1 : -1; });
  582. break;
  583. case "label-asc":
  584. data.sort(function(a, b) { return (a.label.toLowerCase() > b.label.toLowerCase()) ? 1 : -1; });
  585. break;
  586. case "label-desc":
  587. data.sort(function(a, b) { return (a.label.toLowerCase() < b.label.toLowerCase()) ? 1 : -1; });
  588. break;
  589. }
  590. return data;
  591. },
  592. // var pieCenter = math.getPieCenter();
  593. getPieTranslateCenter: function(pieCenter) {
  594. return "translate(" + pieCenter.x + "," + pieCenter.y + ")";
  595. },
  596. /**
  597. * Used to determine where on the canvas the center of the pie chart should be. It takes into account the
  598. * height and position of the title, subtitle and footer, and the various paddings.
  599. * @private
  600. */
  601. calculatePieCenter: function(pie) {
  602. var pieCenterOffset = pie.options.misc.pieCenterOffset;
  603. var hasTopTitle = (pie.textComponents.title.exists && pie.options.header.location !== "pie-center");
  604. var hasTopSubtitle = (pie.textComponents.subtitle.exists && pie.options.header.location !== "pie-center");
  605. var headerOffset = pie.options.misc.canvasPadding.top;
  606. if (hasTopTitle && hasTopSubtitle) {
  607. headerOffset += pie.textComponents.title.h + pie.options.header.titleSubtitlePadding + pie.textComponents.subtitle.h;
  608. } else if (hasTopTitle) {
  609. headerOffset += pie.textComponents.title.h;
  610. } else if (hasTopSubtitle) {
  611. headerOffset += pie.textComponents.subtitle.h;
  612. }
  613. var footerOffset = 0;
  614. if (pie.textComponents.footer.exists) {
  615. footerOffset = pie.textComponents.footer.h + pie.options.misc.canvasPadding.bottom;
  616. }
  617. var x = ((pie.options.size.canvasWidth - pie.options.misc.canvasPadding.left - pie.options.misc.canvasPadding.right) / 2) + pie.options.misc.canvasPadding.left;
  618. var y = ((pie.options.size.canvasHeight - footerOffset - headerOffset) / 2) + headerOffset;
  619. x += pieCenterOffset.x;
  620. y += pieCenterOffset.y;
  621. pie.pieCenter = { x: x, y: y };
  622. },
  623. /**
  624. * Rotates a point (x, y) around an axis (xm, ym) by degrees (a).
  625. * @param x
  626. * @param y
  627. * @param xm
  628. * @param ym
  629. * @param a angle in degrees
  630. * @returns {Array}
  631. */
  632. rotate: function(x, y, xm, ym, a) {
  633. a = a * Math.PI / 180; // convert to radians
  634. var cos = Math.cos,
  635. sin = Math.sin,
  636. // subtract midpoints, so that midpoint is translated to origin and add it in the end again
  637. xr = (x - xm) * cos(a) - (y - ym) * sin(a) + xm,
  638. yr = (x - xm) * sin(a) + (y - ym) * cos(a) + ym;
  639. return { x: xr, y: yr };
  640. },
  641. /**
  642. * Translates a point x, y by distance d, and by angle a.
  643. * @param x
  644. * @param y
  645. * @param dist
  646. * @param a angle in degrees
  647. */
  648. translate: function(x, y, d, a) {
  649. var rads = math.toRadians(a);
  650. return {
  651. x: x + d * Math.sin(rads),
  652. y: y - d * Math.cos(rads)
  653. };
  654. },
  655. // from: http://stackoverflow.com/questions/19792552/d3-put-arc-labels-in-a-pie-chart-if-there-is-enough-space
  656. pointIsInArc: function(pt, ptData, d3Arc) {
  657. // Center of the arc is assumed to be 0,0
  658. // (pt.x, pt.y) are assumed to be relative to the center
  659. var r1 = d3Arc.innerRadius()(ptData), // Note: Using the innerRadius
  660. r2 = d3Arc.outerRadius()(ptData),
  661. theta1 = d3Arc.startAngle()(ptData),
  662. theta2 = d3Arc.endAngle()(ptData);
  663. var dist = pt.x * pt.x + pt.y * pt.y,
  664. angle = Math.atan2(pt.x, -pt.y); // Note: different coordinate system
  665. angle = (angle < 0) ? (angle + Math.PI * 2) : angle;
  666. return (r1 * r1 <= dist) && (dist <= r2 * r2) &&
  667. (theta1 <= angle) && (angle <= theta2);
  668. }
  669. };
  670. //// --------- labels.js -----------
  671. var labels = {
  672. /**
  673. * Adds the labels to the pie chart, but doesn't position them. There are two locations for the
  674. * labels: inside (center) of the segments, or outside the segments on the edge.
  675. * @param section "inner" or "outer"
  676. * @param sectionDisplayType "percentage", "value", "label", "label-value1", etc.
  677. * @param pie
  678. */
  679. add: function(pie, section, sectionDisplayType) {
  680. var include = labels.getIncludes(sectionDisplayType);
  681. var settings = pie.options.labels;
  682. // group the label groups (label, percentage, value) into a single element for simpler positioning
  683. var outerLabel = pie.svg.insert("g", "." + pie.cssPrefix + "labels-" + section)
  684. .attr("class", pie.cssPrefix + "labels-" + section);
  685. var labelGroup = pie.__labels[section] = outerLabel.selectAll("." + pie.cssPrefix + "labelGroup-" + section)
  686. .data(pie.options.data.content)
  687. .enter()
  688. .append("g")
  689. .attr("id", function(d, i) { return pie.cssPrefix + "labelGroup" + i + "-" + section; })
  690. .attr("data-index", function(d, i) { return i; })
  691. .attr("class", pie.cssPrefix + "labelGroup-" + section)
  692. .style("opacity", 0);
  693. var formatterContext = { section: section, sectionDisplayType: sectionDisplayType };
  694. // 1. Add the main label
  695. if (include.mainLabel) {
  696. labelGroup.append("text")
  697. .attr("id", function(d, i) { return pie.cssPrefix + "segmentMainLabel" + i + "-" + section; })
  698. .attr("class", pie.cssPrefix + "segmentMainLabel-" + section)
  699. .text(function(d, i) {
  700. var str = d.label;
  701. // if a custom formatter has been defined, pass it the raw label string - it can do whatever it wants with it.
  702. // we only apply truncation if it's not defined
  703. if (settings.formatter) {
  704. formatterContext.index = i;
  705. formatterContext.part = 'mainLabel';
  706. formatterContext.value = d.value;
  707. formatterContext.label = str;
  708. str = settings.formatter(formatterContext);
  709. } else if (settings.truncation.enabled && d.label.length > settings.truncation.truncateLength) {
  710. str = d.label.substring(0, settings.truncation.truncateLength) + "...";
  711. }
  712. return str;
  713. })
  714. .style("font-size", settings.mainLabel.fontSize + "px")
  715. .style("font-family", settings.mainLabel.font)
  716. .style("font-weight", settings.mainLabel.fontWeight)
  717. .style("fill", function(d, i) {
  718. return (settings.mainLabel.color === "segment") ? pie.options.colors[i] : settings.mainLabel.color;
  719. });
  720. }
  721. // 2. Add the percentage label
  722. if (include.percentage) {
  723. labelGroup.append("text")
  724. .attr("id", function(d, i) { return pie.cssPrefix + "segmentPercentage" + i + "-" + section; })
  725. .attr("class", pie.cssPrefix + "segmentPercentage-" + section)
  726. .text(function(d, i) {
  727. var percentage = d.percentage;
  728. if (settings.formatter) {
  729. formatterContext.index = i;
  730. formatterContext.part = "percentage";
  731. formatterContext.value = d.value;
  732. formatterContext.label = d.percentage;
  733. percentage = settings.formatter(formatterContext);
  734. } else {
  735. percentage += "%";
  736. }
  737. return percentage;
  738. })
  739. .style("font-size", settings.percentage.fontSize + "px")
  740. .style("font-family", settings.percentage.font)
  741. .style("font-weight", settings.percentage.fontWeight)
  742. .style("fill", settings.percentage.color);
  743. }
  744. // 3. Add the value label
  745. if (include.value) {
  746. labelGroup.append("text")
  747. .attr("id", function(d, i) { return pie.cssPrefix + "segmentValue" + i + "-" + section; })
  748. .attr("class", pie.cssPrefix + "segmentValue-" + section)
  749. .text(function(d, i) {
  750. formatterContext.index = i;
  751. formatterContext.part = "value";
  752. formatterContext.value = d.value;
  753. formatterContext.label = d.value;
  754. return settings.formatter ? settings.formatter(formatterContext, d.value) : d.value;
  755. })
  756. .style("font-size", settings.value.fontSize + "px")
  757. .style("font-family", settings.value.font)
  758. .style("font-weight", settings.value.fontWeight)
  759. .style("fill", settings.value.color);
  760. }
  761. },
  762. /**
  763. * @param section "inner" / "outer"
  764. */
  765. positionLabelElements: function(pie, section, sectionDisplayType) {
  766. labels["dimensions-" + section] = [];
  767. // get the latest widths, heights
  768. var labelGroups = pie.__labels[section];
  769. labelGroups.each(function(d, i) {
  770. var mainLabel = d3.select(this).selectAll("." + pie.cssPrefix + "segmentMainLabel-" + section);
  771. var percentage = d3.select(this).selectAll("." + pie.cssPrefix + "segmentPercentage-" + section);
  772. var value = d3.select(this).selectAll("." + pie.cssPrefix + "segmentValue-" + section);
  773. labels["dimensions-" + section].push({
  774. mainLabel: (mainLabel.node() !== null) ? mainLabel.node().getBBox() : null,
  775. percentage: (percentage.node() !== null) ? percentage.node().getBBox() : null,
  776. value: (value.node() !== null) ? value.node().getBBox() : null
  777. });
  778. });
  779. var singleLinePad = 5;
  780. var dims = labels["dimensions-" + section];
  781. switch (sectionDisplayType) {
  782. case "label-value1":
  783. pie.svg.selectAll("." + pie.cssPrefix + "segmentValue-" + section)
  784. .attr("dx", function(d, i) { return dims[i].mainLabel.width + singleLinePad; });
  785. break;
  786. case "label-value2":
  787. pie.svg.selectAll("." + pie.cssPrefix + "segmentValue-" + section)
  788. .attr("dy", function(d, i) { return dims[i].mainLabel.height; });
  789. break;
  790. case "label-percentage1":
  791. pie.svg.selectAll("." + pie.cssPrefix + "segmentPercentage-" + section)
  792. .attr("dx", function(d, i) { return dims[i].mainLabel.width + singleLinePad; });
  793. break;
  794. case "label-percentage2":
  795. pie.svg.selectAll("." + pie.cssPrefix + "segmentPercentage-" + section)
  796. .attr("dx", function(d, i) { return (dims[i].mainLabel.width / 2) - (dims[i].percentage.width / 2); })
  797. .attr("dy", function(d, i) { return dims[i].mainLabel.height; });
  798. break;
  799. }
  800. },
  801. computeLabelLinePositions: function(pie) {
  802. pie.lineCoordGroups = [];
  803. pie.__labels.outer
  804. .each(function(d, i) { return labels.computeLinePosition(pie, i); });
  805. },
  806. computeLinePosition: function(pie, i) {
  807. var angle = segments.getSegmentAngle(i, pie.options.data.content, pie.totalSize, { midpoint: true });
  808. var originCoords = math.rotate(pie.pieCenter.x, pie.pieCenter.y - pie.outerRadius, pie.pieCenter.x, pie.pieCenter.y, angle);
  809. var heightOffset = pie.outerLabelGroupData[i].h / 5; // TODO check
  810. var labelXMargin = 6; // the x-distance of the label from the end of the line [TODO configurable]
  811. var quarter = Math.floor(angle / 90);
  812. var midPoint = 4;
  813. var x2, y2, x3, y3;
  814. // this resolves an issue when the
  815. if (quarter === 2 && angle === 180) {
  816. quarter = 1;
  817. }
  818. switch (quarter) {
  819. case 0:
  820. x2 = pie.outerLabelGroupData[i].x - labelXMargin - ((pie.outerLabelGroupData[i].x - labelXMargin - originCoords.x) / 2);
  821. y2 = pie.outerLabelGroupData[i].y + ((originCoords.y - pie.outerLabelGroupData[i].y) / midPoint);
  822. x3 = pie.outerLabelGroupData[i].x - labelXMargin;
  823. y3 = pie.outerLabelGroupData[i].y - heightOffset;
  824. break;
  825. case 1:
  826. x2 = originCoords.x + (pie.outerLabelGroupData[i].x - originCoords.x) / midPoint;
  827. y2 = originCoords.y + (pie.outerLabelGroupData[i].y - originCoords.y) / midPoint;
  828. x3 = pie.outerLabelGroupData[i].x - labelXMargin;
  829. y3 = pie.outerLabelGroupData[i].y - heightOffset;
  830. break;
  831. case 2:
  832. var startOfLabelX = pie.outerLabelGroupData[i].x + pie.outerLabelGroupData[i].w + labelXMargin;
  833. x2 = originCoords.x - (originCoords.x - startOfLabelX) / midPoint;
  834. y2 = originCoords.y + (pie.outerLabelGroupData[i].y - originCoords.y) / midPoint;
  835. x3 = pie.outerLabelGroupData[i].x + pie.outerLabelGroupData[i].w + labelXMargin;
  836. y3 = pie.outerLabelGroupData[i].y - heightOffset;
  837. break;
  838. case 3:
  839. var startOfLabel = pie.outerLabelGroupData[i].x + pie.outerLabelGroupData[i].w + labelXMargin;
  840. x2 = startOfLabel + ((originCoords.x - startOfLabel) / midPoint);
  841. y2 = pie.outerLabelGroupData[i].y + (originCoords.y - pie.outerLabelGroupData[i].y) / midPoint;
  842. x3 = pie.outerLabelGroupData[i].x + pie.outerLabelGroupData[i].w + labelXMargin;
  843. y3 = pie.outerLabelGroupData[i].y - heightOffset;
  844. break;
  845. }
  846. /*
  847. * x1 / y1: the x/y coords of the start of the line, at the mid point of the segments arc on the pie circumference
  848. * x2 / y2: if "curved" line style is being used, this is the midpoint of the line. Other
  849. * x3 / y3: the end of the line; closest point to the label
  850. */
  851. if (pie.options.labels.lines.style === "straight") {
  852. pie.lineCoordGroups[i] = [
  853. { x: originCoords.x, y: originCoords.y },
  854. { x: x3, y: y3 }
  855. ];
  856. } else {
  857. pie.lineCoordGroups[i] = [
  858. { x: originCoords.x, y: originCoords.y },
  859. { x: x2, y: y2 },
  860. { x: x3, y: y3 }
  861. ];
  862. }
  863. },
  864. addLabelLines: function(pie) {
  865. var lineGroups = pie.svg.insert("g", "." + pie.cssPrefix + "pieChart") // meaning, BEFORE .pieChart
  866. .attr("class", pie.cssPrefix + "lineGroups")
  867. .style("opacity", 1);
  868. var lineGroup = lineGroups.selectAll("." + pie.cssPrefix + "lineGroup")
  869. .data(pie.lineCoordGroups)
  870. .enter()
  871. .append("g")
  872. .attr("class", pie.cssPrefix + "lineGroup");
  873. var lineFunction = d3.line()
  874. .curve(d3.curveBasis)
  875. .x(function(d) { return d.x; })
  876. .y(function(d) { return d.y; });
  877. lineGroup.append("path")
  878. .attr("d", lineFunction)
  879. .attr("stroke", function(d, i) {
  880. return (pie.options.labels.lines.color === "segment") ? pie.options.colors[i] : pie.options.labels.lines.color;
  881. })
  882. .attr("stroke-width", 1)
  883. .attr("fill", "none")
  884. .style("opacity", function(d, i) {
  885. var percentage = pie.options.labels.outer.hideWhenLessThanPercentage;
  886. var isHidden = (percentage !== null && d.percentage < percentage) || pie.options.data.content[i].label === "";
  887. return isHidden ? 0 : 1;
  888. });
  889. },
  890. positionLabelGroups: function(pie, section) {
  891. if (pie.options.labels[section].format === "none")
  892. return;
  893. pie.__labels[section]
  894. .style("opacity", function(d, i) {
  895. var percentage = pie.options.labels[section].hideWhenLessThanPercentage;
  896. return (percentage !== null && d.percentage < percentage) ? 0 : 1;
  897. })
  898. .attr("transform", function(d, i) {
  899. var x, y;
  900. if (section === "outer") {
  901. x = pie.outerLabelGroupData[i].x;
  902. y = pie.outerLabelGroupData[i].y;
  903. } else {
  904. var pieCenterCopy = extend(true, {}, pie.pieCenter);
  905. // now recompute the "center" based on the current _innerRadius
  906. if (pie.innerRadius > 0) {
  907. var angle = segments.getSegmentAngle(i, pie.options.data.content, pie.totalSize, { midpoint: true });
  908. var newCoords = math.translate(pie.pieCenter.x, pie.pieCenter.y, pie.innerRadius, angle);
  909. pieCenterCopy.x = newCoords.x;
  910. pieCenterCopy.y = newCoords.y;
  911. }
  912. var dims = helpers.getDimensions(pie.cssPrefix + "labelGroup" + i + "-inner");
  913. var xOffset = dims.w / 2;
  914. var yOffset = dims.h / 4; // confusing! Why 4? should be 2, but it doesn't look right
  915. x = pieCenterCopy.x + (pie.lineCoordGroups[i][0].x - pieCenterCopy.x) / 1.8;
  916. y = pieCenterCopy.y + (pie.lineCoordGroups[i][0].y - pieCenterCopy.y) / 1.8;
  917. x = x - xOffset;
  918. y = y + yOffset;
  919. }
  920. return "translate(" + x + "," + y + ")";
  921. });
  922. },
  923. getIncludes: function(val) {
  924. var addMainLabel = false;
  925. var addValue = false;
  926. var addPercentage = false;
  927. switch (val) {
  928. case "label":
  929. addMainLabel = true;
  930. break;
  931. case "value":
  932. addValue = true;
  933. break;
  934. case "percentage":
  935. addPercentage = true;
  936. break;
  937. case "label-value1":
  938. case "label-value2":
  939. addMainLabel = true;
  940. addValue = true;
  941. break;
  942. case "label-percentage1":
  943. case "label-percentage2":
  944. addMainLabel = true;
  945. addPercentage = true;
  946. break;
  947. }
  948. return {
  949. mainLabel: addMainLabel,
  950. value: addValue,
  951. percentage: addPercentage
  952. };
  953. },
  954. /**
  955. * This does the heavy-lifting to compute the actual coordinates for the outer label groups. It does two things:
  956. * 1. Make a first pass and position them in the ideal positions, based on the pie sizes
  957. * 2. Do some basic collision avoidance.
  958. */
  959. computeOuterLabelCoords: function(pie) {
  960. // 1. figure out the ideal positions for the outer labels
  961. pie.__labels.outer
  962. .each(function(d, i) {
  963. return labels.getIdealOuterLabelPositions(pie, i);
  964. });
  965. // 2. now adjust those positions to try to accommodate conflicts
  966. labels.resolveOuterLabelCollisions(pie);
  967. },
  968. /**
  969. * This attempts to resolve label positioning collisions.
  970. */
  971. resolveOuterLabelCollisions: function(pie) {
  972. if (pie.options.labels.outer.format === "none") {
  973. return;
  974. }
  975. var size = pie.options.data.content.length;
  976. labels.checkConflict(pie, 0, "clockwise", size);
  977. labels.checkConflict(pie, size-1, "anticlockwise", size);
  978. },
  979. checkConflict: function(pie, currIndex, direction, size) {
  980. var i, curr;
  981. if (size <= 1) {
  982. return;
  983. }
  984. var currIndexHemisphere = pie.outerLabelGroupData[currIndex].hs;
  985. if (direction === "clockwise" && currIndexHemisphere !== "right") {
  986. return;
  987. }
  988. if (direction === "anticlockwise" && currIndexHemisphere !== "left") {
  989. return;
  990. }
  991. var nextIndex = (direction === "clockwise") ? currIndex+1 : currIndex-1;
  992. // this is the current label group being looked at. We KNOW it's positioned properly (the first item
  993. // is always correct)
  994. var currLabelGroup = pie.outerLabelGroupData[currIndex];
  995. // this one we don't know about. That's the one we're going to look at and move if necessary
  996. var examinedLabelGroup = pie.outerLabelGroupData[nextIndex];
  997. var info = {
  998. labelHeights: pie.outerLabelGroupData[0].h,
  999. center: pie.pieCenter,
  1000. lineLength: (pie.outerRadius + pie.options.labels.outer.pieDistance),
  1001. heightChange: pie.outerLabelGroupData[0].h + 1 // 1 = padding
  1002. };
  1003. // loop through *ALL* label groups examined so far to check for conflicts. This is because when they're
  1004. // very tightly fitted, a later label group may still appear high up on the page
  1005. if (direction === "clockwise") {
  1006. i = 0;
  1007. for (; i<=currIndex; i++) {
  1008. curr = pie.outerLabelGroupData[i];
  1009. // if there's a conflict with this label group, shift the label to be AFTER the last known
  1010. // one that's been properly placed
  1011. if (!labels.isLabelHidden(pie, i) && helpers.rectIntersect(curr, examinedLabelGroup)) {
  1012. labels.adjustLabelPos(pie, nextIndex, currLabelGroup, info);
  1013. break;
  1014. }
  1015. }
  1016. } else {
  1017. i = size - 1;
  1018. for (; i >= currIndex; i--) {
  1019. curr = pie.outerLabelGroupData[i];
  1020. // if there's a conflict with this label group, shift the label to be AFTER the last known
  1021. // one that's been properly placed
  1022. if (!labels.isLabelHidden(pie, i) && helpers.rectIntersect(curr, examinedLabelGroup)) {
  1023. labels.adjustLabelPos(pie, nextIndex, currLabelGroup, info);
  1024. break;
  1025. }
  1026. }
  1027. }
  1028. labels.checkConflict(pie, nextIndex, direction, size);
  1029. },
  1030. isLabelHidden: function(pie, index) {
  1031. var percentage = pie.options.labels.outer.hideWhenLessThanPercentage;
  1032. return (percentage !== null && d.percentage < percentage) || pie.options.data.content[index].label === "";
  1033. },
  1034. // does a little math to shift a label into a new position based on the last properly placed one
  1035. adjustLabelPos: function(pie, nextIndex, lastCorrectlyPositionedLabel, info) {
  1036. var xDiff, yDiff, newXPos, newYPos;
  1037. newYPos = lastCorrectlyPositionedLabel.y + info.heightChange;
  1038. yDiff = info.center.y - newYPos;
  1039. if (Math.abs(info.lineLength) > Math.abs(yDiff)) {
  1040. xDiff = Math.sqrt((info.lineLength * info.lineLength) - (yDiff * yDiff));
  1041. } else {
  1042. xDiff = Math.sqrt((yDiff * yDiff) - (info.lineLength * info.lineLength));
  1043. }
  1044. if (lastCorrectlyPositionedLabel.hs === "right") {
  1045. newXPos = info.center.x + xDiff;
  1046. } else {
  1047. newXPos = info.center.x - xDiff - pie.outerLabelGroupData[nextIndex].w;
  1048. }
  1049. pie.outerLabelGroupData[nextIndex].x = newXPos;
  1050. pie.outerLabelGroupData[nextIndex].y = newYPos;
  1051. },
  1052. /**
  1053. * @param i 0-N where N is the dataset size - 1.
  1054. */
  1055. getIdealOuterLabelPositions: function(pie, i) {
  1056. var labelGroupNode = pie.svg.select("#" + pie.cssPrefix + "labelGroup" + i + "-outer").node();
  1057. if (!labelGroupNode) return;
  1058. var labelGroupDims = labelGroupNode.getBBox();
  1059. var angle = segments.getSegmentAngle(i, pie.options.data.content, pie.totalSize, { midpoint: true });
  1060. var originalX = pie.pieCenter.x;
  1061. var originalY = pie.pieCenter.y - (pie.outerRadius + pie.options.labels.outer.pieDistance);
  1062. var newCoords = math.rotate(originalX, originalY, pie.pieCenter.x, pie.pieCenter.y, angle);
  1063. // if the label is on the left half of the pie, adjust the values
  1064. var hemisphere = "right"; // hemisphere
  1065. if (angle > 180) {
  1066. newCoords.x -= (labelGroupDims.width + 8);
  1067. hemisphere = "left";
  1068. } else {
  1069. newCoords.x += 8;
  1070. }
  1071. pie.outerLabelGroupData[i] = {
  1072. x: newCoords.x,
  1073. y: newCoords.y,
  1074. w: labelGroupDims.width,
  1075. h: labelGroupDims.height,
  1076. hs: hemisphere
  1077. };
  1078. }
  1079. };
  1080. //// --------- segments.js -----------
  1081. var segments = {
  1082. effectMap: {
  1083. "none": d3.easeLinear,
  1084. "bounce": d3.easeBounce,
  1085. "linear": d3.easeLinear,
  1086. "sin": d3.easeSin,
  1087. "elastic": d3.easeElastic,
  1088. "back": d3.easeBack,
  1089. "quad": d3.easeQuad,
  1090. "circle": d3.easeCircle,
  1091. "exp": d3.easeExp
  1092. },
  1093. /**
  1094. * Creates the pie chart segments and displays them according to the desired load effect.
  1095. * @private
  1096. */
  1097. create: function(pie) {
  1098. var pieCenter = pie.pieCenter;
  1099. var colors = pie.options.colors;
  1100. var loadEffects = pie.options.effects.load;
  1101. var segmentStroke = pie.options.misc.colors.segmentStroke;
  1102. // we insert the pie chart BEFORE the title, to ensure the title overlaps the pie
  1103. var pieChartElement = pie.svg.insert("g", "#" + pie.cssPrefix + "title")
  1104. .attr("transform", function() { return math.getPieTranslateCenter(pieCenter); })
  1105. .attr("class", pie.cssPrefix + "pieChart");
  1106. var arc = d3.arc()
  1107. .innerRadius(pie.innerRadius)
  1108. .outerRadius(pie.outerRadius)
  1109. .startAngle(0)
  1110. .endAngle(function(d) {
  1111. return (d.value / pie.totalSize) * 2 * Math.PI;
  1112. });
  1113. var g = pieChartElement.selectAll("." + pie.cssPrefix + "arc")
  1114. .data(pie.options.data.content)
  1115. .enter()
  1116. .append("g")
  1117. .attr("class", pie.cssPrefix + "arc");
  1118. // if we're not fading in the pie, just set the load speed to 0
  1119. //var loadSpeed = loadEffects.speed;
  1120. //if (loadEffects.effect === "none") {
  1121. // loadSpeed = 0;
  1122. //}
  1123. g.append("path")
  1124. .attr("id", function(d, i) { return pie.cssPrefix + "segment" + i; })
  1125. .attr("fill", function(d, i) {
  1126. var color = colors[i];
  1127. if (pie.options.misc.gradient.enabled) {
  1128. color = "url(#" + pie.cssPrefix + "grad" + i + ")";
  1129. }
  1130. return color;
  1131. })
  1132. .style("stroke", segmentStroke)
  1133. .style("stroke-width", 1)
  1134. //.transition()
  1135. //.ease(d3.easeCubicInOut)
  1136. //.duration(loadSpeed)
  1137. .attr("data-index", function(d, i) { return i; })
  1138. .attr("d", arc);
  1139. /*
  1140. .attrTween("d", function(b) {
  1141. var i = d3.interpolate({ value: 0 }, b);
  1142. return function(t) {
  1143. var ret = pie.arc(i(t));
  1144. console.log(ret);
  1145. return ret;
  1146. };
  1147. });
  1148. */
  1149. pie.svg.selectAll("g." + pie.cssPrefix + "arc")
  1150. .attr("transform",
  1151. function(d, i) {
  1152. var angle = 0;
  1153. if (i > 0) {
  1154. angle = segments.getSegmentAngle(i-1, pie.options.data.content, pie.totalSize);
  1155. }
  1156. return "rotate(" + angle + ")";
  1157. }
  1158. );
  1159. pie.arc = arc;
  1160. },
  1161. addGradients: function(pie) {
  1162. var grads = pie.svg.append("defs")
  1163. .selectAll("radialGradient")
  1164. .data(pie.options.data.content)
  1165. .enter().append("radialGradient")
  1166. .attr("gradientUnits", "userSpaceOnUse")
  1167. .attr("cx", 0)
  1168. .attr("cy", 0)
  1169. .attr("r", "120%")
  1170. .attr("id", function(d, i) { return pie.cssPrefix + "grad" + i; });
  1171. grads.append("stop").attr("offset", "0%").style("stop-color", function(d, i) { return pie.options.colors[i]; });
  1172. grads.append("stop").attr("offset", pie.options.misc.gradient.percentage + "%").style("stop-color", pie.options.misc.gradient.color);
  1173. },
  1174. addSegmentEventHandlers: function(pie) {
  1175. var arc = pie.svg.selectAll("." + pie.cssPrefix + "arc");
  1176. arc = arc.merge(pie.__labels.inner.merge(pie.__labels.outer));
  1177. arc.on("click", function() {
  1178. var currentEl = d3.select(this);
  1179. var segment;
  1180. // mouseover works on both the segments AND the segment labels, hence the following
  1181. if (currentEl.attr("class") === pie.cssPrefix + "arc") {
  1182. segment = currentEl.select("path");
  1183. } else {
  1184. var index = currentEl.attr("data-index");
  1185. segment = d3.select("#" + pie.cssPrefix + "segment" + index);
  1186. }
  1187. var isExpanded = segment.attr("class") === pie.cssPrefix + "expanded";
  1188. segments.onSegmentEvent(pie, pie.options.callbacks.onClickSegment, segment, isExpanded);
  1189. if (pie.options.effects.pullOutSegmentOnClick.effect !== "none") {
  1190. if (isExpanded) {
  1191. segments.closeSegment(pie, segment.node());
  1192. } else {
  1193. segments.openSegment(pie, segment.node());
  1194. }
  1195. }
  1196. });
  1197. arc.on("mouseover", function() {
  1198. var currentEl = d3.select(this);
  1199. var segment, index;
  1200. if (currentEl.attr("class") === pie.cssPrefix + "arc") {
  1201. segment = currentEl.select("path");
  1202. } else {
  1203. index = currentEl.attr("data-index");
  1204. segment = d3.select("#" + pie.cssPrefix + "segment" + index);
  1205. }
  1206. if (pie.options.effects.highlightSegmentOnMouseover) {
  1207. index = segment.attr("data-index");
  1208. var segColor = pie.options.colors[index];
  1209. segment.style("fill", helpers.getColorShade(segColor, pie.options.effects.highlightLuminosity));
  1210. }
  1211. if (pie.options.tooltips.enabled) {
  1212. index = segment.attr("data-index");
  1213. tt.showTooltip(pie, index);
  1214. }
  1215. var isExpanded = segment.attr("class") === pie.cssPrefix + "expanded";
  1216. segments.onSegmentEvent(pie, pie.options.callbacks.onMouseoverSegment, segment, isExpanded);
  1217. });
  1218. arc.on("mousemove", function() {
  1219. tt.moveTooltip(pie);
  1220. });
  1221. arc.on("mouseout", function() {
  1222. var currentEl = d3.select(this);
  1223. var segment, index;
  1224. if (currentEl.attr("class") === pie.cssPrefix + "arc") {
  1225. segment = currentEl.select("path");
  1226. } else {
  1227. index = currentEl.attr("data-index");
  1228. segment = d3.select("#" + pie.cssPrefix + "segment" + index);
  1229. }
  1230. if (pie.options.effects.highlightSegmentOnMouseover) {
  1231. index = segment.attr("data-index");
  1232. var color = pie.options.colors[index];
  1233. if (pie.options.misc.gradient.enabled) {
  1234. color = "url(#" + pie.cssPrefix + "grad" + index + ")";
  1235. }
  1236. segment.style("fill", color);
  1237. }
  1238. if (pie.options.tooltips.enabled) {
  1239. index = segment.attr("data-index");
  1240. tt.hideTooltip(pie, index);
  1241. }
  1242. var isExpanded = segment.attr("class") === pie.cssPrefix + "expanded";
  1243. segments.onSegmentEvent(pie, pie.options.callbacks.onMouseoutSegment, segment, isExpanded);
  1244. });
  1245. },
  1246. // helper function used to call the click, mouseover, mouseout segment callback functions
  1247. onSegmentEvent: function(pie, func, segment, isExpanded) {
  1248. if (!helpers.isFunction(func)) {
  1249. return;
  1250. }
  1251. var index = parseInt(segment.attr("data-index"), 10);
  1252. func({
  1253. segment: segment.node(),
  1254. index: index,
  1255. expanded: isExpanded,
  1256. data: pie.options.data.content[index]
  1257. });
  1258. },
  1259. openSegment: function(pie, segment) {
  1260. if (pie.isOpeningSegment) {
  1261. return;
  1262. }
  1263. pie.isOpeningSegment = true;
  1264. segments.maybeCloseOpenSegment(pie);
  1265. d3.select(segment)
  1266. .transition()
  1267. .ease(segments.effectMap[pie.options.effects.pullOutSegmentOnClick.effect])
  1268. .duration(pie.options.effects.pullOutSegmentOnClick.speed)
  1269. .attr("transform", function(d, i) {
  1270. var c = pie.arc.centroid(d),
  1271. x = c[0],
  1272. y = c[1],
  1273. h = Math.sqrt(x*x + y*y),
  1274. pullOutSize = parseInt(pie.options.effects.pullOutSegmentOnClick.size, 10);
  1275. return "translate(" + ((x/h) * pullOutSize) + ',' + ((y/h) * pullOutSize) + ")";
  1276. })
  1277. .on("end", function(d, i) {
  1278. pie.currentlyOpenSegment = segment;
  1279. pie.isOpeningSegment = false;
  1280. d3.select(segment).attr("class", pie.cssPrefix + "expanded");
  1281. });
  1282. },
  1283. maybeCloseOpenSegment: function(pie) {
  1284. if (typeof pie !== 'undefined' && pie.svg.selectAll("." + pie.cssPrefix + "expanded").size() > 0) {
  1285. segments.closeSegment(pie, pie.svg.select("." + pie.cssPrefix + "expanded").node());
  1286. }
  1287. },
  1288. closeSegment: function(pie, segment) {
  1289. d3.select(segment)
  1290. .transition()
  1291. .duration(400)
  1292. .attr("transform", "translate(0,0)")
  1293. .on("end", function(d, i) {
  1294. d3.select(segment).attr("class", "");
  1295. pie.currentlyOpenSegment = null;
  1296. });
  1297. },
  1298. getCentroid: function(el) {
  1299. var bbox = el.getBBox();
  1300. return {
  1301. x: bbox.x + bbox.width / 2,
  1302. y: bbox.y + bbox.height / 2
  1303. };
  1304. },
  1305. /**
  1306. * General helper function to return a segment's angle, in various different ways.
  1307. * @param index
  1308. * @param opts optional object for fine-tuning exactly what you want.
  1309. */
  1310. getSegmentAngle: function(index, data, totalSize, opts) {
  1311. var options = extend({
  1312. // if true, this returns the full angle from the origin. Otherwise it returns the single segment angle
  1313. compounded: true,
  1314. // optionally returns the midpoint of the angle instead of the full angle
  1315. midpoint: false
  1316. }, opts);
  1317. var currValue = data[index].value;
  1318. var fullValue;
  1319. if (options.compounded) {
  1320. fullValue = 0;
  1321. // get all values up to and including the specified index
  1322. for (var i=0; i<=index; i++) {
  1323. fullValue += data[i].value;
  1324. }
  1325. }
  1326. if (typeof fullValue === 'undefined') {
  1327. fullValue = currValue;
  1328. }
  1329. // now convert the full value to an angle
  1330. var angle = (fullValue / totalSize) * 360;
  1331. // lastly, if we want the midpoint, factor that sucker in
  1332. if (options.midpoint) {
  1333. var currAngle = (currValue / totalSize) * 360;
  1334. angle -= (currAngle / 2);
  1335. }
  1336. return angle;
  1337. }
  1338. };
  1339. //// --------- text.js -----------
  1340. var text = {
  1341. offscreenCoord: -10000,
  1342. addTitle: function(pie) {
  1343. pie.__title = pie.svg.selectAll("." + pie.cssPrefix + "title")
  1344. .data([pie.options.header.title])
  1345. .enter()
  1346. .append("text")
  1347. .text(function(d) { return d.text; })
  1348. .attr("id", pie.cssPrefix + "title")
  1349. .attr("class", pie.cssPrefix + "title")
  1350. .attr("x", text.offscreenCoord)
  1351. .attr("y", text.offscreenCoord)
  1352. .attr("text-anchor", function() {
  1353. var location;
  1354. if (pie.options.header.location === "top-center" || pie.options.header.location === "pie-center") {
  1355. location = "middle";
  1356. } else {
  1357. location = "left";
  1358. }
  1359. return location;
  1360. })
  1361. .attr("fill", function(d) { return d.color; })
  1362. .style("font-size", function(d) { return d.fontSize + "px"; })
  1363. .style("font-weight", function(d) { return d.fontWeight; })
  1364. .style("font-family", function(d) { return d.font; });
  1365. },
  1366. positionTitle: function(pie) {
  1367. var textComponents = pie.textComponents;
  1368. var headerLocation = pie.options.header.location;
  1369. var canvasPadding = pie.options.misc.canvasPadding;
  1370. var canvasWidth = pie.options.size.canvasWidth;
  1371. var titleSubtitlePadding = pie.options.header.titleSubtitlePadding;
  1372. var x;
  1373. if (headerLocation === "top-left") {
  1374. x = canvasPadding.left;
  1375. } else {
  1376. x = ((canvasWidth - canvasPadding.right) / 2) + canvasPadding.left;
  1377. }
  1378. // add whatever offset has been added by user
  1379. x += pie.options.misc.pieCenterOffset.x;
  1380. var y = canvasPadding.top + textComponents.title.h;
  1381. if (headerLocation === "pie-center") {
  1382. y = pie.pieCenter.y;
  1383. // still not fully correct
  1384. if (textComponents.subtitle.exists) {
  1385. var totalTitleHeight = textComponents.title.h + titleSubtitlePadding + textComponents.subtitle.h;
  1386. y = y - (totalTitleHeight / 2) + textComponents.title.h;
  1387. } else {
  1388. y += (textComponents.title.h / 4);
  1389. }
  1390. }
  1391. pie.__title
  1392. .attr("x", x)
  1393. .attr("y", y);
  1394. },
  1395. addSubtitle: function(pie) {
  1396. var headerLocation = pie.options.header.location;
  1397. pie.__subtitle = pie.svg.selectAll("." + pie.cssPrefix + "subtitle")
  1398. .data([pie.options.header.subtitle])
  1399. .enter()
  1400. .append("text")
  1401. .text(function(d) { return d.text; })
  1402. .attr("x", text.offscreenCoord)
  1403. .attr("y", text.offscreenCoord)
  1404. .attr("id", pie.cssPrefix + "subtitle")
  1405. .attr("class", pie.cssPrefix + "subtitle")
  1406. .attr("text-anchor", function() {
  1407. var location;
  1408. if (headerLocation === "top-center" || headerLocation === "pie-center") {
  1409. location = "middle";
  1410. } else {
  1411. location = "left";
  1412. }
  1413. return location;
  1414. })
  1415. .attr("fill", function(d) { return d.color; })
  1416. .style("font-size", function(d) { return d.fontSize + "px"; })
  1417. .style("font-weight", function(d) { return d.fontWeight; })
  1418. .style("font-family", function(d) { return d.font; });
  1419. },
  1420. positionSubtitle: function(pie) {
  1421. var canvasPadding = pie.options.misc.canvasPadding;
  1422. var canvasWidth = pie.options.size.canvasWidth;
  1423. var x;
  1424. if (pie.options.header.location === "top-left") {
  1425. x = canvasPadding.left;
  1426. } else {
  1427. x = ((canvasWidth - canvasPadding.right) / 2) + canvasPadding.left;
  1428. }
  1429. // add whatever offset has been added by user
  1430. x += pie.options.misc.pieCenterOffset.x;
  1431. var y = text.getHeaderHeight(pie);
  1432. pie.__subtitle
  1433. .attr("x", x)
  1434. .attr("y", y);
  1435. },
  1436. addFooter: function(pie) {
  1437. pie.__footer = pie.svg.selectAll("." + pie.cssPrefix + "footer")
  1438. .data([pie.options.footer])
  1439. .enter()
  1440. .append("text")
  1441. .text(function(d) { return d.text; })
  1442. .attr("x", text.offscreenCoord)
  1443. .attr("y", text.offscreenCoord)
  1444. .attr("id", pie.cssPrefix + "footer")
  1445. .attr("class", pie.cssPrefix + "footer")
  1446. .attr("text-anchor", function() {
  1447. var location = "left";
  1448. if (pie.options.footer.location === "bottom-center") {
  1449. location = "middle";
  1450. } else if (pie.options.footer.location === "bottom-right") {
  1451. location = "left"; // on purpose. We have to change the x-coord to make it properly right-aligned
  1452. }
  1453. return location;
  1454. })
  1455. .attr("fill", function(d) { return d.color; })
  1456. .style("font-size", function(d) { return d.fontSize + "px"; })
  1457. .style("font-weight", function(d) { return d.fontWeight; })
  1458. .style("font-family", function(d) { return d.font; });
  1459. },
  1460. positionFooter: function(pie) {
  1461. var footerLocation = pie.options.footer.location;
  1462. var footerWidth = pie.textComponents.footer.w;
  1463. var canvasWidth = pie.options.size.canvasWidth;
  1464. var canvasHeight = pie.options.size.canvasHeight;
  1465. var canvasPadding = pie.options.misc.canvasPadding;
  1466. var x;
  1467. if (footerLocation === "bottom-left") {
  1468. x = canvasPadding.left;
  1469. } else if (footerLocation === "bottom-right") {
  1470. x = canvasWidth - footerWidth - canvasPadding.right;
  1471. } else {
  1472. x = canvasWidth / 2; // TODO - shouldn't this also take into account padding?
  1473. }
  1474. pie.__footer
  1475. .attr("x", x)
  1476. .attr("y", canvasHeight - canvasPadding.bottom);
  1477. },
  1478. getHeaderHeight: function(pie) {
  1479. var h;
  1480. if (pie.textComponents.title.exists) {
  1481. // if the subtitle isn't defined, it'll be set to 0
  1482. var totalTitleHeight = pie.textComponents.title.h + pie.options.header.titleSubtitlePadding + pie.textComponents.subtitle.h;
  1483. if (pie.options.header.location === "pie-center") {
  1484. h = pie.pieCenter.y - (totalTitleHeight / 2) + totalTitleHeight;
  1485. } else {
  1486. h = totalTitleHeight + pie.options.misc.canvasPadding.top;
  1487. }
  1488. } else {
  1489. if (pie.options.header.location === "pie-center") {
  1490. var footerPlusPadding = pie.options.misc.canvasPadding.bottom + pie.textComponents.footer.h;
  1491. h = ((pie.options.size.canvasHeight - footerPlusPadding) / 2) + pie.options.misc.canvasPadding.top + (pie.textComponents.subtitle.h / 2);
  1492. } else {
  1493. h = pie.options.misc.canvasPadding.top + pie.textComponents.subtitle.h;
  1494. }
  1495. }
  1496. return h;
  1497. }
  1498. };
  1499. //// --------- validate.js -----------
  1500. var tt = {
  1501. addTooltips: function(pie) {
  1502. // group the label groups (label, percentage, value) into a single element for simpler positioning
  1503. var tooltips = pie.svg.insert("g")
  1504. .attr("class", pie.cssPrefix + "tooltips");
  1505. tooltips.selectAll("." + pie.cssPrefix + "tooltip")
  1506. .data(pie.options.data.content)
  1507. .enter()
  1508. .append("g")
  1509. .attr("class", pie.cssPrefix + "tooltip")
  1510. .attr("id", function(d, i) { return pie.cssPrefix + "tooltip" + i; })
  1511. .style("opacity", 0)
  1512. .append("rect")
  1513. .attr("rx", pie.options.tooltips.styles.borderRadius)
  1514. .attr("ry", pie.options.tooltips.styles.borderRadius)
  1515. .attr("x", -pie.options.tooltips.styles.padding)
  1516. .attr("opacity", pie.options.tooltips.styles.backgroundOpacity)
  1517. .style("fill", pie.options.tooltips.styles.backgroundColor);
  1518. tooltips.selectAll("." + pie.cssPrefix + "tooltip")
  1519. .data(pie.options.data.content)
  1520. .append("text")
  1521. .attr("fill", function(d) { return pie.options.tooltips.styles.color; })
  1522. .style("font-size", function(d) { return pie.options.tooltips.styles.fontSize; })
  1523. .style("font-weight", function(d) { return pie.options.tooltips.styles.fontWeight; })
  1524. .style("font-family", function(d) { return pie.options.tooltips.styles.font; })
  1525. .text(function(d, i) {
  1526. var caption = pie.options.tooltips.string;
  1527. if (pie.options.tooltips.type === "caption") {
  1528. caption = d.caption;
  1529. }
  1530. return tt.replacePlaceholders(pie, caption, i, {
  1531. label: d.label,
  1532. value: d.value,
  1533. percentage: d.percentage
  1534. });
  1535. });
  1536. tooltips.selectAll("." + pie.cssPrefix + "tooltip rect")
  1537. .attr("width", function (d, i) {
  1538. var dims = helpers.getDimensions(pie.cssPrefix + "tooltip" + i);
  1539. return dims.w + (2 * pie.options.tooltips.styles.padding);
  1540. })
  1541. .attr("height", function (d, i) {
  1542. var dims = helpers.getDimensions(pie.cssPrefix + "tooltip" + i);
  1543. return dims.h + (2 * pie.options.tooltips.styles.padding);
  1544. })
  1545. .attr("y", function (d, i) {
  1546. var dims = helpers.getDimensions(pie.cssPrefix + "tooltip" + i);
  1547. return -(dims.h / 2) + 1;
  1548. });
  1549. },
  1550. showTooltip: function(pie, index) {
  1551. var fadeInSpeed = pie.options.tooltips.styles.fadeInSpeed;
  1552. if (tt.currentTooltip === index) {
  1553. fadeInSpeed = 1;
  1554. }
  1555. tt.currentTooltip = index;
  1556. d3.select("#" + pie.cssPrefix + "tooltip" + index)
  1557. .transition()
  1558. .duration(fadeInSpeed)
  1559. .style("opacity", function() { return 1; });
  1560. tt.moveTooltip(pie);
  1561. },
  1562. moveTooltip: function(pie) {
  1563. d3.selectAll("#" + pie.cssPrefix + "tooltip" + tt.currentTooltip)
  1564. .attr("transform", function(d) {
  1565. var mouseCoords = d3.mouse(this.parentNode);
  1566. var x = mouseCoords[0] + pie.options.tooltips.styles.padding + 2;
  1567. var y = mouseCoords[1] - (2 * pie.options.tooltips.styles.padding) - 2;
  1568. return "translate(" + x + "," + y + ")";
  1569. });
  1570. },
  1571. hideTooltip: function(pie, index) {
  1572. d3.select("#" + pie.cssPrefix + "tooltip" + index)
  1573. .style("opacity", function() { return 0; });
  1574. // move the tooltip offscreen. This ensures that when the user next mouseovers the segment the hidden
  1575. // element won't interfere
  1576. d3.select("#" + pie.cssPrefix + "tooltip" + tt.currentTooltip)
  1577. .attr("transform", function(d, i) {
  1578. // klutzy, but it accounts for tooltip padding which could push it onscreen
  1579. var x = pie.options.size.canvasWidth + 1000;
  1580. var y = pie.options.size.canvasHeight + 1000;
  1581. return "translate(" + x + "," + y + ")";
  1582. });
  1583. },
  1584. replacePlaceholders: function(pie, str, index, replacements) {
  1585. // if the user has defined a placeholderParser function, call it before doing the replacements
  1586. if (helpers.isFunction(pie.options.tooltips.placeholderParser)) {
  1587. pie.options.tooltips.placeholderParser(index, replacements);
  1588. }
  1589. var replacer = function() {
  1590. return function(match) {
  1591. var placeholder = arguments[1];
  1592. if (replacements.hasOwnProperty(placeholder)) {
  1593. return replacements[arguments[1]];
  1594. } else {
  1595. return arguments[0];
  1596. }
  1597. };
  1598. };
  1599. return str.replace(/\{(\w+)\}/g, replacer(replacements));
  1600. }
  1601. };
  1602. // --------------------------------------------------------------------------------------------
  1603. // our constructor
  1604. var d3pie = function(element, options) {
  1605. // element can be an ID or DOM element
  1606. this.element = element;
  1607. if (typeof element === "string") {
  1608. var el = element.replace(/^#/, ""); // replace any jQuery-like ID hash char
  1609. this.element = document.getElementById(el);
  1610. }
  1611. var opts = {};
  1612. extend(true, opts, defaultSettings, options);
  1613. this.options = opts;
  1614. // if the user specified a custom CSS element prefix (ID, class), use it
  1615. if (this.options.misc.cssPrefix !== null) {
  1616. this.cssPrefix = this.options.misc.cssPrefix;
  1617. } else {
  1618. this.cssPrefix = "p" + _uniqueIDCounter + "_";
  1619. _uniqueIDCounter++;
  1620. }
  1621. // now run some validation on the user-defined info
  1622. if (!validate.initialCheck(this)) {
  1623. return;
  1624. }
  1625. // add a data-role to the DOM node to let anyone know that it contains a d3pie instance, and the d3pie version
  1626. d3.select(this.element).attr(_scriptName, _version);
  1627. // things that are done once
  1628. _setupData.call(this);
  1629. _init.call(this);
  1630. };
  1631. d3pie.prototype.recreate = function() {
  1632. // now run some validation on the user-defined info
  1633. if (!validate.initialCheck(this)) {
  1634. return;
  1635. }
  1636. _setupData.call(this);
  1637. _init.call(this);
  1638. };
  1639. d3pie.prototype.redraw = function() {
  1640. this.element.innerHTML = "";
  1641. _init.call(this);
  1642. };
  1643. d3pie.prototype.destroy = function() {
  1644. this.element.innerHTML = ""; // clear out the SVG
  1645. d3.select(this.element).attr(_scriptName, null); // remove the data attr
  1646. };
  1647. /**
  1648. * Returns all pertinent info about the current open info. Returns null if nothing's open, or if one is, an object of
  1649. * the following form:
  1650. * {
  1651. * element: DOM NODE,
  1652. * index: N,
  1653. * data: {}
  1654. * }
  1655. */
  1656. d3pie.prototype.getOpenSegment = function() {
  1657. var segment = this.currentlyOpenSegment;
  1658. if (segment !== null && typeof segment !== "undefined") {
  1659. var index = parseInt(d3.select(segment).attr("data-index"), 10);
  1660. return {
  1661. element: segment,
  1662. index: index,
  1663. data: this.options.data.content[index]
  1664. };
  1665. } else {
  1666. return null;
  1667. }
  1668. };
  1669. d3pie.prototype.openSegment = function(index) {
  1670. index = parseInt(index, 10);
  1671. if (index < 0 || index > this.options.data.content.length-1) {
  1672. return;
  1673. }
  1674. segments.openSegment(this, d3.select("#" + this.cssPrefix + "segment" + index).node());
  1675. };
  1676. d3pie.prototype.closeSegment = function() {
  1677. segments.maybeCloseOpenSegment(this);
  1678. };
  1679. // this let's the user dynamically update aspects of the pie chart without causing a complete redraw. It
  1680. // intelligently re-renders only the part of the pie that the user specifies. Some things cause a repaint, others
  1681. // just redraw the single element
  1682. d3pie.prototype.updateProp = function(propKey, value) {
  1683. switch (propKey) {
  1684. case "header.title.text":
  1685. var oldVal = helpers.processObj(this.options, propKey);
  1686. helpers.processObj(this.options, propKey, value);
  1687. d3.select("#" + this.cssPrefix + "title").html(value);
  1688. if ((oldVal === "" && value !== "") || (oldVal !== "" && value === "")) {
  1689. this.redraw();
  1690. }
  1691. break;
  1692. case "header.subtitle.text":
  1693. var oldValue = helpers.processObj(this.options, propKey);
  1694. helpers.processObj(this.options, propKey, value);
  1695. d3.select("#" + this.cssPrefix + "subtitle").html(value);
  1696. if ((oldValue === "" && value !== "") || (oldValue !== "" && value === "")) {
  1697. this.redraw();
  1698. }
  1699. break;
  1700. case "callbacks.onload":
  1701. case "callbacks.onMouseoverSegment":
  1702. case "callbacks.onMouseoutSegment":
  1703. case "callbacks.onClickSegment":
  1704. case "effects.pullOutSegmentOnClick.effect":
  1705. case "effects.pullOutSegmentOnClick.speed":
  1706. case "effects.pullOutSegmentOnClick.size":
  1707. case "effects.highlightSegmentOnMouseover":
  1708. case "effects.highlightLuminosity":
  1709. helpers.processObj(this.options, propKey, value);
  1710. break;
  1711. // everything else, attempt to update it & do a repaint
  1712. default:
  1713. helpers.processObj(this.options, propKey, value);
  1714. this.destroy();
  1715. this.recreate();
  1716. break;
  1717. }
  1718. };
  1719. // ------------------------------------------------------------------------------------------------
  1720. var _setupData = function () {
  1721. this.options.data.content = math.sortPieData(this);
  1722. if (this.options.data.smallSegmentGrouping.enabled) {
  1723. this.options.data.content = helpers.applySmallSegmentGrouping(this.options.data.content, this.options.data.smallSegmentGrouping);
  1724. }
  1725. this.options.colors = helpers.initSegmentColors(this);
  1726. this.totalSize = math.getTotalPieSize(this.options.data.content);
  1727. var dp = this.options.labels.percentage.decimalPlaces;
  1728. // add in percentage data to content
  1729. for (var i=0; i<this.options.data.content.length; i++) {
  1730. this.options.data.content[i].percentage = _getPercentage(this.options.data.content[i].value, this.totalSize, dp);
  1731. }
  1732. // adjust the final item to ensure the percentage always adds up to precisely 100%. This is necessary
  1733. var totalPercentage = 0;
  1734. for (var j=0; j<this.options.data.content.length; j++) {
  1735. if (j === this.options.data.content.length - 1) {
  1736. this.options.data.content[j].percentage = (100 - totalPercentage).toFixed(dp);
  1737. }
  1738. totalPercentage += parseFloat(this.options.data.content[j].percentage);
  1739. }
  1740. };
  1741. var _init = function() {
  1742. // prep-work
  1743. this.svg = helpers.addSVGSpace(this);
  1744. // store info about the main text components as part of the d3pie object instance
  1745. this.textComponents = {
  1746. headerHeight: 0,
  1747. title: {
  1748. exists: this.options.header.title.text !== "",
  1749. h: 0,
  1750. w: 0
  1751. },
  1752. subtitle: {
  1753. exists: this.options.header.subtitle.text !== "",
  1754. h: 0,
  1755. w: 0
  1756. },
  1757. footer: {
  1758. exists: this.options.footer.text !== "",
  1759. h: 0,
  1760. w: 0
  1761. }
  1762. };
  1763. this.outerLabelGroupData = [];
  1764. // add the key text components offscreen (title, subtitle, footer). We need to know their widths/heights for later computation
  1765. if (this.textComponents.title.exists) text.addTitle(this);
  1766. if (this.textComponents.subtitle.exists) text.addSubtitle(this);
  1767. text.addFooter(this);
  1768. // console.log(this);
  1769. // the footer never moves. Put it in place now
  1770. var self = this;
  1771. text.positionFooter(self);
  1772. var d3 = helpers.getDimensions(self.__footer.node());
  1773. self.textComponents.footer.h = d3.h;
  1774. self.textComponents.footer.w = d3.w;
  1775. if (self.textComponents.title.exists) {
  1776. var d1 = helpers.getDimensions(self.__title.node());
  1777. self.textComponents.title.h = d1.h;
  1778. self.textComponents.title.w = d1.w;
  1779. }
  1780. if (self.textComponents.subtitle.exists) {
  1781. var d2 = helpers.getDimensions(self.__subtitle.node());
  1782. self.textComponents.subtitle.h = d2.h;
  1783. self.textComponents.subtitle.w = d2.w;
  1784. }
  1785. // now compute the full header height
  1786. if (self.textComponents.title.exists || self.textComponents.subtitle.exists) {
  1787. var headerHeight = 0;
  1788. if (self.textComponents.title.exists) {
  1789. headerHeight += self.textComponents.title.h;
  1790. if (self.textComponents.subtitle.exists) {
  1791. headerHeight += self.options.header.titleSubtitlePadding;
  1792. }
  1793. }
  1794. if (self.textComponents.subtitle.exists) {
  1795. headerHeight += self.textComponents.subtitle.h;
  1796. }
  1797. self.textComponents.headerHeight = headerHeight;
  1798. }
  1799. // at this point, all main text component dimensions have been calculated
  1800. math.computePieRadius(self);
  1801. // this value is used all over the place for placing things and calculating locations. We figure it out ONCE
  1802. // and store it as part of the object
  1803. math.calculatePieCenter(self);
  1804. // position the title and subtitle
  1805. text.positionTitle(self);
  1806. text.positionSubtitle(self);
  1807. // now create the pie chart segments, and gradients if the user desired
  1808. if (self.options.misc.gradient.enabled) {
  1809. segments.addGradients(self);
  1810. }
  1811. segments.create(self); // also creates this.arc
  1812. self.__labels = {};
  1813. labels.add(self, "inner", self.options.labels.inner.format);
  1814. labels.add(self, "outer", self.options.labels.outer.format);
  1815. // position the label elements relatively within their individual group (label, percentage, value)
  1816. labels.positionLabelElements(self, "inner", self.options.labels.inner.format);
  1817. labels.positionLabelElements(self, "outer", self.options.labels.outer.format);
  1818. labels.computeOuterLabelCoords(self);
  1819. // this is (and should be) dumb. It just places the outer groups at their calculated, collision-free positions
  1820. labels.positionLabelGroups(self, "outer");
  1821. // we use the label line positions for many other calculations, so ALWAYS compute them
  1822. labels.computeLabelLinePositions(self);
  1823. // only add them if they're actually enabled
  1824. if (self.options.labels.lines.enabled && self.options.labels.outer.format !== "none") {
  1825. labels.addLabelLines(self);
  1826. }
  1827. labels.positionLabelGroups(self, "inner");
  1828. if (helpers.isFunction(self.options.callbacks.onload)) {
  1829. try {
  1830. self.options.callbacks.onload();
  1831. } catch (e) { }
  1832. }
  1833. // add and position the tooltips
  1834. if (self.options.tooltips.enabled) {
  1835. tt.addTooltips(self);
  1836. }
  1837. segments.addSegmentEventHandlers(self);
  1838. };
  1839. var _getPercentage = function(value, total, decimalPlaces) {
  1840. var relativeAmount = value / total;
  1841. if (decimalPlaces <= 0) {
  1842. return Math.round(relativeAmount * 100);
  1843. } else {
  1844. return (relativeAmount * 100).toFixed(decimalPlaces);
  1845. }
  1846. };
  1847. return d3pie;
  1848. }));