|
1 /* Flot plugin for drawing legends. |
|
2 |
|
3 */ |
|
4 |
|
5 (function($) { |
|
6 var defaultOptions = { |
|
7 legend: { |
|
8 show: false, |
|
9 noColumns: 1, |
|
10 labelFormatter: null, // fn: string -> string |
|
11 container: null, // container (as jQuery object) to put legend in, null means default on top of graph |
|
12 position: 'ne', // position of default legend container within plot |
|
13 margin: 5, // distance from grid edge to default legend container within plot |
|
14 sorted: null // default to no legend sorting |
|
15 } |
|
16 }; |
|
17 |
|
18 function insertLegend(plot, options, placeholder, legendEntries) { |
|
19 // clear before redraw |
|
20 if (options.legend.container != null) { |
|
21 $(options.legend.container).html(''); |
|
22 } else { |
|
23 placeholder.find('.legend').remove(); |
|
24 } |
|
25 |
|
26 if (!options.legend.show) { |
|
27 return; |
|
28 } |
|
29 |
|
30 // Save the legend entries in legend options |
|
31 var entries = options.legend.legendEntries = legendEntries, |
|
32 plotOffset = options.legend.plotOffset = plot.getPlotOffset(), |
|
33 html = [], |
|
34 entry, labelHtml, iconHtml, |
|
35 j = 0, |
|
36 i, |
|
37 pos = "", |
|
38 p = options.legend.position, |
|
39 m = options.legend.margin, |
|
40 shape = { |
|
41 name: '', |
|
42 label: '', |
|
43 xPos: '', |
|
44 yPos: '' |
|
45 }; |
|
46 |
|
47 html[j++] = '<svg class="legendLayer" style="width:inherit;height:inherit;">'; |
|
48 html[j++] = '<rect class="background" width="100%" height="100%"/>'; |
|
49 html[j++] = svgShapeDefs; |
|
50 |
|
51 var left = 0; |
|
52 var columnWidths = []; |
|
53 var style = window.getComputedStyle(document.querySelector('body')); |
|
54 for (i = 0; i < entries.length; ++i) { |
|
55 var columnIndex = i % options.legend.noColumns; |
|
56 entry = entries[i]; |
|
57 shape.label = entry.label; |
|
58 var info = plot.getSurface().getTextInfo('', shape.label, { |
|
59 style: style.fontStyle, |
|
60 variant: style.fontVariant, |
|
61 weight: style.fontWeight, |
|
62 size: parseInt(style.fontSize), |
|
63 lineHeight: parseInt(style.lineHeight), |
|
64 family: style.fontFamily |
|
65 }); |
|
66 |
|
67 var labelWidth = info.width; |
|
68 // 36px = 1.5em + 6px margin |
|
69 var iconWidth = 48; |
|
70 if (columnWidths[columnIndex]) { |
|
71 if (labelWidth > columnWidths[columnIndex]) { |
|
72 columnWidths[columnIndex] = labelWidth + iconWidth; |
|
73 } |
|
74 } else { |
|
75 columnWidths[columnIndex] = labelWidth + iconWidth; |
|
76 } |
|
77 } |
|
78 |
|
79 // Generate html for icons and labels from a list of entries |
|
80 for (i = 0; i < entries.length; ++i) { |
|
81 var columnIndex = i % options.legend.noColumns; |
|
82 entry = entries[i]; |
|
83 iconHtml = ''; |
|
84 shape.label = entry.label; |
|
85 shape.xPos = (left + 3) + 'px'; |
|
86 left += columnWidths[columnIndex]; |
|
87 if ((i + 1) % options.legend.noColumns === 0) { |
|
88 left = 0; |
|
89 } |
|
90 shape.yPos = Math.floor(i / options.legend.noColumns) * 1.5 + 'em'; |
|
91 // area |
|
92 if (entry.options.lines.show && entry.options.lines.fill) { |
|
93 shape.name = 'area'; |
|
94 shape.fillColor = entry.color; |
|
95 iconHtml += getEntryIconHtml(shape); |
|
96 } |
|
97 // bars |
|
98 if (entry.options.bars.show) { |
|
99 shape.name = 'bar'; |
|
100 shape.fillColor = entry.color; |
|
101 iconHtml += getEntryIconHtml(shape); |
|
102 } |
|
103 // lines |
|
104 if (entry.options.lines.show && !entry.options.lines.fill) { |
|
105 shape.name = 'line'; |
|
106 shape.strokeColor = entry.color; |
|
107 shape.strokeWidth = entry.options.lines.lineWidth; |
|
108 iconHtml += getEntryIconHtml(shape); |
|
109 } |
|
110 // points |
|
111 if (entry.options.points.show) { |
|
112 shape.name = entry.options.points.symbol; |
|
113 shape.strokeColor = entry.color; |
|
114 shape.fillColor = entry.options.points.fillColor; |
|
115 shape.strokeWidth = entry.options.points.lineWidth; |
|
116 iconHtml += getEntryIconHtml(shape); |
|
117 } |
|
118 |
|
119 labelHtml = '<text x="' + shape.xPos + '" y="' + shape.yPos + '" text-anchor="start"><tspan dx="2em" dy="1.2em">' + shape.label + '</tspan></text>' |
|
120 html[j++] = '<g>' + iconHtml + labelHtml + '</g>'; |
|
121 } |
|
122 |
|
123 html[j++] = '</svg>'; |
|
124 if (m[0] == null) { |
|
125 m = [m, m]; |
|
126 } |
|
127 |
|
128 if (p.charAt(0) === 'n') { |
|
129 pos += 'top:' + (m[1] + plotOffset.top) + 'px;'; |
|
130 } else if (p.charAt(0) === 's') { |
|
131 pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;'; |
|
132 } |
|
133 |
|
134 if (p.charAt(1) === 'e') { |
|
135 pos += 'right:' + (m[0] + plotOffset.right) + 'px;'; |
|
136 } else if (p.charAt(1) === 'w') { |
|
137 pos += 'left:' + (m[0] + plotOffset.left) + 'px;'; |
|
138 } |
|
139 |
|
140 var width = 6; |
|
141 for (i = 0; i < columnWidths.length; ++i) { |
|
142 width += columnWidths[i]; |
|
143 } |
|
144 |
|
145 var legendEl, |
|
146 height = Math.ceil(entries.length / options.legend.noColumns) * 1.6; |
|
147 if (!options.legend.container) { |
|
148 legendEl = $('<div class="legend" style="position:absolute;' + pos + '">' + html.join('') + '</div>').appendTo(placeholder); |
|
149 legendEl.css('width', width + 'px'); |
|
150 legendEl.css('height', height + 'em'); |
|
151 legendEl.css('pointerEvents', 'none'); |
|
152 } else { |
|
153 legendEl = $(html.join('')).appendTo(options.legend.container)[0]; |
|
154 options.legend.container.style.width = width + 'px'; |
|
155 options.legend.container.style.height = height + 'em'; |
|
156 } |
|
157 } |
|
158 |
|
159 // Generate html for a shape |
|
160 function getEntryIconHtml(shape) { |
|
161 var html = '', |
|
162 name = shape.name, |
|
163 x = shape.xPos, |
|
164 y = shape.yPos, |
|
165 fill = shape.fillColor, |
|
166 stroke = shape.strokeColor, |
|
167 width = shape.strokeWidth; |
|
168 switch (name) { |
|
169 case 'circle': |
|
170 html = '<use xlink:href="#circle" 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 'diamond': |
|
180 html = '<use xlink:href="#diamond" 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 'cross': |
|
190 html = '<use xlink:href="#cross" 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 case 'rectangle': |
|
200 html = '<use xlink:href="#rectangle" class="legendIcon" ' + |
|
201 'x="' + x + '" ' + |
|
202 'y="' + y + '" ' + |
|
203 'fill="' + fill + '" ' + |
|
204 'stroke="' + stroke + '" ' + |
|
205 'stroke-width="' + width + '" ' + |
|
206 'width="1.5em" height="1.5em"' + |
|
207 '/>'; |
|
208 break; |
|
209 case 'plus': |
|
210 html = '<use xlink:href="#plus" class="legendIcon" ' + |
|
211 'x="' + x + '" ' + |
|
212 'y="' + y + '" ' + |
|
213 // 'fill="' + fill + '" ' + |
|
214 'stroke="' + stroke + '" ' + |
|
215 'stroke-width="' + width + '" ' + |
|
216 'width="1.5em" height="1.5em"' + |
|
217 '/>'; |
|
218 break; |
|
219 case 'bar': |
|
220 html = '<use xlink:href="#bars" class="legendIcon" ' + |
|
221 'x="' + x + '" ' + |
|
222 'y="' + y + '" ' + |
|
223 'fill="' + fill + '" ' + |
|
224 // 'stroke="' + stroke + '" ' + |
|
225 // 'stroke-width="' + width + '" ' + |
|
226 'width="1.5em" height="1.5em"' + |
|
227 '/>'; |
|
228 break; |
|
229 case 'area': |
|
230 html = '<use xlink:href="#area" class="legendIcon" ' + |
|
231 'x="' + x + '" ' + |
|
232 'y="' + y + '" ' + |
|
233 'fill="' + fill + '" ' + |
|
234 // 'stroke="' + stroke + '" ' + |
|
235 // 'stroke-width="' + width + '" ' + |
|
236 'width="1.5em" height="1.5em"' + |
|
237 '/>'; |
|
238 break; |
|
239 case 'line': |
|
240 html = '<use xlink:href="#line" class="legendIcon" ' + |
|
241 'x="' + x + '" ' + |
|
242 'y="' + y + '" ' + |
|
243 // 'fill="' + fill + '" ' + |
|
244 'stroke="' + stroke + '" ' + |
|
245 'stroke-width="' + width + '" ' + |
|
246 'width="1.5em" height="1.5em"' + |
|
247 '/>'; |
|
248 break; |
|
249 default: |
|
250 // default is circle |
|
251 html = '<use xlink:href="#circle" class="legendIcon" ' + |
|
252 'x="' + x + '" ' + |
|
253 'y="' + y + '" ' + |
|
254 'fill="' + fill + '" ' + |
|
255 'stroke="' + stroke + '" ' + |
|
256 'stroke-width="' + width + '" ' + |
|
257 'width="1.5em" height="1.5em"' + |
|
258 '/>'; |
|
259 } |
|
260 |
|
261 return html; |
|
262 } |
|
263 |
|
264 // Define svg symbols for shapes |
|
265 var svgShapeDefs = '' + |
|
266 '<defs>' + |
|
267 '<symbol id="line" fill="none" viewBox="-5 -5 25 25">' + |
|
268 '<polyline points="0,15 5,5 10,10 15,0"/>' + |
|
269 '</symbol>' + |
|
270 |
|
271 '<symbol id="area" stroke-width="1" viewBox="-5 -5 25 25">' + |
|
272 '<polyline points="0,15 5,5 10,10 15,0, 15,15, 0,15"/>' + |
|
273 '</symbol>' + |
|
274 |
|
275 '<symbol id="bars" stroke-width="1" viewBox="-5 -5 25 25">' + |
|
276 '<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"/>' + |
|
277 '</symbol>' + |
|
278 |
|
279 '<symbol id="circle" viewBox="-5 -5 25 25">' + |
|
280 '<circle cx="0" cy="15" r="2.5"/>' + |
|
281 '<circle cx="5" cy="5" r="2.5"/>' + |
|
282 '<circle cx="10" cy="10" r="2.5"/>' + |
|
283 '<circle cx="15" cy="0" r="2.5"/>' + |
|
284 '</symbol>' + |
|
285 |
|
286 '<symbol id="rectangle" viewBox="-5 -5 25 25">' + |
|
287 '<rect x="-2.1" y="12.9" width="4.2" height="4.2"/>' + |
|
288 '<rect x="2.9" y="2.9" width="4.2" height="4.2"/>' + |
|
289 '<rect x="7.9" y="7.9" width="4.2" height="4.2"/>' + |
|
290 '<rect x="12.9" y="-2.1" width="4.2" height="4.2"/>' + |
|
291 '</symbol>' + |
|
292 |
|
293 '<symbol id="diamond" viewBox="-5 -5 25 25">' + |
|
294 '<path d="M-3,15 L0,12 L3,15, L0,18 Z"/>' + |
|
295 '<path d="M2,5 L5,2 L8,5, L5,8 Z"/>' + |
|
296 '<path d="M7,10 L10,7 L13,10, L10,13 Z"/>' + |
|
297 '<path d="M12,0 L15,-3 L18,0, L15,3 Z"/>' + |
|
298 '</symbol>' + |
|
299 |
|
300 '<symbol id="cross" fill="none" viewBox="-5 -5 25 25">' + |
|
301 '<path d="M-2.1,12.9 L2.1,17.1, M2.1,12.9 L-2.1,17.1 Z"/>' + |
|
302 '<path d="M2.9,2.9 L7.1,7.1 M7.1,2.9 L2.9,7.1 Z"/>' + |
|
303 '<path d="M7.9,7.9 L12.1,12.1 M12.1,7.9 L7.9,12.1 Z"/>' + |
|
304 '<path d="M12.9,-2.1 L17.1,2.1 M17.1,-2.1 L12.9,2.1 Z"/>' + |
|
305 '</symbol>' + |
|
306 |
|
307 '<symbol id="plus" fill="none" viewBox="-5 -5 25 25">' + |
|
308 '<path d="M0,12 L0,18, M-3,15 L3,15 Z"/>' + |
|
309 '<path d="M5,2 L5,8 M2,5 L8,5 Z"/>' + |
|
310 '<path d="M10,7 L10,13 M7,10 L13,10 Z"/>' + |
|
311 '<path d="M15,-3 L15,3 M12,0 L18,0 Z"/>' + |
|
312 '</symbol>' + |
|
313 '</defs>'; |
|
314 |
|
315 // Generate a list of legend entries in their final order |
|
316 function getLegendEntries(series, labelFormatter, sorted) { |
|
317 var lf = labelFormatter, |
|
318 legendEntries = series.reduce(function(validEntries, s, i) { |
|
319 var labelEval = (lf ? lf(s.label, s) : s.label) |
|
320 if (s.hasOwnProperty("label") ? labelEval : true) { |
|
321 var entry = { |
|
322 label: labelEval || 'Plot ' + (i + 1), |
|
323 color: s.color, |
|
324 options: { |
|
325 lines: s.lines, |
|
326 points: s.points, |
|
327 bars: s.bars |
|
328 } |
|
329 } |
|
330 validEntries.push(entry) |
|
331 } |
|
332 return validEntries; |
|
333 }, []); |
|
334 |
|
335 // Sort the legend using either the default or a custom comparator |
|
336 if (sorted) { |
|
337 if ($.isFunction(sorted)) { |
|
338 legendEntries.sort(sorted); |
|
339 } else if (sorted === 'reverse') { |
|
340 legendEntries.reverse(); |
|
341 } else { |
|
342 var ascending = (sorted !== 'descending'); |
|
343 legendEntries.sort(function(a, b) { |
|
344 return a.label === b.label |
|
345 ? 0 |
|
346 : ((a.label < b.label) !== ascending ? 1 : -1 // Logical XOR |
|
347 ); |
|
348 }); |
|
349 } |
|
350 } |
|
351 |
|
352 return legendEntries; |
|
353 } |
|
354 |
|
355 // return false if opts1 same as opts2 |
|
356 function checkOptions(opts1, opts2) { |
|
357 for (var prop in opts1) { |
|
358 if (opts1.hasOwnProperty(prop)) { |
|
359 if (opts1[prop] !== opts2[prop]) { |
|
360 return true; |
|
361 } |
|
362 } |
|
363 } |
|
364 return false; |
|
365 } |
|
366 |
|
367 // Compare two lists of legend entries |
|
368 function shouldRedraw(oldEntries, newEntries) { |
|
369 if (!oldEntries || !newEntries) { |
|
370 return true; |
|
371 } |
|
372 |
|
373 if (oldEntries.length !== newEntries.length) { |
|
374 return true; |
|
375 } |
|
376 var i, newEntry, oldEntry, newOpts, oldOpts; |
|
377 for (i = 0; i < newEntries.length; i++) { |
|
378 newEntry = newEntries[i]; |
|
379 oldEntry = oldEntries[i]; |
|
380 |
|
381 if (newEntry.label !== oldEntry.label) { |
|
382 return true; |
|
383 } |
|
384 |
|
385 if (newEntry.color !== oldEntry.color) { |
|
386 return true; |
|
387 } |
|
388 |
|
389 // check for changes in lines options |
|
390 newOpts = newEntry.options.lines; |
|
391 oldOpts = oldEntry.options.lines; |
|
392 if (checkOptions(newOpts, oldOpts)) { |
|
393 return true; |
|
394 } |
|
395 |
|
396 // check for changes in points options |
|
397 newOpts = newEntry.options.points; |
|
398 oldOpts = oldEntry.options.points; |
|
399 if (checkOptions(newOpts, oldOpts)) { |
|
400 return true; |
|
401 } |
|
402 |
|
403 // check for changes in bars options |
|
404 newOpts = newEntry.options.bars; |
|
405 oldOpts = oldEntry.options.bars; |
|
406 if (checkOptions(newOpts, oldOpts)) { |
|
407 return true; |
|
408 } |
|
409 } |
|
410 |
|
411 return false; |
|
412 } |
|
413 |
|
414 function init(plot) { |
|
415 plot.hooks.setupGrid.push(function (plot) { |
|
416 var options = plot.getOptions(); |
|
417 var series = plot.getData(), |
|
418 labelFormatter = options.legend.labelFormatter, |
|
419 oldEntries = options.legend.legendEntries, |
|
420 oldPlotOffset = options.legend.plotOffset, |
|
421 newEntries = getLegendEntries(series, labelFormatter, options.legend.sorted), |
|
422 newPlotOffset = plot.getPlotOffset(); |
|
423 |
|
424 if (shouldRedraw(oldEntries, newEntries) || |
|
425 checkOptions(oldPlotOffset, newPlotOffset)) { |
|
426 insertLegend(plot, options, plot.getPlaceholder(), newEntries); |
|
427 } |
|
428 }); |
|
429 } |
|
430 |
|
431 $.plot.plugins.push({ |
|
432 init: init, |
|
433 options: defaultOptions, |
|
434 name: 'legend', |
|
435 version: '1.0' |
|
436 }); |
|
437 })(jQuery); |