"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) { cursor.y += fontSize + (labelOpts.padding); cursor.line++; 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; } } } } }); };