RGraph.rose.js 54 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368
  1. /**
  2. * o------------------------------------------------------------------------------o
  3. * | This file is part of the RGraph package - you can learn more at: |
  4. * | |
  5. * | http://www.rgraph.net |
  6. * | |
  7. * | This package is licensed under the RGraph license. For all kinds of business |
  8. * | purposes there is a small one-time licensing fee to pay and for non |
  9. * | commercial purposes it is free to use. You can read the full license here: |
  10. * | |
  11. * | http://www.rgraph.net/LICENSE.txt |
  12. * o------------------------------------------------------------------------------o
  13. */
  14. if (typeof(RGraph) == 'undefined') RGraph = {};
  15. /**
  16. * The rose chart constuctor
  17. *
  18. * @param object canvas
  19. * @param array data
  20. */
  21. RGraph.Rose = function (id, data)
  22. {
  23. this.id = id;
  24. this.canvas = document.getElementById(typeof id === 'object' ? id.id : id);
  25. this.context = this.canvas.getContext('2d');
  26. this.data = data;
  27. this.canvas.__object__ = this;
  28. this.type = 'rose';
  29. this.isRGraph = true;
  30. this.uid = RGraph.CreateUID();
  31. this.canvas.uid = this.canvas.uid ? this.canvas.uid : RGraph.CreateUID();
  32. this.colorsParsed = false;
  33. this.coordsText = [];
  34. /**
  35. * Compatibility with older browsers
  36. */
  37. RGraph.OldBrowserCompat(this.context);
  38. this.centerx = 0;
  39. this.centery = 0;
  40. this.radius = 0;
  41. this.max = 0;
  42. this.angles = [];
  43. this.angles2 = [];
  44. this.properties = {
  45. 'chart.background.axes': true,
  46. 'chart.background.axes.color': 'black',
  47. 'chart.background.grid': true,
  48. 'chart.background.grid.color': '#ccc',
  49. 'chart.background.grid.size': null,
  50. 'chart.background.grid.spokes': null,
  51. 'chart.background.grid.count': 5,
  52. 'chart.centerx': null,
  53. 'chart.centery': null,
  54. 'chart.radius': null,
  55. 'chart.angles.start': 0,
  56. 'chart.colors': ['rgba(255,0,0,0.5)', 'rgba(255,255,0,0.5)', 'rgba(0,255,255,0.5)', 'rgb(0,255,0)', 'gray', 'blue', 'rgb(255,128,255)','green', 'pink', 'gray', 'aqua'],
  57. 'chart.colors.sequential': false,
  58. 'chart.colors.alpha': null,
  59. 'chart.margin': 0,
  60. 'chart.strokestyle': '#aaa',
  61. 'chart.gutter.left': 25,
  62. 'chart.gutter.right': 25,
  63. 'chart.gutter.top': 25,
  64. 'chart.gutter.bottom': 25,
  65. 'chart.title': '',
  66. 'chart.title.background': null,
  67. 'chart.title.hpos': null,
  68. 'chart.title.vpos': null,
  69. 'chart.title.bold': true,
  70. 'chart.title.font': null,
  71. 'chart.title.x': null,
  72. 'chart.title.y': null,
  73. 'chart.title.halign': null,
  74. 'chart.title.valign': null,
  75. 'chart.labels': null,
  76. 'chart.labels.position': 'center',
  77. 'chart.labels.axes': 'nsew',
  78. 'chart.labels.offset': 0,
  79. 'chart.text.color': 'black',
  80. 'chart.text.font': 'Arial',
  81. 'chart.text.size': 10,
  82. 'chart.key': null,
  83. 'chart.key.background': 'white',
  84. 'chart.key.position': 'graph',
  85. 'chart.key.halign': 'right',
  86. 'chart.key.shadow': false,
  87. 'chart.key.shadow.color': '#666',
  88. 'chart.key.shadow.blur': 3,
  89. 'chart.key.shadow.offsetx': 2,
  90. 'chart.key.shadow.offsety': 2,
  91. 'chart.key.position.gutter.boxed': false,
  92. 'chart.key.position.x': null,
  93. 'chart.key.position.y': null,
  94. 'chart.key.color.shape': 'square',
  95. 'chart.key.rounded': true,
  96. 'chart.key.linewidth': 1,
  97. 'chart.key.colors': null,
  98. 'chart.key.interactive': false,
  99. 'chart.key.interactive.highlight.chart.stroke': 'black',
  100. 'chart.key.interactive.highlight.chart.fill': 'rgba(255,255,255,0.7)',
  101. 'chart.key.interactive.highlight.label': 'rgba(255,0,0,0.2)',
  102. 'chart.key.text.color': 'black',
  103. 'chart.contextmenu': null,
  104. 'chart.tooltips': null,
  105. 'chart.tooltips.event': 'onclick',
  106. 'chart.tooltips.effect': 'fade',
  107. 'chart.tooltips.css.class': 'RGraph_tooltip',
  108. 'chart.tooltips.highlight': true,
  109. 'chart.highlight.stroke': 'rgba(0,0,0,0)',
  110. 'chart.highlight.fill': 'rgba(255,255,255,0.7)',
  111. 'chart.annotatable': false,
  112. 'chart.annotate.color': 'black',
  113. 'chart.zoom.factor': 1.5,
  114. 'chart.zoom.fade.in': true,
  115. 'chart.zoom.fade.out': true,
  116. 'chart.zoom.hdir': 'right',
  117. 'chart.zoom.vdir': 'down',
  118. 'chart.zoom.frames': 25,
  119. 'chart.zoom.delay': 16.666,
  120. 'chart.zoom.shadow': true,
  121. 'chart.zoom.background': true,
  122. 'chart.zoom.action': 'zoom',
  123. 'chart.resizable': false,
  124. 'chart.resize.handle.adjust': [0,0],
  125. 'chart.resize.handle.background': null,
  126. 'chart.adjustable': false,
  127. 'chart.ymax': null,
  128. 'chart.ymin': 0,
  129. 'chart.scale.decimals': null,
  130. 'chart.scale.point': '.',
  131. 'chart.scale.thousand': ',',
  132. 'chart.variant': 'stacked',
  133. 'chart.exploded': 0,
  134. 'chart.events.mousemove': null,
  135. 'chart.events.click': null,
  136. 'chart.animation.roundrobin.factor': 1,
  137. 'chart.animation.roundrobin.radius': true,
  138. 'chart.animation.grow.multiplier': 1,
  139. 'chart.labels.count': 5
  140. }
  141. /**
  142. * Create the $ objects. In the case of non-equi-angular rose charts it actually creates too many $ objects,
  143. * but it doesn't matter.
  144. */
  145. var linear_data = RGraph.array_linearize(this.data);
  146. for (var i=0; i<linear_data.length; ++i) {
  147. this["$" + i] = {}
  148. }
  149. /**
  150. * Translate half a pixel for antialiasing purposes - but only if it hasn't beeen
  151. * done already
  152. */
  153. if (!this.canvas.__rgraph_aa_translated__) {
  154. this.context.translate(0.5,0.5);
  155. this.canvas.__rgraph_aa_translated__ = true;
  156. }
  157. ///////////////////////////////// SHORT PROPERTIES /////////////////////////////////
  158. var RG = RGraph;
  159. var ca = this.canvas;
  160. var co = ca.getContext('2d');
  161. var prop = this.properties;
  162. //////////////////////////////////// METHODS ///////////////////////////////////////
  163. /**
  164. * A simple setter
  165. *
  166. * @param string name The name of the property to set
  167. * @param string value The value of the property
  168. */
  169. this.Set = function (name, value)
  170. {
  171. /**
  172. * This should be done first - prepend the propertyy name with "chart." if necessary
  173. */
  174. if (name.substr(0,6) != 'chart.') {
  175. name = 'chart.' + name;
  176. }
  177. prop[name.toLowerCase()] = value;
  178. return this;
  179. }
  180. /**
  181. * A simple getter
  182. *
  183. * @param string name The name of the property to get
  184. */
  185. this.Get = function (name)
  186. {
  187. /**
  188. * This should be done first - prepend the property name with "chart." if necessary
  189. */
  190. if (name.substr(0,6) != 'chart.') {
  191. name = 'chart.' + name;
  192. }
  193. return prop[name.toLowerCase()];
  194. }
  195. /**
  196. * This method draws the rose chart
  197. */
  198. this.Draw = function ()
  199. {
  200. /**
  201. * Fire the onbeforedraw event
  202. */
  203. RG.FireCustomEvent(this, 'onbeforedraw');
  204. /**
  205. * This doesn't affect the chart, but is used for compatibility
  206. */
  207. this.gutterLeft = prop['chart.gutter.left'];
  208. this.gutterRight = prop['chart.gutter.right'];
  209. this.gutterTop = prop['chart.gutter.top'];
  210. this.gutterBottom = prop['chart.gutter.bottom'];
  211. // Calculate the radius
  212. this.radius = (Math.min(ca.width - this.gutterLeft - this.gutterRight, ca.height - this.gutterTop - this.gutterBottom) / 2);
  213. this.centerx = ((ca.width - this.gutterLeft - this.gutterRight) / 2) + this.gutterLeft;
  214. this.centery = ((ca.height - this.gutterTop - this.gutterBottom) / 2) + this.gutterTop;
  215. this.angles = [];
  216. this.angles2 = [];
  217. this.total = 0;
  218. this.startRadians = prop['chart.angles.start'];
  219. /**
  220. * Change the centerx marginally if the key is defined
  221. */
  222. if (prop['chart.key'] && prop['chart.key'].length > 0 && prop['chart.key'].length >= 3) {
  223. this.centerx = this.centerx - this.gutterRight + 5;
  224. }
  225. // User specified radius, centerx and centery
  226. if (typeof(prop['chart.centerx']) == 'number') this.centerx = prop['chart.centerx'];
  227. if (typeof(prop['chart.centery']) == 'number') this.centery = prop['chart.centery'];
  228. if (typeof(prop['chart.radius']) == 'number') this.radius = prop['chart.radius'];
  229. /**
  230. * Parse the colors for gradients. Its down here so that the center X/Y can be used
  231. */
  232. if (!this.colorsParsed) {
  233. this.parseColors();
  234. // Don't want to do this again
  235. this.colorsParsed = true;
  236. }
  237. this.DrawBackground();
  238. this.DrawRose();
  239. this.DrawLabels();
  240. /**
  241. * Setup the context menu if required
  242. */
  243. if (prop['chart.contextmenu']) {
  244. RG.ShowContext(this);
  245. }
  246. /**
  247. * This function enables resizing
  248. */
  249. if (prop['chart.resizable']) {
  250. RG.AllowResizing(this);
  251. }
  252. /**
  253. * This function enables adjusting
  254. */
  255. if (prop['chart.adjustable']) {
  256. RG.AllowAdjusting(this);
  257. }
  258. /**
  259. * This installs the event listeners
  260. */
  261. RG.InstallEventListeners(this);
  262. /**
  263. * Fire the RGraph ondraw event
  264. */
  265. RG.FireCustomEvent(this, 'ondraw');
  266. return this;
  267. }
  268. /**
  269. * This method draws the rose charts background
  270. */
  271. this.DrawBackground = function ()
  272. {
  273. co.lineWidth = 1;
  274. // Draw the background grey circles/spokes
  275. if (prop['chart.background.grid']) {
  276. if (typeof(prop['chart.background.grid.count']) == 'number') {
  277. prop['chart.background.grid.size'] = this.radius / prop['chart.background.grid.count'];
  278. }
  279. co.beginPath();
  280. co.strokeStyle = prop['chart.background.grid.color'];
  281. // Radius must be greater than 0 for Opera to work
  282. for (var i=prop['chart.background.grid.size']; i<=this.radius; i+=prop['chart.background.grid.size']) {
  283. // Hmmm... This is questionable
  284. co.moveTo(this.centerx + i, this.centery);
  285. // Radius must be greater than 0 for Opera to work
  286. co.arc(this.centerx,
  287. this.centery,
  288. i,
  289. 0,
  290. TWOPI,
  291. false);
  292. }
  293. co.stroke();
  294. // Draw the background lines that go from the center outwards
  295. co.beginPath();
  296. if (typeof(prop['chart.background.grid.spokes']) == 'number') {
  297. var num = (360 / prop['chart.background.grid.spokes']);
  298. for (var i=num; i<=360; i+=num) {
  299. // Radius must be greater than 0 for Opera to work
  300. co.arc(this.centerx,
  301. this.centery,
  302. this.radius,
  303. ((i / (180 / PI)) - HALFPI) + this.startRadians,
  304. (((i + 0.0001) / (180 / PI)) - HALFPI) + this.startRadians,
  305. false);
  306. co.lineTo(this.centerx, this.centery);
  307. }
  308. } else {
  309. for (var i=15; i<=360; i+=15) {
  310. // Radius must be greater than 0 for Opera to work
  311. co.arc(this.centerx,
  312. this.centery,
  313. this.radius,
  314. (i / (180/ PI)) - HALFPI,
  315. ((i + 0.0001) / (180/ PI)) - HALFPI,
  316. false);
  317. co.lineTo(this.centerx, this.centery);
  318. }
  319. }
  320. co.stroke();
  321. }
  322. if (prop['chart.background.axes']) {
  323. co.beginPath();
  324. co.strokeStyle = prop['chart.background.axes.color'];
  325. // Draw the X axis
  326. co.moveTo(this.centerx - this.radius, Math.round(this.centery) );
  327. co.lineTo(this.centerx + this.radius, Math.round(this.centery) );
  328. // Draw the X ends
  329. co.moveTo(Math.round(this.centerx - this.radius), this.centery - 5);
  330. co.lineTo(Math.round(this.centerx - this.radius), this.centery + 5);
  331. co.moveTo(Math.round(this.centerx + this.radius), this.centery - 5);
  332. co.lineTo(Math.round(this.centerx + this.radius), this.centery + 5);
  333. // Draw the X check marks
  334. for (var i=(this.centerx - this.radius); i<(this.centerx + this.radius); i+=(this.radius / 5)) {
  335. co.moveTo(Math.round(i), this.centery - 3);
  336. co.lineTo(Math.round(i), this.centery + 3.5);
  337. }
  338. // Draw the Y check marks
  339. for (var i=(this.centery - this.radius); i<(this.centery + this.radius); i+=(this.radius / 5)) {
  340. co.moveTo(this.centerx - 3, Math.round(i));
  341. co.lineTo(this.centerx + 3, Math.round(i));
  342. }
  343. // Draw the Y axis
  344. co.moveTo(Math.round(this.centerx), this.centery - this.radius);
  345. co.lineTo(Math.round(this.centerx), this.centery + this.radius);
  346. // Draw the Y ends
  347. co.moveTo(this.centerx - 5, Math.round(this.centery - this.radius));
  348. co.lineTo(this.centerx + 5, Math.round(this.centery - this.radius));
  349. co.moveTo(this.centerx - 5, Math.round(this.centery + this.radius));
  350. co.lineTo(this.centerx + 5, Math.round(this.centery + this.radius));
  351. // Stroke it
  352. co.closePath();
  353. co.stroke();
  354. }
  355. }
  356. /**
  357. * This method draws the data on the graph
  358. */
  359. this.DrawRose = function ()
  360. {
  361. var max = 0;
  362. var data = this.data;
  363. var margin = RGraph.degrees2Radians(prop['chart.margin']);
  364. // Must be at least two data points
  365. //if (data.length < 2) {
  366. // alert('[ROSE] Must be at least two data points! [' + data + ']');
  367. // return;
  368. //}
  369. // Work out the maximum value and the sum
  370. if (RG.is_null(prop['chart.ymax'])) {
  371. // Work out the max
  372. for (var i=0; i<data.length; ++i) {
  373. if (typeof(data[i]) == 'number') {
  374. max = Math.max(max, data[i]);
  375. } else if (typeof(data[i]) == 'object' && prop['chart.variant'] == 'non-equi-angular') {
  376. max = Math.max(max, data[i][0]);
  377. // Fallback is stacked
  378. } else {
  379. max = Math.max(max, RG.array_sum(data[i]));
  380. }
  381. }
  382. this.scale2 = RG.getScale2(this, {
  383. 'max':max,
  384. 'min':0,
  385. 'scale.thousand':prop['chart.scale.thousand'],
  386. 'scale.point':prop['chart.scale.point'],
  387. 'scale.decimals':prop['chart.scale.decimals'],
  388. 'ylabels.count':prop['chart.labels.count'],
  389. 'scale.round':prop['chart.scale.round'],
  390. 'units.pre': prop['chart.units.pre'],
  391. 'units.post': prop['chart.units.post']
  392. });
  393. this.max = this.scale2.max;
  394. } else {
  395. var ymax = prop['chart.ymax'];
  396. this.scale2 = RG.getScale2(this, {
  397. 'max':ymax,
  398. 'strict':true,
  399. 'scale.thousand':prop['chart.scale.thousand'],
  400. 'scale.point':prop['chart.scale.point'],
  401. 'scale.decimals':prop['chart.scale.decimals'],
  402. 'ylabels.count':prop['chart.labels.count'],
  403. 'scale.round':prop['chart.scale.round'],
  404. 'units.pre': prop['chart.units.pre'],
  405. 'units.post': prop['chart.units.post']
  406. });
  407. this.max = this.scale2.max
  408. }
  409. this.sum = RG.array_sum(data);
  410. // Move to the centre
  411. co.moveTo(this.centerx, this.centery);
  412. co.stroke(); // Stroke the background so it stays grey
  413. // Transparency
  414. if (prop['chart.colors.alpha']) {
  415. co.globalAlpha = prop['chart.colors.alpha'];
  416. }
  417. /*******************************************************
  418. * A non-equi-angular Rose chart
  419. *******************************************************/
  420. if (typeof(prop['chart.variant']) == 'string' && prop['chart.variant'] == 'non-equi-angular') {
  421. /*******************************************************
  422. * NON-EQUI-ANGULAR GOES HERE
  423. *******************************************************/
  424. var total=0;
  425. for (var i=0; i<data.length; ++i) {
  426. total += data[i][1];
  427. }
  428. for (var i=0; i<this.data.length; ++i) {
  429. var segmentRadians = ((this.data[i][1] / total) * TWOPI);
  430. var radius = ((this.data[i][0] - prop['chart.ymin']) / (this.max - prop['chart.ymin'])) * this.radius;
  431. radius = radius * prop['chart.animation.grow.multiplier'];
  432. co.strokeStyle = prop['chart.strokestyle'];
  433. co.fillStyle = prop['chart.colors'][0];
  434. if (prop['chart.colors.sequential']) {
  435. co.fillStyle = prop['chart.colors'][i];
  436. }
  437. co.beginPath(); // Begin the segment
  438. var startAngle = (this.startRadians * prop['chart.animation.roundrobin.factor']) - HALFPI + margin;
  439. var endAngle = ((this.startRadians + segmentRadians) * prop['chart.animation.roundrobin.factor']) - HALFPI - margin;
  440. var exploded = this.getexploded(i, startAngle, endAngle, prop['chart.exploded']);
  441. var explodedX = exploded[0];
  442. var explodedY = exploded[1];
  443. co.arc(this.centerx + explodedX,
  444. this.centery + explodedY,
  445. prop['chart.animation.roundrobin.radius'] ? radius * prop['chart.animation.roundrobin.factor'] : radius,
  446. startAngle,
  447. endAngle,
  448. 0);
  449. co.lineTo(this.centerx + explodedX, this.centery + explodedY);
  450. co.closePath(); // End the segment
  451. co.stroke();
  452. co.fill();
  453. // Store the start and end angles
  454. this.angles.push(gg = [
  455. startAngle,
  456. endAngle,
  457. 0,
  458. radius,
  459. this.centerx + explodedX,
  460. this.centery + explodedY
  461. ]);
  462. this.startRadians += segmentRadians;
  463. }
  464. } else {
  465. var sequentialColorIndex = 0;
  466. /*******************************************************
  467. * Draw regular segments here
  468. *******************************************************/
  469. for (var i=0; i<this.data.length; ++i) {
  470. co.strokeStyle = prop['chart.strokestyle'];
  471. co.fillStyle = prop['chart.colors'][0];
  472. /*******************************************************
  473. * This allows sequential colors
  474. *******************************************************/
  475. if (prop['chart.colors.sequential']) {
  476. co.fillStyle = prop['chart.colors'][i];
  477. }
  478. var segmentRadians = (1 / this.data.length) * TWOPI;
  479. if (typeof(this.data[i]) == 'number') {
  480. co.beginPath(); // Begin the segment
  481. var radius = ((this.data[i] - prop['chart.ymin']) / (this.max - prop['chart.ymin'])) * this.radius;
  482. radius = radius * prop['chart.animation.grow.multiplier'];
  483. var startAngle = (this.startRadians * prop['chart.animation.roundrobin.factor']) - HALFPI + margin;
  484. var endAngle = (this.startRadians * prop['chart.animation.roundrobin.factor']) + (segmentRadians * prop['chart.animation.roundrobin.factor']) - HALFPI - margin;
  485. var exploded = this.getexploded(i, startAngle, endAngle, prop['chart.exploded']);
  486. var explodedX = exploded[0];
  487. var explodedY = exploded[1];
  488. co.arc(this.centerx + explodedX,
  489. this.centery + explodedY,
  490. prop['chart.animation.roundrobin.radius'] ? radius * prop['chart.animation.roundrobin.factor'] : radius,
  491. startAngle,
  492. endAngle,
  493. 0);
  494. co.lineTo(this.centerx + explodedX, this.centery + explodedY);
  495. co.closePath(); // End the segment
  496. co.stroke();
  497. co.fill();
  498. if (endAngle == 0) {
  499. //endAngle = TWOPI;
  500. }
  501. // Store the start and end angles
  502. this.angles.push([
  503. startAngle,
  504. endAngle,
  505. 0,
  506. radius * prop['chart.animation.roundrobin.factor'],
  507. this.centerx + explodedX,
  508. this.centery + explodedY
  509. ]);
  510. /*******************************************************
  511. * Draw a stacked segment
  512. *******************************************************/
  513. } else if (typeof(this.data[i]) == 'object') {
  514. var margin = prop['chart.margin'] / (180 / PI);
  515. // Initialise the angles2 array so there's no undefined error
  516. if (!this.angles2[i]) {
  517. this.angles2[i] = [];
  518. }
  519. for (var j=0; j<this.data[i].length; ++j) {
  520. var startAngle = (this.startRadians * prop['chart.animation.roundrobin.factor']) - HALFPI + margin;
  521. var endAngle = (this.startRadians * prop['chart.animation.roundrobin.factor'])+ (segmentRadians * prop['chart.animation.roundrobin.factor']) - HALFPI - margin;
  522. var exploded = this.getexploded(i, startAngle, endAngle, prop['chart.exploded']);
  523. var explodedX = exploded[0];
  524. var explodedY = exploded[1];
  525. co.fillStyle = prop['chart.colors'][j];
  526. // This facilitates sequential color support
  527. if (prop['chart.colors.sequential']) {
  528. co.fillStyle = prop['chart.colors'][sequentialColorIndex++];
  529. }
  530. if (j == 0) {
  531. co.beginPath(); // Begin the segment
  532. var startRadius = 0;
  533. var endRadius = ((this.data[i][j] - prop['chart.ymin']) / (this.max - prop['chart.ymin'])) * this.radius;
  534. endRadius = endRadius * prop['chart.animation.grow.multiplier'];
  535. co.arc(this.centerx + explodedX,
  536. this.centery + explodedY,
  537. prop['chart.animation.roundrobin.radius'] ? endRadius * prop['chart.animation.roundrobin.factor'] : endRadius,
  538. startAngle,
  539. endAngle,
  540. 0);
  541. co.lineTo(this.centerx + explodedX, this.centery + explodedY);
  542. co.closePath(); // End the segment
  543. co.stroke();
  544. co.fill();
  545. this.angles.push([
  546. startAngle,
  547. endAngle,
  548. 0,
  549. endRadius * prop['chart.animation.roundrobin.factor'],
  550. this.centerx + explodedX,
  551. this.centery + explodedY
  552. ]);
  553. this.angles2[i].push([
  554. startAngle,
  555. endAngle,
  556. 0,
  557. endRadius * prop['chart.animation.roundrobin.factor'],
  558. this.centerx + explodedX,
  559. this.centery + explodedY
  560. ]);
  561. } else {
  562. co.beginPath(); // Begin the segment
  563. var startRadius = endRadius; // This comes from the prior iteration of this loop
  564. var endRadius = (((this.data[i][j] - prop['chart.ymin']) / (this.max - prop['chart.ymin'])) * this.radius) + startRadius;
  565. endRadius = endRadius * prop['chart.animation.grow.multiplier'];
  566. co.arc(this.centerx + explodedX,
  567. this.centery + explodedY,
  568. startRadius * prop['chart.animation.roundrobin.factor'],
  569. startAngle,
  570. endAngle,
  571. 0);
  572. co.arc(this.centerx + explodedX,
  573. this.centery + explodedY,
  574. endRadius * prop['chart.animation.roundrobin.factor'],
  575. endAngle,
  576. startAngle,
  577. true);
  578. co.closePath(); // End the segment
  579. co.stroke();
  580. co.fill();
  581. this.angles.push([
  582. startAngle,
  583. endAngle,
  584. startRadius * prop['chart.animation.roundrobin.factor'],
  585. endRadius * prop['chart.animation.roundrobin.factor'],
  586. this.centerx + explodedX,
  587. this.centery + explodedY
  588. ]);
  589. this.angles2[i].push([
  590. startAngle,
  591. endAngle,
  592. startRadius * prop['chart.animation.roundrobin.factor'],
  593. endRadius * prop['chart.animation.roundrobin.factor'],
  594. this.centerx + explodedX,
  595. this.centery + explodedY
  596. ]);
  597. }
  598. }
  599. }
  600. this.startRadians += segmentRadians;
  601. }
  602. }
  603. // Turn off the transparency
  604. if (prop['chart.colors.alpha']) {
  605. co.globalAlpha = 1;
  606. }
  607. // Draw the title if any has been set
  608. if (prop['chart.title']) {
  609. RG.DrawTitle(this,
  610. prop['chart.title'],
  611. (ca.height / 2) - this.radius,
  612. this.centerx,
  613. prop['chart.title.size'] ? prop['chart.title.size'] : prop['chart.text.size'] + 2);
  614. }
  615. }
  616. /**
  617. * Unsuprisingly, draws the labels
  618. */
  619. this.DrawLabels = function ()
  620. {
  621. co.lineWidth = 1;
  622. var key = prop['chart.key'];
  623. if (key && key.length) {
  624. RG.DrawKey(this, key, prop['chart.colors']);
  625. }
  626. // Set the color to black
  627. co.fillStyle = prop['chart.text.color'];
  628. co.strokeStyle = 'black';
  629. var radius = this.radius;
  630. var font = prop['chart.text.font'];
  631. var size = prop['chart.text.size'];
  632. var axes = prop['chart.labels.axes'].toLowerCase();
  633. var decimals = prop['chart.scale.decimals'];
  634. var units_pre = prop['chart.units.pre'];
  635. var units_post = prop['chart.units.post'];
  636. var centerx = this.centerx;
  637. var centery = this.centery;
  638. // Draw any circular labels
  639. if (typeof(prop['chart.labels']) == 'object' && prop['chart.labels']) {
  640. this.DrawCircularLabels(co, prop['chart.labels'], font, size, radius + 10);
  641. }
  642. // Size can be specified seperately for the scale now
  643. if (typeof(prop['chart.text.size.scale']) == 'number') {
  644. size = prop['chart.text.size.scale'];
  645. }
  646. var color = 'rgba(255,255,255,0.8)';
  647. // The "North" axis labels
  648. if (axes.indexOf('n') > -1) {
  649. for (var i=0; i<prop['chart.labels.count']; ++i) {
  650. RG.Text2(this, {'font':font,
  651. 'size':size,
  652. 'x':centerx,
  653. 'y':centery - (radius * ((i+1) / prop['chart.labels.count'])),
  654. 'text':this.scale2.labels[i],
  655. 'valign':'center',
  656. 'halign':'center',
  657. 'bounding':true,
  658. 'boundingFill':color,
  659. 'tag': 'scale'
  660. });
  661. }
  662. }
  663. // The "South" axis labels
  664. if (axes.indexOf('s') > -1) {
  665. for (var i=0; i<prop['chart.labels.count']; ++i) {
  666. RG.Text2(this, {'font':font,
  667. 'size':size,
  668. 'x':centerx,
  669. 'y':centery + (radius * ((i+1) / prop['chart.labels.count'])),
  670. 'text':this.scale2.labels[i],
  671. 'valign':'center',
  672. 'halign':'center',
  673. 'bounding':true,
  674. 'boundingFill':color,
  675. 'tag': 'scale'
  676. });
  677. }
  678. }
  679. // The "East" axis labels
  680. if (axes.indexOf('e') > -1) {
  681. for (var i=0; i<prop['chart.labels.count']; ++i) {
  682. RG.Text2(this, {'font':font,
  683. 'size':size,
  684. 'x':centerx + (radius * ((i+1) / prop['chart.labels.count'])),
  685. 'y':centery,
  686. 'text':this.scale2.labels[i],
  687. 'valign':'center',
  688. 'halign':'center',
  689. 'bounding':true,
  690. 'boundingFill':color,
  691. 'tag': 'scale'
  692. });
  693. }
  694. }
  695. // The "West" axis labels
  696. if (axes.indexOf('w') > -1) {
  697. for (var i=0; i<prop['chart.labels.count']; ++i) {
  698. RG.Text2(this, {'font':font,
  699. 'size':size,
  700. 'x':centerx - (radius * ((i+1) / prop['chart.labels.count'])),
  701. 'y':centery,
  702. 'text':this.scale2.labels[i],
  703. 'valign':'center',
  704. 'halign':'center',
  705. 'bounding':true,
  706. 'boundingFill':color,
  707. 'tag': 'scale'
  708. });
  709. }
  710. }
  711. if (axes.length > 0) {
  712. RG.Text2(this, {'font':font,
  713. 'size':size,
  714. 'x':centerx,
  715. 'y':centery,
  716. 'text':typeof(prop['chart.ymin']) == 'number' ? RG.number_format(this, Number(prop['chart.ymin']).toFixed(prop['chart.scale.decimals']), units_pre, units_post) : '0',
  717. 'valign':'center',
  718. 'halign':'center',
  719. 'bounding':true,
  720. 'boundingFill':color,
  721. 'tag': 'scale'
  722. });
  723. }
  724. }
  725. /**
  726. * Draws the circular labels that go around the charts
  727. *
  728. * @param labels array The labels that go around the chart
  729. */
  730. this.DrawCircularLabels = function (co, labels, font, size, radius)
  731. {
  732. var variant = prop['chart.variant'];
  733. var position = prop['chart.labels.position'];
  734. var radius = radius + 5 + prop['chart.labels.offset'];
  735. var centerx = this.centerx;
  736. var centery = this.centery;
  737. for (var i=0; i<labels.length; ++i) {
  738. if (typeof(variant) == 'string' && variant == 'non-equi-angular') {
  739. var a = Number(this.angles[i][0]) + ((this.angles[i][1] - this.angles[i][0]) / 2);
  740. } else {
  741. var a = (TWOPI / labels.length) * (i + 1) - (TWOPI / (labels.length * 2));
  742. var a = a - HALFPI + (prop['chart.labels.position'] == 'edge' ? ((TWOPI / labels.length) / 2) : 0);
  743. }
  744. var x = centerx + (Math.cos(a) * radius);
  745. var y = centery + (Math.sin(a) * radius);
  746. // Horizontal alignment
  747. if (x > centerx) {
  748. halign = 'left';
  749. } else if (Math.round(x) == centerx) {
  750. halign = 'center';
  751. } else {
  752. halign = 'right';
  753. }
  754. RG.Text2(this, {'font':font,
  755. 'size':size,
  756. 'x':x,
  757. 'y':y,
  758. 'text':String(labels[i]),
  759. 'halign':halign,
  760. 'valign':'center',
  761. 'tag': 'labels'
  762. });
  763. }
  764. }
  765. /**
  766. * This function is for use with circular graph types, eg the Pie or Rose. Pass it your event object
  767. * and it will pass you back the corresponding segment details as an array:
  768. *
  769. * [x, y, r, startAngle, endAngle]
  770. *
  771. * Angles are measured in degrees, and are measured from the "east" axis (just like the canvas).
  772. *
  773. * @param object e Your event object
  774. */
  775. this.getShape =
  776. this.getSegment = function (e)
  777. {
  778. RG.FixEventObject(e);
  779. var angles = this.angles;
  780. var ret = [];
  781. /**
  782. * Go through all of the angles checking each one
  783. */
  784. for (var i=0; i<angles.length ; ++i) {
  785. var angleStart = angles[i][0];
  786. var angleEnd = angles[i][1];
  787. var radiusStart = angles[i][2];
  788. var radiusEnd = angles[i][3];
  789. var centerX = angles[i][4];
  790. var centerY = angles[i][5];
  791. var mouseXY = RG.getMouseXY(e);
  792. var mouseX = mouseXY[0] - centerX;
  793. var mouseY = mouseXY[1] - centerY;
  794. // New click testing (the 0.01 is there because Opera doesn't like 0 as the radius)
  795. co.beginPath();
  796. co.arc(centerX, centerY, radiusStart ? radiusStart : 0.01, angleStart, angleEnd, false);
  797. co.arc(centerX, centerY, radiusEnd, angleEnd, angleStart, true);
  798. co.closePath();
  799. // No stroke() or fill()
  800. if (co.isPointInPath(mouseXY[0], mouseXY[1])) {
  801. angles[i][6] = i;
  802. if (RG.parseTooltipText) {
  803. var tooltip = RG.parseTooltipText(prop['chart.tooltips'], angles[i][6]);
  804. }
  805. // Add the textual keys
  806. angles[i]['object'] = this;
  807. angles[i]['x'] = angles[i][4];
  808. angles[i]['y'] = angles[i][5];
  809. angles[i]['angle.start'] = angles[i][0];
  810. angles[i]['angle.end'] = angles[i][1];
  811. angles[i]['radius.start'] = angles[i][2];
  812. angles[i]['radius.end'] = angles[i][3];
  813. angles[i]['index'] = angles[i][6];
  814. angles[i]['tooltip'] = tooltip ? tooltip : null;
  815. return angles[i];
  816. }
  817. }
  818. return null;
  819. }
  820. /**
  821. * Returns any exploded for a particular segment
  822. */
  823. this.getexploded = function (index, startAngle, endAngle, exploded)
  824. {
  825. var explodedx, explodedy;
  826. /**
  827. * Retrieve any exploded - the exploded can be an array of numbers or a single number
  828. * (which is applied to all segments)
  829. */
  830. if (typeof(exploded) == 'object' && typeof(exploded[index]) == 'number') {
  831. explodedx = Math.cos(((endAngle - startAngle) / 2) + startAngle) * exploded[index];
  832. explodedy = Math.sin(((endAngle - startAngle) / 2) + startAngle) * exploded[index];
  833. } else if (typeof(exploded) == 'number') {
  834. explodedx = Math.cos(((endAngle - startAngle) / 2) + startAngle) * exploded;
  835. explodedy = Math.sin(((endAngle - startAngle) / 2) + startAngle) * exploded;
  836. } else {
  837. explodedx = 0;
  838. explodedy = 0;
  839. }
  840. return [explodedx, explodedy];
  841. }
  842. /**
  843. * This function facilitates the installation of tooltip event listeners if
  844. * tooltips are defined.
  845. */
  846. this.AllowTooltips = function ()
  847. {
  848. // Preload any tooltip images that are used in the tooltips
  849. RG.PreLoadTooltipImages(this);
  850. /**
  851. * This installs the window mousedown event listener that lears any
  852. * highlight that may be visible.
  853. */
  854. RG.InstallWindowMousedownTooltipListener(this);
  855. /**
  856. * This installs the canvas mousemove event listener. This function
  857. * controls the pointer shape.
  858. */
  859. RG.InstallCanvasMousemoveTooltipListener(this);
  860. /**
  861. * This installs the canvas mouseup event listener. This is the
  862. * function that actually shows the appropriate tooltip (if any).
  863. */
  864. RG.InstallCanvasMouseupTooltipListener(this);
  865. }
  866. /**
  867. * Each object type has its own Highlight() function which highlights the appropriate shape
  868. *
  869. * @param object shape The shape to highlight
  870. */
  871. this.Highlight = function (shape)
  872. {
  873. if (prop['chart.tooltips.highlight']) {
  874. // Add the new segment highlight
  875. co.beginPath();
  876. co.strokeStyle = prop['chart.highlight.stroke'];
  877. co.fillStyle = prop['chart.highlight.fill'];
  878. co.arc(shape['x'], shape['y'], shape['radius.end'], shape['angle.start'], shape['angle.end'], false);
  879. if (shape['radius.start'] > 0) {
  880. co.arc(shape['x'], shape['y'], shape['radius.start'], shape['angle.end'], shape['angle.start'], true);
  881. } else {
  882. co.lineTo(shape['x'], shape['y']);
  883. }
  884. co.closePath();
  885. co.stroke();
  886. co.fill();
  887. }
  888. }
  889. /**
  890. * The getObjectByXY() worker method. Don't call this call:
  891. *
  892. * RGraph.ObjectRegistry.getObjectByXY(e)
  893. *
  894. * @param object e The event object
  895. */
  896. this.getObjectByXY = function (e)
  897. {
  898. var mouseXY = RGraph.getMouseXY(e);
  899. // Work out the radius
  900. var radius = RG.getHypLength(this.centerx, this.centery, mouseXY[0], mouseXY[1]);
  901. if (
  902. mouseXY[0] > (this.centerx - this.radius)
  903. && mouseXY[0] < (this.centerx + this.radius)
  904. && mouseXY[1] > (this.centery - this.radius)
  905. && mouseXY[1] < (this.centery + this.radius)
  906. && radius <= this.radius
  907. ) {
  908. return this;
  909. }
  910. }
  911. /**
  912. * This function positions a tooltip when it is displayed
  913. *
  914. * @param obj object The chart object
  915. * @param int x The X coordinate specified for the tooltip
  916. * @param int y The Y coordinate specified for the tooltip
  917. * @param objec tooltip The tooltips DIV element
  918. */
  919. this.positionTooltip = function (obj, x, y, tooltip, idx)
  920. {
  921. var coordX = obj.angles[idx][4];
  922. var coordY = obj.angles[idx][5];
  923. var angleStart = obj.angles[idx][0];
  924. var angleEnd = obj.angles[idx][1];
  925. var radius = ((obj.angles[idx][3] - obj.angles[idx][2]) / 2) + obj.angles[idx][2];
  926. var angleCenter = ((angleEnd - angleStart) / 2) + angleStart;
  927. var canvasXY = RG.getCanvasXY(obj.canvas);
  928. var gutterLeft = this.gutterLeft;
  929. var gutterTop = this.gutterTop;
  930. var width = tooltip.offsetWidth;
  931. var height = tooltip.offsetHeight;
  932. // By default any overflow is hidden
  933. tooltip.style.overflow = '';
  934. // The arrow
  935. var img = new Image();
  936. img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAAFCAYAAACjKgd3AAAARUlEQVQYV2NkQAN79+797+RkhC4M5+/bd47B2dmZEVkBCgcmgcsgbAaA9GA1BCSBbhAuA/AagmwQPgMIGgIzCD0M0AMMAEFVIAa6UQgcAAAAAElFTkSuQmCC';
  937. img.style.position = 'absolute';
  938. img.id = '__rgraph_tooltip_pointer__';
  939. img.style.top = (tooltip.offsetHeight - 2) + 'px';
  940. tooltip.appendChild(img);
  941. // Reposition the tooltip if at the edges:
  942. // LEFT edge
  943. if ((canvasXY[0] + coordX + (Math.cos(angleCenter) * radius) - (width / 2)) < 10) {
  944. tooltip.style.left = (canvasXY[0] + coordX + (Math.cos(angleCenter) * radius)- (width * 0.1)) + 'px';
  945. tooltip.style.top = (canvasXY[1] + coordY + (Math.sin(angleCenter) * radius)- height - 5) + 'px';
  946. img.style.left = ((width * 0.1) - 8.5) + 'px';
  947. // RIGHT edge
  948. } else if ((canvasXY[0] + coordX + (Math.cos(angleCenter) * radius) + (width / 2)) > (document.body.offsetWidth - 10) ) {
  949. tooltip.style.left = (canvasXY[0] + coordX + (Math.cos(angleCenter) * radius) - (width * 0.9)) + 'px';
  950. tooltip.style.top = (canvasXY[1] + coordY + (Math.sin(angleCenter) * radius)- height - 5) + 'px';
  951. img.style.left = ((width * 0.9) - 8.5) + 'px';
  952. // Default positioning - CENTERED
  953. } else {
  954. tooltip.style.left = (canvasXY[0] + coordX + (Math.cos(angleCenter) * radius)- (width / 2)) + 'px';
  955. tooltip.style.top = (canvasXY[1] + coordY + (Math.sin(angleCenter) * radius)- height - 5) + 'px';
  956. img.style.left = ((width * 0.5) - 8.5) + 'px';
  957. }
  958. }
  959. /**
  960. * This method gives you the relevant radius for a particular value
  961. *
  962. * @param number value The relevant value to get the radius for
  963. */
  964. this.getRadius = function (value)
  965. {
  966. // Range checking (the Rose minimum is always 0)
  967. if (value < 0 || value > this.max) {
  968. return null;
  969. }
  970. var r = (value / this.max) * this.radius;
  971. return r;
  972. }
  973. /**
  974. * This allows for easy specification of gradients
  975. */
  976. this.parseColors = function ()
  977. {
  978. for (var i=0; i<prop['chart.colors'].length; ++i) {
  979. prop['chart.colors'][i] = this.parseSingleColorForGradient(prop['chart.colors'][i]);
  980. }
  981. /**
  982. * Key colors
  983. */
  984. if (!RG.is_null(prop['chart.key.colors'])) {
  985. for (var i=0; i<prop['chart.key.colors'].length; ++i) {
  986. prop['chart.key.colors'][i] = this.parseSingleColorForGradient(prop['chart.key.colors'][i]);
  987. }
  988. }
  989. prop['chart.text.color'] = this.parseSingleColorForGradient(prop['chart.text.color']);
  990. prop['chart.title.color'] = this.parseSingleColorForGradient(prop['chart.title.color']);
  991. prop['chart.highlight.fill'] = this.parseSingleColorForGradient(prop['chart.highlight.fill']);
  992. prop['chart.highlight.stroke'] = this.parseSingleColorForGradient(prop['chart.highlight.stroke']);
  993. }
  994. /**
  995. * This parses a single color value
  996. */
  997. this.parseSingleColorForGradient = function (color)
  998. {
  999. if (!color || typeof(color) != 'string') {
  1000. return color;
  1001. }
  1002. if (color.match(/^gradient\((.*)\)$/i)) {
  1003. var parts = RegExp.$1.split(':');
  1004. // Create the gradient
  1005. //var grad = context.createLinearGradient(0,0,canvas.width,0);
  1006. var grad = co.createRadialGradient(this.centerx, this.centery, 0, this.centerx, this.centery, this.radius);
  1007. var diff = 1 / (parts.length - 1);
  1008. grad.addColorStop(0, RG.trim(parts[0]));
  1009. for (var j=1; j<parts.length; ++j) {
  1010. grad.addColorStop(j * diff, RG.trim(parts[j]));
  1011. }
  1012. }
  1013. return grad ? grad : color;
  1014. }
  1015. /**
  1016. * This function handles highlighting an entire data-series for the interactive
  1017. * key
  1018. *
  1019. * @param int index The index of the data series to be highlighted
  1020. */
  1021. this.interactiveKeyHighlight = function (index)
  1022. {
  1023. this.angles2.forEach(function (val, idx, arr)
  1024. {
  1025. var segment = val[index];
  1026. if (segment) {
  1027. co.beginPath();
  1028. co.lineWidth = 2;
  1029. co.fillStyle = prop['chart.key.interactive.highlight.chart.fill'];
  1030. co.strokeStyle = prop['chart.key.interactive.highlight.chart.stroke'];
  1031. co.arc(segment[4], segment[5], segment[2], segment[0], segment[1], false);
  1032. co.arc(segment[4], segment[5], segment[3], segment[1], segment[0], true);
  1033. co.closePath();
  1034. co.fill();
  1035. co.stroke();
  1036. }
  1037. });
  1038. }
  1039. /**
  1040. * Register this object
  1041. */
  1042. RG.Register(this);
  1043. }