Pie.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  1. /*
  2. Copyright (c) 2004-2012, The Dojo Foundation All Rights Reserved.
  3. Available via Academic Free License >= 2.1 OR the modified BSD license.
  4. see: http://dojotoolkit.org/license for details
  5. */
  6. if(!dojo._hasResource["dojox.charting.plot2d.Pie"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  7. dojo._hasResource["dojox.charting.plot2d.Pie"] = true;
  8. dojo.provide("dojox.charting.plot2d.Pie");
  9. dojo.require("dojox.charting.Element");
  10. dojo.require("dojox.charting.axis2d.common");
  11. dojo.require("dojox.charting.plot2d.common");
  12. dojo.require("dojox.charting.plot2d._PlotEvents");
  13. dojo.require("dojox.lang.functional");
  14. dojo.require("dojox.lang.utils");
  15. dojo.require("dojox.gfx");
  16. /*=====
  17. dojo.declare("dojox.charting.plot2d.__PieCtorArgs", dojox.charting.plot2d.__DefaultCtorArgs, {
  18. // summary:
  19. // Specialized keyword arguments object for use in defining parameters on a Pie chart.
  20. // labels: Boolean?
  21. // Whether or not to draw labels within each pie slice. Default is true.
  22. labels: true,
  23. // ticks: Boolean?
  24. // Whether or not to draw ticks to labels within each slice. Default is false.
  25. ticks: false,
  26. // fixed: Boolean?
  27. // TODO
  28. fixed: true,
  29. // precision: Number?
  30. // The precision at which to sum/add data values. Default is 1.
  31. precision: 1,
  32. // labelOffset: Number?
  33. // The amount in pixels by which to offset labels. Default is 20.
  34. labelOffset: 20,
  35. // labelStyle: String?
  36. // Options as to where to draw labels. Values include "default", "rows", and "auto". Default is "default".
  37. labelStyle: "default", // default/rows/auto
  38. // htmlLabels: Boolean?
  39. // Whether or not to use HTML to render slice labels. Default is true.
  40. htmlLabels: true,
  41. // radGrad: String?
  42. // The type of radial gradient to use in rendering. Default is "native".
  43. radGrad: "native",
  44. // fanSize: Number?
  45. // The amount for a radial gradient. Default is 5.
  46. fanSize: 5,
  47. // startAngle: Number?
  48. // Where to being rendering gradients in slices, in degrees. Default is 0.
  49. startAngle: 0,
  50. // radius: Number?
  51. // The size of the radial gradient. Default is 0.
  52. radius: 0
  53. });
  54. =====*/
  55. (function(){
  56. var df = dojox.lang.functional, du = dojox.lang.utils,
  57. dc = dojox.charting.plot2d.common,
  58. da = dojox.charting.axis2d.common,
  59. g = dojox.gfx, m = g.matrix,
  60. FUDGE_FACTOR = 0.2; // use to overlap fans
  61. dojo.declare("dojox.charting.plot2d.Pie", [dojox.charting.Element, dojox.charting.plot2d._PlotEvents], {
  62. // summary:
  63. // The plot that represents a typical pie chart.
  64. defaultParams: {
  65. labels: true,
  66. ticks: false,
  67. fixed: true,
  68. precision: 1,
  69. labelOffset: 20,
  70. labelStyle: "default", // default/rows/auto/columns
  71. htmlLabels: true, // use HTML to draw labels
  72. radGrad: "native", // or "linear", or "fan"
  73. fanSize: 5, // maximum fan size in degrees
  74. startAngle: 0 // start angle for slices in degrees
  75. },
  76. optionalParams: {
  77. radius: 0,
  78. // theme components
  79. stroke: {},
  80. outline: {},
  81. shadow: {},
  82. fill: {},
  83. font: "",
  84. fontColor: "",
  85. labelWiring: {}
  86. },
  87. constructor: function(chart, kwArgs){
  88. // summary:
  89. // Create a pie plot.
  90. this.opt = dojo.clone(this.defaultParams);
  91. du.updateWithObject(this.opt, kwArgs);
  92. du.updateWithPattern(this.opt, kwArgs, this.optionalParams);
  93. this.run = null;
  94. this.dyn = [];
  95. },
  96. clear: function(){
  97. // summary:
  98. // Clear out all of the information tied to this plot.
  99. // returns: dojox.charting.plot2d.Pie
  100. // A reference to this plot for functional chaining.
  101. this.dirty = true;
  102. this.dyn = [];
  103. this.run = null;
  104. return this; // dojox.charting.plot2d.Pie
  105. },
  106. setAxis: function(axis){
  107. // summary:
  108. // Dummy method, since axes are irrelevant with a Pie chart.
  109. // returns: dojox.charting.plot2d.Pie
  110. // The reference to this plot for functional chaining.
  111. return this; // dojox.charting.plot2d.Pie
  112. },
  113. addSeries: function(run){
  114. // summary:
  115. // Add a series of data to this plot.
  116. // returns: dojox.charting.plot2d.Pie
  117. // The reference to this plot for functional chaining.
  118. this.run = run;
  119. return this; // dojox.charting.plot2d.Pie
  120. },
  121. getSeriesStats: function(){
  122. // summary:
  123. // Returns default stats (irrelevant for this type of plot).
  124. // returns: Object
  125. // {hmin, hmax, vmin, vmax} min/max in both directions.
  126. return dojo.delegate(dc.defaultStats);
  127. },
  128. initializeScalers: function(){
  129. // summary:
  130. // Does nothing (irrelevant for this type of plot).
  131. return this;
  132. },
  133. getRequiredColors: function(){
  134. // summary:
  135. // Return the number of colors needed to draw this plot.
  136. return this.run ? this.run.data.length : 0;
  137. },
  138. render: function(dim, offsets){
  139. // summary:
  140. // Render the plot on the chart.
  141. // dim: Object
  142. // An object of the form { width, height }.
  143. // offsets: Object
  144. // An object of the form { l, r, t, b }.
  145. // returns: dojox.charting.plot2d.Pie
  146. // A reference to this plot for functional chaining.
  147. if(!this.dirty){ return this; }
  148. this.resetEvents();
  149. this.dirty = false;
  150. this._eventSeries = {};
  151. this.cleanGroup();
  152. var s = this.group, t = this.chart.theme;
  153. if(!this.run || !this.run.data.length){
  154. return this;
  155. }
  156. // calculate the geometry
  157. var rx = (dim.width - offsets.l - offsets.r) / 2,
  158. ry = (dim.height - offsets.t - offsets.b) / 2,
  159. r = Math.min(rx, ry),
  160. taFont = "font" in this.opt ? this.opt.font : t.axis.font,
  161. size = taFont ? g.normalizedLength(g.splitFontString(taFont).size) : 0,
  162. taFontColor = "fontColor" in this.opt ? this.opt.fontColor : t.axis.fontColor,
  163. startAngle = m._degToRad(this.opt.startAngle),
  164. start = startAngle, step, filteredRun, slices, labels, shift, labelR,
  165. run = this.run.data,
  166. events = this.events();
  167. if(typeof run[0] == "number"){
  168. filteredRun = df.map(run, "x ? Math.max(x, 0) : 0");
  169. if(df.every(filteredRun, "<= 0")){
  170. return this;
  171. }
  172. slices = df.map(filteredRun, "/this", df.foldl(filteredRun, "+", 0));
  173. if(this.opt.labels){
  174. labels = dojo.map(slices, function(x){
  175. return x > 0 ? this._getLabel(x * 100) + "%" : "";
  176. }, this);
  177. }
  178. }else{
  179. filteredRun = df.map(run, "x ? Math.max(x.y, 0) : 0");
  180. if(df.every(filteredRun, "<= 0")){
  181. return this;
  182. }
  183. slices = df.map(filteredRun, "/this", df.foldl(filteredRun, "+", 0));
  184. if(this.opt.labels){
  185. labels = dojo.map(slices, function(x, i){
  186. if(x <= 0){ return ""; }
  187. var v = run[i];
  188. return "text" in v ? v.text : this._getLabel(x * 100) + "%";
  189. }, this);
  190. }
  191. }
  192. var themes = df.map(run, function(v, i){
  193. if(v === null || typeof v == "number"){
  194. return t.next("slice", [this.opt, this.run], true);
  195. }
  196. return t.next("slice", [this.opt, this.run, v], true);
  197. }, this);
  198. if(this.opt.labels){
  199. shift = df.foldl1(df.map(labels, function(label, i){
  200. var font = themes[i].series.font;
  201. return dojox.gfx._base._getTextBox(label, {font: font}).w;
  202. }, this), "Math.max(a, b)") / 2;
  203. if(this.opt.labelOffset < 0){
  204. r = Math.min(rx - 2 * shift, ry - size) + this.opt.labelOffset;
  205. }
  206. labelR = r - this.opt.labelOffset;
  207. }
  208. if("radius" in this.opt){
  209. r = this.opt.radius;
  210. labelR = r - this.opt.labelOffset;
  211. }
  212. var circle = {
  213. cx: offsets.l + rx,
  214. cy: offsets.t + ry,
  215. r: r
  216. };
  217. this.dyn = [];
  218. // draw slices
  219. var eventSeries = new Array(slices.length);
  220. dojo.some(slices, function(slice, i){
  221. if(slice < 0){
  222. // degenerated slice
  223. return false; // continue
  224. }
  225. if(slice == 0){
  226. this.dyn.push({fill: null, stroke: null});
  227. return false;
  228. }
  229. var v = run[i], theme = themes[i], specialFill;
  230. if(slice >= 1){
  231. // whole pie
  232. specialFill = this._plotFill(theme.series.fill, dim, offsets);
  233. specialFill = this._shapeFill(specialFill,
  234. {
  235. x: circle.cx - circle.r, y: circle.cy - circle.r,
  236. width: 2 * circle.r, height: 2 * circle.r
  237. });
  238. specialFill = this._pseudoRadialFill(specialFill, {x: circle.cx, y: circle.cy}, circle.r);
  239. var shape = s.createCircle(circle).setFill(specialFill).setStroke(theme.series.stroke);
  240. this.dyn.push({fill: specialFill, stroke: theme.series.stroke});
  241. if(events){
  242. var o = {
  243. element: "slice",
  244. index: i,
  245. run: this.run,
  246. shape: shape,
  247. x: i,
  248. y: typeof v == "number" ? v : v.y,
  249. cx: circle.cx,
  250. cy: circle.cy,
  251. cr: r
  252. };
  253. this._connectEvents(o);
  254. eventSeries[i] = o;
  255. }
  256. return true; // stop iteration
  257. }
  258. // calculate the geometry of the slice
  259. var end = start + slice * 2 * Math.PI;
  260. if(i + 1 == slices.length){
  261. end = startAngle + 2 * Math.PI;
  262. }
  263. var step = end - start,
  264. x1 = circle.cx + r * Math.cos(start),
  265. y1 = circle.cy + r * Math.sin(start),
  266. x2 = circle.cx + r * Math.cos(end),
  267. y2 = circle.cy + r * Math.sin(end);
  268. // draw the slice
  269. var fanSize = m._degToRad(this.opt.fanSize);
  270. if(theme.series.fill && theme.series.fill.type === "radial" && this.opt.radGrad === "fan" && step > fanSize){
  271. var group = s.createGroup(), nfans = Math.ceil(step / fanSize), delta = step / nfans;
  272. specialFill = this._shapeFill(theme.series.fill,
  273. {x: circle.cx - circle.r, y: circle.cy - circle.r, width: 2 * circle.r, height: 2 * circle.r});
  274. for(var j = 0; j < nfans; ++j){
  275. var fansx = j == 0 ? x1 : circle.cx + r * Math.cos(start + (j - FUDGE_FACTOR) * delta),
  276. fansy = j == 0 ? y1 : circle.cy + r * Math.sin(start + (j - FUDGE_FACTOR) * delta),
  277. fanex = j == nfans - 1 ? x2 : circle.cx + r * Math.cos(start + (j + 1 + FUDGE_FACTOR) * delta),
  278. faney = j == nfans - 1 ? y2 : circle.cy + r * Math.sin(start + (j + 1 + FUDGE_FACTOR) * delta),
  279. fan = group.createPath({}).
  280. moveTo(circle.cx, circle.cy).
  281. lineTo(fansx, fansy).
  282. arcTo(r, r, 0, delta > Math.PI, true, fanex, faney).
  283. lineTo(circle.cx, circle.cy).
  284. closePath().
  285. setFill(this._pseudoRadialFill(specialFill, {x: circle.cx, y: circle.cy}, r, start + (j + 0.5) * delta, start + (j + 0.5) * delta));
  286. }
  287. group.createPath({}).
  288. moveTo(circle.cx, circle.cy).
  289. lineTo(x1, y1).
  290. arcTo(r, r, 0, step > Math.PI, true, x2, y2).
  291. lineTo(circle.cx, circle.cy).
  292. closePath().
  293. setStroke(theme.series.stroke);
  294. shape = group;
  295. }else{
  296. shape = s.createPath({}).
  297. moveTo(circle.cx, circle.cy).
  298. lineTo(x1, y1).
  299. arcTo(r, r, 0, step > Math.PI, true, x2, y2).
  300. lineTo(circle.cx, circle.cy).
  301. closePath().
  302. setStroke(theme.series.stroke);
  303. var specialFill = theme.series.fill;
  304. if(specialFill && specialFill.type === "radial"){
  305. specialFill = this._shapeFill(specialFill, {x: circle.cx - circle.r, y: circle.cy - circle.r, width: 2 * circle.r, height: 2 * circle.r});
  306. if(this.opt.radGrad === "linear"){
  307. specialFill = this._pseudoRadialFill(specialFill, {x: circle.cx, y: circle.cy}, r, start, end);
  308. }
  309. }else if(specialFill && specialFill.type === "linear"){
  310. specialFill = this._plotFill(specialFill, dim, offsets);
  311. specialFill = this._shapeFill(specialFill, shape.getBoundingBox());
  312. }
  313. shape.setFill(specialFill);
  314. }
  315. this.dyn.push({fill: specialFill, stroke: theme.series.stroke});
  316. if(events){
  317. var o = {
  318. element: "slice",
  319. index: i,
  320. run: this.run,
  321. shape: shape,
  322. x: i,
  323. y: typeof v == "number" ? v : v.y,
  324. cx: circle.cx,
  325. cy: circle.cy,
  326. cr: r
  327. };
  328. this._connectEvents(o);
  329. eventSeries[i] = o;
  330. }
  331. start = end;
  332. return false; // continue
  333. }, this);
  334. // draw labels
  335. if(this.opt.labels){
  336. if(this.opt.labelStyle == "default"){
  337. start = startAngle;
  338. dojo.some(slices, function(slice, i){
  339. if(slice <= 0){
  340. // degenerated slice
  341. return false; // continue
  342. }
  343. var theme = themes[i];
  344. if(slice >= 1){
  345. // whole pie
  346. var v = run[i], elem = da.createText[this.opt.htmlLabels && dojox.gfx.renderer != "vml" ? "html" : "gfx"](
  347. this.chart, s, circle.cx, circle.cy + size / 2, "middle", labels[i],
  348. theme.series.font, theme.series.fontColor);
  349. if(this.opt.htmlLabels){
  350. this.htmlElements.push(elem);
  351. }
  352. return true; // stop iteration
  353. }
  354. // calculate the geometry of the slice
  355. var end = start + slice * 2 * Math.PI, v = run[i];
  356. if(i + 1 == slices.length){
  357. end = startAngle + 2 * Math.PI;
  358. }
  359. var labelAngle = (start + end) / 2,
  360. x = circle.cx + labelR * Math.cos(labelAngle),
  361. y = circle.cy + labelR * Math.sin(labelAngle) + size / 2;
  362. // draw the label
  363. var elem = da.createText[this.opt.htmlLabels && dojox.gfx.renderer != "vml" ? "html" : "gfx"]
  364. (this.chart, s, x, y, "middle", labels[i], theme.series.font, theme.series.fontColor);
  365. if(this.opt.htmlLabels){
  366. this.htmlElements.push(elem);
  367. }
  368. start = end;
  369. return false; // continue
  370. }, this);
  371. }else if(this.opt.labelStyle == "columns"){
  372. start = startAngle;
  373. //calculate label angles
  374. var labeledSlices = [];
  375. dojo.forEach(slices, function(slice, i){
  376. var end = start + slice * 2 * Math.PI;
  377. if(i + 1 == slices.length){
  378. end = startAngle + 2 * Math.PI;
  379. }
  380. var labelAngle = (start + end) / 2;
  381. labeledSlices.push({
  382. angle: labelAngle,
  383. left: Math.cos(labelAngle) < 0,
  384. theme: themes[i],
  385. index: i,
  386. omit: end - start < 0.001
  387. });
  388. start = end;
  389. });
  390. //calculate label radius to each slice
  391. var labelHeight = dojox.gfx._base._getTextBox("a",{font:taFont}).h;
  392. this._getProperLabelRadius(labeledSlices, labelHeight, circle.r * 1.1);
  393. //draw label and wiring
  394. dojo.forEach(labeledSlices, function(slice, i){
  395. if (!slice.omit) {
  396. var leftColumn = circle.cx - circle.r * 2,
  397. rightColumn = circle.cx + circle.r * 2,
  398. labelWidth = dojox.gfx._base._getTextBox(labels[i], {font: taFont}).w,
  399. x = circle.cx + slice.labelR * Math.cos(slice.angle),
  400. y = circle.cy + slice.labelR * Math.sin(slice.angle),
  401. jointX = (slice.left) ? (leftColumn + labelWidth) : (rightColumn - labelWidth),
  402. labelX = (slice.left) ? leftColumn : jointX;
  403. var wiring = s.createPath().moveTo(circle.cx + circle.r * Math.cos(slice.angle), circle.cy + circle.r * Math.sin(slice.angle))
  404. if (Math.abs(slice.labelR * Math.cos(slice.angle)) < circle.r * 2 - labelWidth) {
  405. wiring.lineTo(x, y);
  406. }
  407. wiring.lineTo(jointX, y).setStroke(slice.theme.series.labelWiring);
  408. var elem = da.createText[this.opt.htmlLabels && dojox.gfx.renderer != "vml" ? "html" : "gfx"](
  409. this.chart, s, labelX, y, "left", labels[i], slice.theme.series.font, slice.theme.series.fontColor);
  410. if (this.opt.htmlLabels) {
  411. this.htmlElements.push(elem);
  412. }
  413. }
  414. },this);
  415. }
  416. }
  417. // post-process events to restore the original indexing
  418. var esi = 0;
  419. this._eventSeries[this.run.name] = df.map(run, function(v){
  420. return v <= 0 ? null : eventSeries[esi++];
  421. });
  422. return this; // dojox.charting.plot2d.Pie
  423. },
  424. _getProperLabelRadius: function(slices, labelHeight, minRidius){
  425. var leftCenterSlice = {},rightCenterSlice = {},leftMinSIN = 1, rightMinSIN = 1;
  426. if (slices.length == 1) {
  427. slices[0].labelR = minRidius;
  428. return;
  429. }
  430. for(var i = 0;i<slices.length;i++){
  431. var tempSIN = Math.abs(Math.sin(slices[i].angle));
  432. if(slices[i].left){
  433. if(leftMinSIN > tempSIN){
  434. leftMinSIN = tempSIN;
  435. leftCenterSlice = slices[i];
  436. }
  437. }else{
  438. if(rightMinSIN > tempSIN){
  439. rightMinSIN = tempSIN;
  440. rightCenterSlice = slices[i];
  441. }
  442. }
  443. }
  444. leftCenterSlice.labelR = rightCenterSlice.labelR = minRidius;
  445. this._caculateLabelR(leftCenterSlice,slices,labelHeight);
  446. this._caculateLabelR(rightCenterSlice,slices,labelHeight);
  447. },
  448. _caculateLabelR: function(firstSlice,slices,labelHeight){
  449. var i = firstSlice.index,length = slices.length,
  450. currentLabelR = firstSlice.labelR;
  451. while(!(slices[i%length].left ^ slices[(i+1)%length].left)){
  452. if (!slices[(i + 1) % length].omit) {
  453. var nextLabelR = (Math.sin(slices[i % length].angle) * currentLabelR + ((slices[i % length].left) ? (-labelHeight) : labelHeight)) /
  454. Math.sin(slices[(i + 1) % length].angle);
  455. currentLabelR = (nextLabelR < firstSlice.labelR) ? firstSlice.labelR : nextLabelR;
  456. slices[(i + 1) % length].labelR = currentLabelR;
  457. }
  458. i++;
  459. }
  460. i = firstSlice.index,j = (i == 0)?length-1 : i - 1;
  461. while(!(slices[i].left ^ slices[j].left)){
  462. if (!slices[j].omit) {
  463. var nextLabelR = (Math.sin(slices[i].angle) * currentLabelR + ((slices[i].left) ? labelHeight : (-labelHeight))) /
  464. Math.sin(slices[j].angle);
  465. currentLabelR = (nextLabelR < firstSlice.labelR) ? firstSlice.labelR : nextLabelR;
  466. slices[j].labelR = currentLabelR;
  467. }
  468. i--;j--;
  469. i = (i < 0)?i+slices.length:i;
  470. j = (j < 0)?j+slices.length:j;
  471. }
  472. },
  473. // utilities
  474. _getLabel: function(number){
  475. return dc.getLabel(number, this.opt.fixed, this.opt.precision);
  476. }
  477. });
  478. })();
  479. }