jquery.flot.legend.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. /* Flot plugin for drawing legends.
  2. */
  3. (function($) {
  4. var defaultOptions = {
  5. legend: {
  6. show: false,
  7. labelFormatter: null, // fn: string -> string
  8. container: null, // container (as jQuery object) to put legend in, null means default on top of graph
  9. position: 'ne', // position of default legend container within plot
  10. margin: 5, // distance from grid edge to default legend container within plot
  11. sorted: null // default to no legend sorting
  12. }
  13. };
  14. function insertLegend(plot, options, placeholder, legendEntries) {
  15. // clear before redraw
  16. if (options.legend.container != null) {
  17. $(options.legend.container).html('');
  18. } else {
  19. placeholder.find('.legend').remove();
  20. }
  21. if (!options.legend.show) {
  22. return;
  23. }
  24. // Save the legend entries in legend options
  25. var entries = options.legend.legendEntries = legendEntries,
  26. plotOffset = options.legend.plotOffset = plot.getPlotOffset(),
  27. html = [],
  28. entry, labelHtml, iconHtml,
  29. maxLabelLength = 0,
  30. j = 0,
  31. pos = "",
  32. p = options.legend.position,
  33. m = options.legend.margin,
  34. shape = {
  35. name: '',
  36. label: '',
  37. xPos: '',
  38. yPos: ''
  39. };
  40. html[j++] = '<svg class="legendLayer" style="width:inherit;height:inherit;">';
  41. html[j++] = '<rect class="background" width="100%" height="100%"/>';
  42. html[j++] = svgShapeDefs;
  43. // Generate html for icons and labels from a list of entries
  44. for (var i = 0; i < entries.length; ++i) {
  45. entry = entries[i];
  46. iconHtml = '';
  47. shape.label = entry.label;
  48. shape.xPos = '0em';
  49. shape.yPos = i * 1.5 + 'em';
  50. // area
  51. if (entry.options.lines.show && entry.options.lines.fill) {
  52. shape.name = 'area';
  53. shape.fillColor = entry.color;
  54. iconHtml += getEntryIconHtml(shape);
  55. }
  56. // bars
  57. if (entry.options.bars.show) {
  58. shape.name = 'bar';
  59. shape.fillColor = entry.color;
  60. iconHtml += getEntryIconHtml(shape);
  61. }
  62. // lines
  63. if (entry.options.lines.show && !entry.options.lines.fill) {
  64. shape.name = 'line';
  65. shape.strokeColor = entry.color;
  66. shape.strokeWidth = entry.options.lines.lineWidth;
  67. iconHtml += getEntryIconHtml(shape);
  68. }
  69. // points
  70. if (entry.options.points.show) {
  71. shape.name = entry.options.points.symbol;
  72. shape.strokeColor = entry.color;
  73. shape.fillColor = entry.options.points.fillColor;
  74. shape.strokeWidth = entry.options.points.lineWidth;
  75. iconHtml += getEntryIconHtml(shape);
  76. }
  77. maxLabelLength = maxLabelLength < shape.label.length ? shape.label.length : maxLabelLength;
  78. labelHtml = '<text x="' + shape.xPos + '" y="' + shape.yPos + '" text-anchor="start"><tspan dx="2em" dy="1.2em">' + shape.label + '</tspan></text>'
  79. html[j++] = '<g>' + iconHtml + labelHtml + '</g>';
  80. }
  81. html[j++] = '</svg>';
  82. if (m[0] == null) {
  83. m = [m, m];
  84. }
  85. if (p.charAt(0) === 'n') {
  86. pos += 'top:' + (m[1] + plotOffset.top) + 'px;';
  87. } else if (p.charAt(0) === 's') {
  88. pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;';
  89. }
  90. if (p.charAt(1) === 'e') {
  91. pos += 'right:' + (m[0] + plotOffset.right) + 'px;';
  92. } else if (p.charAt(1) === 'w') {
  93. pos += 'left:' + (m[0] + plotOffset.left) + 'px;';
  94. }
  95. var legendEl,
  96. width = 3 + maxLabelLength / 2,
  97. height = entries.length * 1.6;
  98. if (!options.legend.container) {
  99. legendEl = $('<div class="legend" style="position:absolute;' + pos + '">' + html.join('') + '</div>').appendTo(placeholder);
  100. legendEl.css('width', width + 'em');
  101. legendEl.css('height', height + 'em');
  102. legendEl.css('pointerEvents', 'none');
  103. } else {
  104. legendEl = $(html.join('')).appendTo(options.legend.container)[0];
  105. options.legend.container.style.width = width + 'em';
  106. options.legend.container.style.height = height + 'em';
  107. }
  108. }
  109. // Generate html for a shape
  110. function getEntryIconHtml(shape) {
  111. var html = '',
  112. name = shape.name,
  113. x = shape.xPos,
  114. y = shape.yPos,
  115. fill = shape.fillColor,
  116. stroke = shape.strokeColor,
  117. width = shape.strokeWidth;
  118. switch (name) {
  119. case 'circle':
  120. html = '<use xlink:href="#circle" class="legendIcon" ' +
  121. 'x="' + x + '" ' +
  122. 'y="' + y + '" ' +
  123. 'fill="' + fill + '" ' +
  124. 'stroke="' + stroke + '" ' +
  125. 'stroke-width="' + width + '" ' +
  126. 'width="1.5em" height="1.5em"' +
  127. '/>';
  128. break;
  129. case 'diamond':
  130. html = '<use xlink:href="#diamond" class="legendIcon" ' +
  131. 'x="' + x + '" ' +
  132. 'y="' + y + '" ' +
  133. 'fill="' + fill + '" ' +
  134. 'stroke="' + stroke + '" ' +
  135. 'stroke-width="' + width + '" ' +
  136. 'width="1.5em" height="1.5em"' +
  137. '/>';
  138. break;
  139. case 'cross':
  140. html = '<use xlink:href="#cross" class="legendIcon" ' +
  141. 'x="' + x + '" ' +
  142. 'y="' + y + '" ' +
  143. // 'fill="' + fill + '" ' +
  144. 'stroke="' + stroke + '" ' +
  145. 'stroke-width="' + width + '" ' +
  146. 'width="1.5em" height="1.5em"' +
  147. '/>';
  148. break;
  149. case 'rectangle':
  150. html = '<use xlink:href="#rectangle" class="legendIcon" ' +
  151. 'x="' + x + '" ' +
  152. 'y="' + y + '" ' +
  153. 'fill="' + fill + '" ' +
  154. 'stroke="' + stroke + '" ' +
  155. 'stroke-width="' + width + '" ' +
  156. 'width="1.5em" height="1.5em"' +
  157. '/>';
  158. break;
  159. case 'plus':
  160. html = '<use xlink:href="#plus" class="legendIcon" ' +
  161. 'x="' + x + '" ' +
  162. 'y="' + y + '" ' +
  163. // 'fill="' + fill + '" ' +
  164. 'stroke="' + stroke + '" ' +
  165. 'stroke-width="' + width + '" ' +
  166. 'width="1.5em" height="1.5em"' +
  167. '/>';
  168. break;
  169. case 'bar':
  170. html = '<use xlink:href="#bars" class="legendIcon" ' +
  171. 'x="' + x + '" ' +
  172. 'y="' + y + '" ' +
  173. 'fill="' + fill + '" ' +
  174. // 'stroke="' + stroke + '" ' +
  175. // 'stroke-width="' + width + '" ' +
  176. 'width="1.5em" height="1.5em"' +
  177. '/>';
  178. break;
  179. case 'area':
  180. html = '<use xlink:href="#area" class="legendIcon" ' +
  181. 'x="' + x + '" ' +
  182. 'y="' + y + '" ' +
  183. 'fill="' + fill + '" ' +
  184. // 'stroke="' + stroke + '" ' +
  185. // 'stroke-width="' + width + '" ' +
  186. 'width="1.5em" height="1.5em"' +
  187. '/>';
  188. break;
  189. case 'line':
  190. html = '<use xlink:href="#line" class="legendIcon" ' +
  191. 'x="' + x + '" ' +
  192. 'y="' + y + '" ' +
  193. // 'fill="' + fill + '" ' +
  194. 'stroke="' + stroke + '" ' +
  195. 'stroke-width="' + width + '" ' +
  196. 'width="1.5em" height="1.5em"' +
  197. '/>';
  198. break;
  199. default:
  200. // default is circle
  201. html = '<use xlink:href="#circle" class="legendIcon" ' +
  202. 'x="' + x + '" ' +
  203. 'y="' + y + '" ' +
  204. 'fill="' + fill + '" ' +
  205. 'stroke="' + stroke + '" ' +
  206. 'stroke-width="' + width + '" ' +
  207. 'width="1.5em" height="1.5em"' +
  208. '/>';
  209. }
  210. return html;
  211. }
  212. // Define svg symbols for shapes
  213. var svgShapeDefs = '' +
  214. '<defs>' +
  215. '<symbol id="line" fill="none" viewBox="-5 -5 25 25">' +
  216. '<polyline points="0,15 5,5 10,10 15,0"/>' +
  217. '</symbol>' +
  218. '<symbol id="area" stroke-width="1" viewBox="-5 -5 25 25">' +
  219. '<polyline points="0,15 5,5 10,10 15,0, 15,15, 0,15"/>' +
  220. '</symbol>' +
  221. '<symbol id="bars" stroke-width="1" viewBox="-5 -5 25 25">' +
  222. '<polyline points="1.5,15.5 1.5,12.5, 4.5,12.5 4.5,15.5 6.5,15.5 6.5,3.5, 9.5,3.5 9.5,15.5 11.5,15.5 11.5,7.5 14.5,7.5 14.5,15.5 1.5,15.5"/>' +
  223. '</symbol>' +
  224. '<symbol id="circle" viewBox="-5 -5 25 25">' +
  225. '<circle cx="0" cy="15" r="2.5"/>' +
  226. '<circle cx="5" cy="5" r="2.5"/>' +
  227. '<circle cx="10" cy="10" r="2.5"/>' +
  228. '<circle cx="15" cy="0" r="2.5"/>' +
  229. '</symbol>' +
  230. '<symbol id="rectangle" viewBox="-5 -5 25 25">' +
  231. '<rect x="-2.1" y="12.9" width="4.2" height="4.2"/>' +
  232. '<rect x="2.9" y="2.9" width="4.2" height="4.2"/>' +
  233. '<rect x="7.9" y="7.9" width="4.2" height="4.2"/>' +
  234. '<rect x="12.9" y="-2.1" width="4.2" height="4.2"/>' +
  235. '</symbol>' +
  236. '<symbol id="diamond" viewBox="-5 -5 25 25">' +
  237. '<path d="M-3,15 L0,12 L3,15, L0,18 Z"/>' +
  238. '<path d="M2,5 L5,2 L8,5, L5,8 Z"/>' +
  239. '<path d="M7,10 L10,7 L13,10, L10,13 Z"/>' +
  240. '<path d="M12,0 L15,-3 L18,0, L15,3 Z"/>' +
  241. '</symbol>' +
  242. '<symbol id="cross" fill="none" viewBox="-5 -5 25 25">' +
  243. '<path d="M-2.1,12.9 L2.1,17.1, M2.1,12.9 L-2.1,17.1 Z"/>' +
  244. '<path d="M2.9,2.9 L7.1,7.1 M7.1,2.9 L2.9,7.1 Z"/>' +
  245. '<path d="M7.9,7.9 L12.1,12.1 M12.1,7.9 L7.9,12.1 Z"/>' +
  246. '<path d="M12.9,-2.1 L17.1,2.1 M17.1,-2.1 L12.9,2.1 Z"/>' +
  247. '</symbol>' +
  248. '<symbol id="plus" fill="none" viewBox="-5 -5 25 25">' +
  249. '<path d="M0,12 L0,18, M-3,15 L3,15 Z"/>' +
  250. '<path d="M5,2 L5,8 M2,5 L8,5 Z"/>' +
  251. '<path d="M10,7 L10,13 M7,10 L13,10 Z"/>' +
  252. '<path d="M15,-3 L15,3 M12,0 L18,0 Z"/>' +
  253. '</symbol>' +
  254. '</defs>';
  255. // Generate a list of legend entries in their final order
  256. function getLegendEntries(series, labelFormatter, sorted) {
  257. var lf = labelFormatter,
  258. legendEntries = series.map(function(s, i) {
  259. return {
  260. label: (lf ? lf(s.label, s) : s.label) || 'Plot ' + (i + 1),
  261. color: s.color,
  262. options: {
  263. lines: s.lines,
  264. points: s.points,
  265. bars: s.bars
  266. }
  267. };
  268. });
  269. // Sort the legend using either the default or a custom comparator
  270. if (sorted) {
  271. if ($.isFunction(sorted)) {
  272. legendEntries.sort(sorted);
  273. } else if (sorted === 'reverse') {
  274. legendEntries.reverse();
  275. } else {
  276. var ascending = (sorted !== 'descending');
  277. legendEntries.sort(function(a, b) {
  278. return a.label === b.label
  279. ? 0
  280. : ((a.label < b.label) !== ascending ? 1 : -1 // Logical XOR
  281. );
  282. });
  283. }
  284. }
  285. return legendEntries;
  286. }
  287. // return false if opts1 same as opts2
  288. function checkOptions(opts1, opts2) {
  289. for (var prop in opts1) {
  290. if (opts1.hasOwnProperty(prop)) {
  291. if (opts1[prop] !== opts2[prop]) {
  292. return true;
  293. }
  294. }
  295. }
  296. return false;
  297. }
  298. // Compare two lists of legend entries
  299. function shouldRedraw(oldEntries, newEntries) {
  300. if (!oldEntries || !newEntries) {
  301. return true;
  302. }
  303. if (oldEntries.length !== newEntries.length) {
  304. return true;
  305. }
  306. var i, newEntry, oldEntry, newOpts, oldOpts;
  307. for (i = 0; i < newEntries.length; i++) {
  308. newEntry = newEntries[i];
  309. oldEntry = oldEntries[i];
  310. if (newEntry.label !== oldEntry.label) {
  311. return true;
  312. }
  313. if (newEntry.color !== oldEntry.color) {
  314. return true;
  315. }
  316. // check for changes in lines options
  317. newOpts = newEntry.options.lines;
  318. oldOpts = oldEntry.options.lines;
  319. if (checkOptions(newOpts, oldOpts)) {
  320. return true;
  321. }
  322. // check for changes in points options
  323. newOpts = newEntry.options.points;
  324. oldOpts = oldEntry.options.points;
  325. if (checkOptions(newOpts, oldOpts)) {
  326. return true;
  327. }
  328. // check for changes in bars options
  329. newOpts = newEntry.options.bars;
  330. oldOpts = oldEntry.options.bars;
  331. if (checkOptions(newOpts, oldOpts)) {
  332. return true;
  333. }
  334. }
  335. return false;
  336. }
  337. function init(plot) {
  338. plot.hooks.setupGrid.push(function (plot) {
  339. var options = plot.getOptions();
  340. var series = plot.getData(),
  341. labelFormatter = options.legend.labelFormatter,
  342. oldEntries = options.legend.legendEntries,
  343. oldPlotOffset = options.legend.plotOffset,
  344. newEntries = getLegendEntries(series, labelFormatter, options.legend.sorted),
  345. newPlotOffset = plot.getPlotOffset();
  346. if (shouldRedraw(oldEntries, newEntries) ||
  347. checkOptions(oldPlotOffset, newPlotOffset)) {
  348. insertLegend(plot, options, plot.getPlaceholder(), newEntries);
  349. }
  350. });
  351. }
  352. $.plot.plugins.push({
  353. init: init,
  354. options: defaultOptions,
  355. name: 'legend',
  356. version: '1.0'
  357. });
  358. })(jQuery);