Chart.js/src/core/core.legend.js
2016-05-04 21:06:19 -04:00

351 lines
9.7 KiB
JavaScript

"use strict";
module.exports = function(Chart) {
var helpers = Chart.helpers;
var noop = helpers.noop;
Chart.defaults.global.legend = {
display: true,
position: 'top',
fullWidth: true, // marks that this box should take the full width of the canvas (pushing down other boxes)
reverse: false,
// a callback that will handle
onClick: function(e, legendItem) {
var index = legendItem.datasetIndex;
var ci = this.chart;
var meta = ci.getDatasetMeta(index);
// See controller.isDatasetVisible comment
meta.hidden = meta.hidden === null? !ci.data.datasets[index].hidden : null;
// We hid a dataset ... rerender the chart
ci.update();
},
labels: {
boxWidth: 40,
padding: 10,
// Generates labels shown in the legend
// Valid properties to return:
// text : text to display
// fillStyle : fill of coloured box
// strokeStyle: stroke of coloured box
// hidden : if this legend item refers to a hidden item
// lineCap : cap style for line
// lineDash
// lineDashOffset :
// lineJoin :
// lineWidth :
generateLabels: function(chart) {
var data = chart.data;
return helpers.isArray(data.datasets) ? data.datasets.map(function(dataset, i) {
return {
text: dataset.label,
fillStyle: dataset.backgroundColor,
hidden: !chart.isDatasetVisible(i),
lineCap: dataset.borderCapStyle,
lineDash: dataset.borderDash,
lineDashOffset: dataset.borderDashOffset,
lineJoin: dataset.borderJoinStyle,
lineWidth: dataset.borderWidth,
strokeStyle: dataset.borderColor,
// Below is extra data used for toggling the datasets
datasetIndex: i
};
}, this) : [];
}
}
};
Chart.Legend = Chart.Element.extend({
initialize: function(config) {
helpers.extend(this, config);
// Contains hit boxes for each dataset (in dataset order)
this.legendHitBoxes = [];
// Are we in doughnut mode which has a different data type
this.doughnutMode = false;
},
// These methods are ordered by lifecyle. Utilities then follow.
// Any function defined here is inherited by all legend types.
// Any function can be extended by the legend type
beforeUpdate: noop,
update: function(maxWidth, maxHeight, margins) {
// Update Lifecycle - Probably don't want to ever extend or overwrite this function ;)
this.beforeUpdate();
// Absorb the master measurements
this.maxWidth = maxWidth;
this.maxHeight = maxHeight;
this.margins = margins;
// Dimensions
this.beforeSetDimensions();
this.setDimensions();
this.afterSetDimensions();
// Labels
this.beforeBuildLabels();
this.buildLabels();
this.afterBuildLabels();
// Fit
this.beforeFit();
this.fit();
this.afterFit();
//
this.afterUpdate();
return this.minSize;
},
afterUpdate: noop,
//
beforeSetDimensions: noop,
setDimensions: function() {
// Set the unconstrained dimension before label rotation
if (this.isHorizontal()) {
// Reset position before calculating rotation
this.width = this.maxWidth;
this.left = 0;
this.right = this.width;
} else {
this.height = this.maxHeight;
// Reset position before calculating rotation
this.top = 0;
this.bottom = this.height;
}
// Reset padding
this.paddingLeft = 0;
this.paddingTop = 0;
this.paddingRight = 0;
this.paddingBottom = 0;
// Reset minSize
this.minSize = {
width: 0,
height: 0
};
},
afterSetDimensions: noop,
//
beforeBuildLabels: noop,
buildLabels: function() {
this.legendItems = this.options.labels.generateLabels.call(this, this.chart);
if(this.options.reverse){
this.legendItems.reverse();
}
},
afterBuildLabels: noop,
//
beforeFit: noop,
fit: function() {
var opts = this.options;
var labelOpts = opts.labels;
var display = opts.display;
var ctx = this.ctx;
var globalDefault = Chart.defaults.global,
itemOrDefault = helpers.getValueOrDefault,
fontSize = itemOrDefault(labelOpts.fontSize, globalDefault.defaultFontSize),
fontStyle = itemOrDefault(labelOpts.fontStyle, globalDefault.defaultFontStyle),
fontFamily = itemOrDefault(labelOpts.fontFamily, globalDefault.defaultFontFamily),
labelFont = helpers.fontString(fontSize, fontStyle, fontFamily);
// Reset hit boxes
var hitboxes = this.legendHitBoxes = [];
var minSize = this.minSize;
var isHorizontal = this.isHorizontal();
if (isHorizontal) {
minSize.width = this.maxWidth; // fill all the width
minSize.height = display ? 10 : 0;
} else {
minSize.width = display ? 10 : 0;
minSize.height = this.maxHeight; // fill all the height
}
// Increase sizes here
if (display) {
if (isHorizontal) {
// Labels
// Width of each line of legend boxes. Labels wrap onto multiple lines when there are too many to fit on one
var lineWidths = this.lineWidths = [0];
var totalHeight = this.legendItems.length ? fontSize + (labelOpts.padding) : 0;
ctx.textAlign = "left";
ctx.textBaseline = 'top';
ctx.font = labelFont;
helpers.each(this.legendItems, function(legendItem, i) {
var width = labelOpts.boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width;
if (lineWidths[lineWidths.length - 1] + width + labelOpts.padding >= this.width) {
totalHeight += fontSize + (labelOpts.padding);
lineWidths[lineWidths.length] = this.left;
}
// Store the hitbox width and height here. Final position will be updated in `draw`
hitboxes[i] = {
left: 0,
top: 0,
width: width,
height: fontSize
};
lineWidths[lineWidths.length - 1] += width + labelOpts.padding;
}, this);
minSize.height += totalHeight;
} else {
// TODO vertical
}
}
this.width = minSize.width;
this.height = minSize.height;
},
afterFit: noop,
// Shared Methods
isHorizontal: function() {
return this.options.position === "top" || this.options.position === "bottom";
},
// Actualy draw the legend on the canvas
draw: function() {
var opts = this.options;
var labelOpts = opts.labels;
var globalDefault = Chart.defaults.global,
lineDefault = globalDefault.elements.line,
legendWidth = this.width,
lineWidths = this.lineWidths;
if (opts.display) {
var ctx = this.ctx,
cursor = {
x: this.left + ((legendWidth - lineWidths[0]) / 2),
y: this.top + labelOpts.padding,
line: 0
},
itemOrDefault = helpers.getValueOrDefault,
fontColor = itemOrDefault(labelOpts.fontColor, globalDefault.defaultFontColor),
fontSize = itemOrDefault(labelOpts.fontSize, globalDefault.defaultFontSize),
fontStyle = itemOrDefault(labelOpts.fontStyle, globalDefault.defaultFontStyle),
fontFamily = itemOrDefault(labelOpts.fontFamily, globalDefault.defaultFontFamily),
labelFont = helpers.fontString(fontSize, fontStyle, fontFamily);
// Horizontal
if (this.isHorizontal()) {
// Labels
ctx.textAlign = "left";
ctx.textBaseline = 'top';
ctx.lineWidth = 0.5;
ctx.strokeStyle = fontColor; // for strikethrough effect
ctx.fillStyle = fontColor; // render in correct colour
ctx.font = labelFont;
var boxWidth = labelOpts.boxWidth,
hitboxes = this.legendHitBoxes;
helpers.each(this.legendItems, function(legendItem, i) {
var textWidth = ctx.measureText(legendItem.text).width,
width = boxWidth + (fontSize / 2) + textWidth,
x = cursor.x,
y = cursor.y;
if (x + width >= legendWidth) {
y = cursor.y += fontSize + (labelOpts.padding);
cursor.line++;
x = cursor.x = this.left + ((legendWidth - lineWidths[cursor.line]) / 2);
}
// Set the ctx for the box
ctx.save();
ctx.fillStyle = itemOrDefault(legendItem.fillStyle, globalDefault.defaultColor);
ctx.lineCap = itemOrDefault(legendItem.lineCap, lineDefault.borderCapStyle);
ctx.lineDashOffset = itemOrDefault(legendItem.lineDashOffset, lineDefault.borderDashOffset);
ctx.lineJoin = itemOrDefault(legendItem.lineJoin, lineDefault.borderJoinStyle);
ctx.lineWidth = itemOrDefault(legendItem.lineWidth, lineDefault.borderWidth);
ctx.strokeStyle = itemOrDefault(legendItem.strokeStyle, globalDefault.defaultColor);
if (ctx.setLineDash) {
// IE 9 and 10 do not support line dash
ctx.setLineDash(itemOrDefault(legendItem.lineDash, lineDefault.borderDash));
}
// Draw the box
ctx.strokeRect(x, y, boxWidth, fontSize);
ctx.fillRect(x, y, boxWidth, fontSize);
ctx.restore();
hitboxes[i].left = x;
hitboxes[i].top = y;
// Fill the actual label
ctx.fillText(legendItem.text, boxWidth + (fontSize / 2) + x, y);
if (legendItem.hidden) {
// Strikethrough the text if hidden
ctx.beginPath();
ctx.lineWidth = 2;
ctx.moveTo(boxWidth + (fontSize / 2) + x, y + (fontSize / 2));
ctx.lineTo(boxWidth + (fontSize / 2) + x + textWidth, y + (fontSize / 2));
ctx.stroke();
}
cursor.x += width + (labelOpts.padding);
}, this);
} else {
}
}
},
// Handle an event
handleEvent: function(e) {
var position = helpers.getRelativePosition(e, this.chart.chart),
x = position.x,
y = position.y,
opts = this.options;
if (x >= this.left && x <= this.right && y >= this.top && y <= this.bottom) {
// See if we are touching one of the dataset boxes
var lh = this.legendHitBoxes;
for (var i = 0; i < lh.length; ++i) {
var hitBox = lh[i];
if (x >= hitBox.left && x <= hitBox.left + hitBox.width && y >= hitBox.top && y <= hitBox.top + hitBox.height) {
// Touching an element
if (opts.onClick) {
opts.onClick.call(this, e, this.legendItems[i]);
}
break;
}
}
}
}
});
};