406 lines
15 KiB
JavaScript
406 lines
15 KiB
JavaScript
/* Flot plugin for drawing legends.
|
|
|
|
*/
|
|
|
|
(function($) {
|
|
var defaultOptions = {
|
|
legend: {
|
|
show: false,
|
|
noColumns: 1,
|
|
labelFormatter: null, // fn: string -> string
|
|
container: null, // container (as jQuery object) to put legend in, null means default on top of graph
|
|
position: 'ne', // position of default legend container within plot
|
|
margin: 5, // distance from grid edge to default legend container within plot
|
|
sorted: null // default to no legend sorting
|
|
}
|
|
};
|
|
var series = [];
|
|
function insertLegend() {
|
|
if (options.legend.container != null) {
|
|
$(options.legend.container).html("");
|
|
} else {
|
|
placeholder.find(".legend").remove();
|
|
}
|
|
|
|
if (!options.legend.show) {
|
|
return;
|
|
}
|
|
|
|
var fragments = [], entries = [], rowStarted = false,
|
|
lf = options.legend.labelFormatter, s, label;
|
|
|
|
// Build a list of legend entries, with each having a label and a color
|
|
|
|
for (var i = 0; i < series.length; ++i) {
|
|
s = series[i];
|
|
if (s.label) {
|
|
label = lf ? lf(s.label, s) : s.label;
|
|
if (label) {
|
|
entries.push({
|
|
label: label,
|
|
color: s.color
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort the legend using either the default or a custom comparator
|
|
|
|
if (options.legend.sorted) {
|
|
if ($.isFunction(options.legend.sorted)) {
|
|
entries.sort(options.legend.sorted);
|
|
} else if (options.legend.sorted == "reverse") {
|
|
entries.reverse();
|
|
} else {
|
|
var ascending = options.legend.sorted != "descending";
|
|
entries.sort(function(a, b) {
|
|
return a.label == b.label ? 0 : (
|
|
(a.label < b.label) != ascending ? 1 : -1 // Logical XOR
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Generate markup for the list of entries, in their final order
|
|
|
|
for (var i = 0; i < entries.length; ++i) {
|
|
|
|
var entry = entries[i];
|
|
|
|
if (i % options.legend.noColumns == 0) {
|
|
if (rowStarted)
|
|
fragments.push('</tr>');
|
|
fragments.push('<tr>');
|
|
rowStarted = true;
|
|
}
|
|
|
|
fragments.push(
|
|
'<td class="legendColorBox"><div style="border:1px solid ' + options.legend.labelBoxBorderColor + ';padding:1px"><div style="width:4px;height:0;border:5px solid ' + entry.color + ';overflow:hidden"></div></div></td>' +
|
|
'<td class="legendLabel">' + entry.label + '</td>'
|
|
);
|
|
}
|
|
|
|
if (rowStarted)
|
|
fragments.push('</tr>');
|
|
|
|
if (fragments.length == 0)
|
|
return;
|
|
|
|
var table = '<table style="font-size:smaller;color:' + options.grid.color + '">' + fragments.join("") + '</table>';
|
|
if (options.legend.container != null)
|
|
$(options.legend.container).html(table);
|
|
else {
|
|
var pos = "",
|
|
p = options.legend.position,
|
|
m = options.legend.margin;
|
|
if (m[0] == null)
|
|
m = [m, m];
|
|
if (p.charAt(0) == "n")
|
|
pos += 'top:' + (m[1] + plotOffset.top) + 'px;';
|
|
else if (p.charAt(0) == "s")
|
|
pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;';
|
|
if (p.charAt(1) == "e")
|
|
pos += 'right:' + (m[0] + plotOffset.right) + 'px;';
|
|
else if (p.charAt(1) == "w")
|
|
pos += 'left:' + (m[0] + plotOffset.left) + 'px;';
|
|
var legend = $('<div class="legend">' + table.replace('style="', 'style="position:absolute;' + pos +';') + '</div>').appendTo(placeholder);
|
|
if (options.legend.backgroundOpacity != 0.0) {
|
|
// put in the transparent background
|
|
// separately to avoid blended labels and
|
|
// label boxes
|
|
var c = options.legend.backgroundColor;
|
|
if (c == null) {
|
|
c = options.grid.backgroundColor;
|
|
if (c && typeof c == "string")
|
|
c = $.color.parse(c);
|
|
else
|
|
c = $.color.extract(legend, 'background-color');
|
|
c.a = 1;
|
|
c = c.toString();
|
|
}
|
|
var div = legend.children();
|
|
$('<div style="position:absolute;width:' + div.width() + 'px;height:' + div.height() + 'px;' + pos +'background-color:' + c + ';"> </div>').prependTo(legend).css('opacity', options.legend.backgroundOpacity);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Generate html for a shape
|
|
function getEntryIconHtml(shape) {
|
|
var html = '',
|
|
name = shape.name,
|
|
x = shape.xPos,
|
|
y = shape.yPos,
|
|
fill = shape.fillColor,
|
|
stroke = shape.strokeColor,
|
|
width = shape.strokeWidth;
|
|
switch (name) {
|
|
case 'circle':
|
|
html = '<use xlink:href="#circle" class="legendIcon" ' +
|
|
'x="' + x + '" ' +
|
|
'y="' + y + '" ' +
|
|
'fill="' + fill + '" ' +
|
|
'stroke="' + stroke + '" ' +
|
|
'stroke-width="' + width + '" ' +
|
|
'width="1.5em" height="1.5em"' +
|
|
'/>';
|
|
break;
|
|
case 'diamond':
|
|
html = '<use xlink:href="#diamond" class="legendIcon" ' +
|
|
'x="' + x + '" ' +
|
|
'y="' + y + '" ' +
|
|
'fill="' + fill + '" ' +
|
|
'stroke="' + stroke + '" ' +
|
|
'stroke-width="' + width + '" ' +
|
|
'width="1.5em" height="1.5em"' +
|
|
'/>';
|
|
break;
|
|
case 'cross':
|
|
html = '<use xlink:href="#cross" class="legendIcon" ' +
|
|
'x="' + x + '" ' +
|
|
'y="' + y + '" ' +
|
|
// 'fill="' + fill + '" ' +
|
|
'stroke="' + stroke + '" ' +
|
|
'stroke-width="' + width + '" ' +
|
|
'width="1.5em" height="1.5em"' +
|
|
'/>';
|
|
break;
|
|
case 'rectangle':
|
|
html = '<use xlink:href="#rectangle" class="legendIcon" ' +
|
|
'x="' + x + '" ' +
|
|
'y="' + y + '" ' +
|
|
'fill="' + fill + '" ' +
|
|
'stroke="' + stroke + '" ' +
|
|
'stroke-width="' + width + '" ' +
|
|
'width="1.5em" height="1.5em"' +
|
|
'/>';
|
|
break;
|
|
case 'plus':
|
|
html = '<use xlink:href="#plus" class="legendIcon" ' +
|
|
'x="' + x + '" ' +
|
|
'y="' + y + '" ' +
|
|
// 'fill="' + fill + '" ' +
|
|
'stroke="' + stroke + '" ' +
|
|
'stroke-width="' + width + '" ' +
|
|
'width="1.5em" height="1.5em"' +
|
|
'/>';
|
|
break;
|
|
case 'bar':
|
|
html = '<use xlink:href="#bars" class="legendIcon" ' +
|
|
'x="' + x + '" ' +
|
|
'y="' + y + '" ' +
|
|
'fill="' + fill + '" ' +
|
|
// 'stroke="' + stroke + '" ' +
|
|
// 'stroke-width="' + width + '" ' +
|
|
'width="1.5em" height="1.5em"' +
|
|
'/>';
|
|
break;
|
|
case 'area':
|
|
html = '<use xlink:href="#area" class="legendIcon" ' +
|
|
'x="' + x + '" ' +
|
|
'y="' + y + '" ' +
|
|
'fill="' + fill + '" ' +
|
|
// 'stroke="' + stroke + '" ' +
|
|
// 'stroke-width="' + width + '" ' +
|
|
'width="1.5em" height="1.5em"' +
|
|
'/>';
|
|
break;
|
|
case 'line':
|
|
html = '<use xlink:href="#line" class="legendIcon" ' +
|
|
'x="' + x + '" ' +
|
|
'y="' + y + '" ' +
|
|
// 'fill="' + fill + '" ' +
|
|
'stroke="' + stroke + '" ' +
|
|
'stroke-width="' + width + '" ' +
|
|
'width="1.5em" height="1.5em"' +
|
|
'/>';
|
|
break;
|
|
default:
|
|
// default is circle
|
|
html = '<use xlink:href="#circle" class="legendIcon" ' +
|
|
'x="' + x + '" ' +
|
|
'y="' + y + '" ' +
|
|
'fill="' + fill + '" ' +
|
|
'stroke="' + stroke + '" ' +
|
|
'stroke-width="' + width + '" ' +
|
|
'width="1.5em" height="1.5em"' +
|
|
'/>';
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
// Define svg symbols for shapes
|
|
var svgShapeDefs = '' +
|
|
'<defs>' +
|
|
'<symbol id="line" fill="none" viewBox="-5 -5 25 25">' +
|
|
'<polyline points="0,15 5,5 10,10 15,0"/>' +
|
|
'</symbol>' +
|
|
|
|
'<symbol id="area" stroke-width="1" viewBox="-5 -5 25 25">' +
|
|
'<polyline points="0,15 5,5 10,10 15,0, 15,15, 0,15"/>' +
|
|
'</symbol>' +
|
|
|
|
'<symbol id="bars" stroke-width="1" viewBox="-5 -5 25 25">' +
|
|
'<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"/>' +
|
|
'</symbol>' +
|
|
|
|
'<symbol id="circle" viewBox="-5 -5 25 25">' +
|
|
'<circle cx="0" cy="15" r="2.5"/>' +
|
|
'<circle cx="5" cy="5" r="2.5"/>' +
|
|
'<circle cx="10" cy="10" r="2.5"/>' +
|
|
'<circle cx="15" cy="0" r="2.5"/>' +
|
|
'</symbol>' +
|
|
|
|
'<symbol id="rectangle" viewBox="-5 -5 25 25">' +
|
|
'<rect x="-2.1" y="12.9" width="4.2" height="4.2"/>' +
|
|
'<rect x="2.9" y="2.9" width="4.2" height="4.2"/>' +
|
|
'<rect x="7.9" y="7.9" width="4.2" height="4.2"/>' +
|
|
'<rect x="12.9" y="-2.1" width="4.2" height="4.2"/>' +
|
|
'</symbol>' +
|
|
|
|
'<symbol id="diamond" viewBox="-5 -5 25 25">' +
|
|
'<path d="M-3,15 L0,12 L3,15, L0,18 Z"/>' +
|
|
'<path d="M2,5 L5,2 L8,5, L5,8 Z"/>' +
|
|
'<path d="M7,10 L10,7 L13,10, L10,13 Z"/>' +
|
|
'<path d="M12,0 L15,-3 L18,0, L15,3 Z"/>' +
|
|
'</symbol>' +
|
|
|
|
'<symbol id="cross" fill="none" viewBox="-5 -5 25 25">' +
|
|
'<path d="M-2.1,12.9 L2.1,17.1, M2.1,12.9 L-2.1,17.1 Z"/>' +
|
|
'<path d="M2.9,2.9 L7.1,7.1 M7.1,2.9 L2.9,7.1 Z"/>' +
|
|
'<path d="M7.9,7.9 L12.1,12.1 M12.1,7.9 L7.9,12.1 Z"/>' +
|
|
'<path d="M12.9,-2.1 L17.1,2.1 M17.1,-2.1 L12.9,2.1 Z"/>' +
|
|
'</symbol>' +
|
|
|
|
'<symbol id="plus" fill="none" viewBox="-5 -5 25 25">' +
|
|
'<path d="M0,12 L0,18, M-3,15 L3,15 Z"/>' +
|
|
'<path d="M5,2 L5,8 M2,5 L8,5 Z"/>' +
|
|
'<path d="M10,7 L10,13 M7,10 L13,10 Z"/>' +
|
|
'<path d="M15,-3 L15,3 M12,0 L18,0 Z"/>' +
|
|
'</symbol>' +
|
|
'</defs>';
|
|
|
|
// Generate a list of legend entries in their final order
|
|
function getLegendEntries(series, labelFormatter, sorted) {
|
|
var lf = labelFormatter,
|
|
legendEntries = series.reduce(function(validEntries, s, i) {
|
|
var labelEval = (lf ? lf(s.label, s) : s.label)
|
|
if (s.hasOwnProperty("label") ? labelEval : true) {
|
|
var entry = {
|
|
label: labelEval || 'Plot ' + (i + 1),
|
|
color: s.color,
|
|
options: {
|
|
lines: s.lines,
|
|
points: s.points,
|
|
bars: s.bars
|
|
}
|
|
}
|
|
validEntries.push(entry)
|
|
}
|
|
return validEntries;
|
|
}, []);
|
|
|
|
// Sort the legend using either the default or a custom comparator
|
|
if (sorted) {
|
|
if ($.isFunction(sorted)) {
|
|
legendEntries.sort(sorted);
|
|
} else if (sorted === 'reverse') {
|
|
legendEntries.reverse();
|
|
} else {
|
|
var ascending = (sorted !== 'descending');
|
|
legendEntries.sort(function(a, b) {
|
|
return a.label === b.label
|
|
? 0
|
|
: ((a.label < b.label) !== ascending ? 1 : -1 // Logical XOR
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
return legendEntries;
|
|
}
|
|
|
|
// return false if opts1 same as opts2
|
|
function checkOptions(opts1, opts2) {
|
|
for (var prop in opts1) {
|
|
if (opts1.hasOwnProperty(prop)) {
|
|
if (opts1[prop] !== opts2[prop]) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Compare two lists of legend entries
|
|
function shouldRedraw(oldEntries, newEntries) {
|
|
if (!oldEntries || !newEntries) {
|
|
return true;
|
|
}
|
|
|
|
if (oldEntries.length !== newEntries.length) {
|
|
return true;
|
|
}
|
|
var i, newEntry, oldEntry, newOpts, oldOpts;
|
|
for (i = 0; i < newEntries.length; i++) {
|
|
newEntry = newEntries[i];
|
|
oldEntry = oldEntries[i];
|
|
|
|
if (newEntry.label !== oldEntry.label) {
|
|
return true;
|
|
}
|
|
|
|
if (newEntry.color !== oldEntry.color) {
|
|
return true;
|
|
}
|
|
|
|
// check for changes in lines options
|
|
newOpts = newEntry.options.lines;
|
|
oldOpts = oldEntry.options.lines;
|
|
if (checkOptions(newOpts, oldOpts)) {
|
|
return true;
|
|
}
|
|
|
|
// check for changes in points options
|
|
newOpts = newEntry.options.points;
|
|
oldOpts = oldEntry.options.points;
|
|
if (checkOptions(newOpts, oldOpts)) {
|
|
return true;
|
|
}
|
|
|
|
// check for changes in bars options
|
|
newOpts = newEntry.options.bars;
|
|
oldOpts = oldEntry.options.bars;
|
|
if (checkOptions(newOpts, oldOpts)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function init(plot) {
|
|
plot.hooks.setupGrid.push(function (plot) {
|
|
series = plot.getData()
|
|
var options = plot.getOptions();
|
|
var labelFormatter = options.legend.labelFormatter,
|
|
oldEntries = options.legend.legendEntries,
|
|
oldPlotOffset = options.legend.plotOffset,
|
|
newEntries = getLegendEntries(series, labelFormatter, options.legend.sorted),
|
|
newPlotOffset = plot.getPlotOffset();
|
|
|
|
if (shouldRedraw(oldEntries, newEntries) ||
|
|
checkOptions(oldPlotOffset, newPlotOffset)) {
|
|
insertLegend(plot, options, plot.getPlaceholder(), newEntries);
|
|
}
|
|
});
|
|
}
|
|
|
|
$.plot.plugins.push({
|
|
init: init,
|
|
options: defaultOptions,
|
|
name: 'legend',
|
|
version: '1.0'
|
|
});
|
|
})(jQuery);
|