UNPKG

16.7 kBJavaScriptView Raw
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 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);