Pie.js 17 KB

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