You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
437 lines
16 KiB
437 lines
16 KiB
/* 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
|
|
}
|
|
};
|
|
|
|
function insertLegend(plot, options, placeholder, legendEntries) {
|
|
// clear before redraw
|
|
if (options.legend.container != null) {
|
|
$(options.legend.container).html('');
|
|
} else {
|
|
placeholder.find('.legend').remove();
|
|
}
|
|
|
|
if (!options.legend.show) {
|
|
return;
|
|
}
|
|
|
|
// Save the legend entries in legend options
|
|
var entries = options.legend.legendEntries = legendEntries,
|
|
plotOffset = options.legend.plotOffset = plot.getPlotOffset(),
|
|
html = [],
|
|
entry, labelHtml, iconHtml,
|
|
j = 0,
|
|
i,
|
|
pos = "",
|
|
p = options.legend.position,
|
|
m = options.legend.margin,
|
|
shape = {
|
|
name: '',
|
|
label: '',
|
|
xPos: '',
|
|
yPos: ''
|
|
};
|
|
|
|
html[j++] = '<svg class="legendLayer" style="width:inherit;height:inherit;">';
|
|
html[j++] = '<rect class="background" width="100%" height="100%"/>';
|
|
html[j++] = svgShapeDefs;
|
|
|
|
var left = 0;
|
|
var columnWidths = [];
|
|
var style = window.getComputedStyle(document.querySelector('body'));
|
|
for (i = 0; i < entries.length; ++i) {
|
|
let columnIndex = i % options.legend.noColumns;
|
|
entry = entries[i];
|
|
shape.label = entry.label;
|
|
var info = plot.getSurface().getTextInfo('', shape.label, {
|
|
style: style.fontStyle,
|
|
variant: style.fontVariant,
|
|
weight: style.fontWeight,
|
|
size: parseInt(style.fontSize),
|
|
lineHeight: parseInt(style.lineHeight),
|
|
family: style.fontFamily
|
|
});
|
|
|
|
var labelWidth = info.width;
|
|
// 36px = 1.5em + 6px margin
|
|
var iconWidth = 48;
|
|
if (columnWidths[columnIndex]) {
|
|
if (labelWidth > columnWidths[columnIndex]) {
|
|
columnWidths[columnIndex] = labelWidth + iconWidth;
|
|
}
|
|
} else {
|
|
columnWidths[columnIndex] = labelWidth + iconWidth;
|
|
}
|
|
}
|
|
|
|
// Generate html for icons and labels from a list of entries
|
|
for (i = 0; i < entries.length; ++i) {
|
|
let columnIndex = i % options.legend.noColumns;
|
|
entry = entries[i];
|
|
iconHtml = '';
|
|
shape.label = entry.label;
|
|
shape.xPos = (left + 3) + 'px';
|
|
left += columnWidths[columnIndex];
|
|
if ((i + 1) % options.legend.noColumns === 0) {
|
|
left = 0;
|
|
}
|
|
shape.yPos = Math.floor(i / options.legend.noColumns) * 1.5 + 'em';
|
|
// area
|
|
if (entry.options.lines.show && entry.options.lines.fill) {
|
|
shape.name = 'area';
|
|
shape.fillColor = entry.color;
|
|
iconHtml += getEntryIconHtml(shape);
|
|
}
|
|
// bars
|
|
if (entry.options.bars.show) {
|
|
shape.name = 'bar';
|
|
shape.fillColor = entry.color;
|
|
iconHtml += getEntryIconHtml(shape);
|
|
}
|
|
// lines
|
|
if (entry.options.lines.show && !entry.options.lines.fill) {
|
|
shape.name = 'line';
|
|
shape.strokeColor = entry.color;
|
|
shape.strokeWidth = entry.options.lines.lineWidth;
|
|
iconHtml += getEntryIconHtml(shape);
|
|
}
|
|
// points
|
|
if (entry.options.points.show) {
|
|
shape.name = entry.options.points.symbol;
|
|
shape.strokeColor = entry.color;
|
|
shape.fillColor = entry.options.points.fillColor;
|
|
shape.strokeWidth = entry.options.points.lineWidth;
|
|
iconHtml += getEntryIconHtml(shape);
|
|
}
|
|
|
|
labelHtml = '<text x="' + shape.xPos + '" y="' + shape.yPos + '" text-anchor="start"><tspan dx="2em" dy="1.2em">' + shape.label + '</tspan></text>'
|
|
html[j++] = '<g>' + iconHtml + labelHtml + '</g>';
|
|
}
|
|
|
|
html[j++] = '</svg>';
|
|
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 width = 6;
|
|
for (i = 0; i < columnWidths.length; ++i) {
|
|
width += columnWidths[i];
|
|
}
|
|
|
|
var legendEl,
|
|
height = Math.ceil(entries.length / options.legend.noColumns) * 1.6;
|
|
if (!options.legend.container) {
|
|
legendEl = $('<div class="legend" style="position:absolute;' + pos + '">' + html.join('') + '</div>').appendTo(placeholder);
|
|
legendEl.css('width', width + 'px');
|
|
legendEl.css('height', height + 'em');
|
|
legendEl.css('pointerEvents', 'none');
|
|
} else {
|
|
legendEl = $(html.join('')).appendTo(options.legend.container)[0];
|
|
options.legend.container.style.width = width + 'px';
|
|
options.legend.container.style.height = height + 'em';
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
var options = plot.getOptions();
|
|
var series = plot.getData(),
|
|
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);
|
|
|