diff --git a/src/core/core.layoutService.js b/src/core/core.layoutService.js index bd82e4780e9..9d40a2e407f 100644 --- a/src/core/core.layoutService.js +++ b/src/core/core.layoutService.js @@ -4,30 +4,90 @@ module.exports = function(Chart) { var helpers = Chart.helpers; + function filterByPosition(array, position) { + return helpers.where(array, function(v) { + return v.position === position; + }); + } + + function sortByWeight(array, reverse) { + array.forEach(function(v, i) { + v._tmpIndex_ = i; + return v; + }); + array.sort(function(a, b) { + var v0 = reverse ? b : a; + var v1 = reverse ? a : b; + return v0.weight === v1.weight ? + v0._tmpIndex_ - v1._tmpIndex_ : + v0.weight - v1.weight; + }); + array.forEach(function(v) { + delete v._tmpIndex_; + }); + } + + /** + * @interface ILayoutItem + * @prop {String} position - The position of the item in the chart layout. Possible values are + * 'left', 'top', 'right', 'bottom', and 'chartArea' + * @prop {Number} weight - The weight used to sort the item. Higher weights are further away from the chart area + * @prop {Boolean} fullWidth - if true, and the item is horizontal, then push vertical boxes down + * @prop {Function} isHorizontal - returns true if the layout item is horizontal (ie. top or bottom) + * @prop {Function} update - Takes two parameters: width and height. Returns size of item + * @prop {Function} getPadding - Returns an object with padding on the edges + * @prop {Number} width - Width of item. Must be valid after update() + * @prop {Number} height - Height of item. Must be valid after update() + * @prop {Number} left - Left edge of the item. Set by layout system and cannot be used in update + * @prop {Number} top - Top edge of the item. Set by layout system and cannot be used in update + * @prop {Number} right - Right edge of the item. Set by layout system and cannot be used in update + * @prop {Number} bottom - Bottom edge of the item. Set by layout system and cannot be used in update + */ + // The layout service is very self explanatory. It's responsible for the layout within a chart. // Scales, Legends and Plugins all rely on the layout service and can easily register to be placed anywhere they need // It is this service's responsibility of carrying out that layout. Chart.layoutService = { defaults: {}, - // Register a box to a chart. A box is simply a reference to an object that requires layout. eg. Scales, Legend, Plugins. - addBox: function(chart, box) { + /** + * Register a box to a chart. + * A box is simply a reference to an object that requires layout. eg. Scales, Legend, Title. + * @param {Chart} chart - the chart to use + * @param {ILayoutItem} layoutItem - the item to add to be layed out + */ + addBox: function(chart, layoutItem) { if (!chart.boxes) { chart.boxes = []; } - chart.boxes.push(box); + + // Ensure that all layout items have a weight + if (!layoutItem.weight) { + layoutItem.weight = 0; + } + chart.boxes.push(layoutItem); }, - removeBox: function(chart, box) { + /** + * Remove a layoutItem from a chart + * @param {Chart} chart - the chart to remove the box from + * @param {Object} layoutItem - the item to remove from the layout + */ + removeBox: function(chart, layoutItem) { if (!chart.boxes) { return; } - chart.boxes.splice(chart.boxes.indexOf(box), 1); + chart.boxes.splice(chart.boxes.indexOf(layoutItem), 1); }, - // The most important function + /** + * Fits boxes of the given chart into the given size by having each box measure itself + * then running a fitting algorithm + * @param {Chart} chart - the chart + * @param {Number} width - the width to fit into + * @param {Number} height - the height to fit into + */ update: function(chart, width, height) { - if (!chart) { return; } @@ -53,31 +113,17 @@ module.exports = function(Chart) { bottomPadding = padding.bottom || 0; } - var leftBoxes = helpers.where(chart.boxes, function(box) { - return box.options.position === 'left'; - }); - var rightBoxes = helpers.where(chart.boxes, function(box) { - return box.options.position === 'right'; - }); - var topBoxes = helpers.where(chart.boxes, function(box) { - return box.options.position === 'top'; - }); - var bottomBoxes = helpers.where(chart.boxes, function(box) { - return box.options.position === 'bottom'; - }); - - // Boxes that overlay the chartarea such as the radialLinear scale - var chartAreaBoxes = helpers.where(chart.boxes, function(box) { - return box.options.position === 'chartArea'; - }); + var leftBoxes = filterByPosition(chart.boxes, 'left'); + var rightBoxes = filterByPosition(chart.boxes, 'right'); + var topBoxes = filterByPosition(chart.boxes, 'top'); + var bottomBoxes = filterByPosition(chart.boxes, 'bottom'); + var chartAreaBoxes = filterByPosition(chart.boxes, 'chartArea'); - // Ensure that full width boxes are at the very top / bottom - topBoxes.sort(function(a, b) { - return (b.options.fullWidth ? 1 : 0) - (a.options.fullWidth ? 1 : 0); - }); - bottomBoxes.sort(function(a, b) { - return (a.options.fullWidth ? 1 : 0) - (b.options.fullWidth ? 1 : 0); - }); + // Sort boxes by weight. A higher weight is further away from the chart area + sortByWeight(leftBoxes, true); + sortByWeight(rightBoxes, false); + sortByWeight(topBoxes, true); + sortByWeight(bottomBoxes, false); // Essentially we now have any number of boxes on each of the 4 sides. // Our canvas looks like the following. @@ -138,7 +184,7 @@ module.exports = function(Chart) { var isHorizontal = box.isHorizontal(); if (isHorizontal) { - minSize = box.update(box.options.fullWidth ? chartWidth : maxChartAreaWidth, horizontalBoxHeight); + minSize = box.update(box.fullWidth ? chartWidth : maxChartAreaWidth, horizontalBoxHeight); maxChartAreaHeight -= minSize.height; } else { minSize = box.update(verticalBoxWidth, chartAreaHeight); @@ -201,7 +247,7 @@ module.exports = function(Chart) { // Don't use min size here because of label rotation. When the labels are rotated, their rotation highly depends // on the margin. Sometimes they need to increase in size slightly - box.update(box.options.fullWidth ? chartWidth : maxChartAreaWidth, chartHeight / 2, scaleMargin); + box.update(box.fullWidth ? chartWidth : maxChartAreaWidth, chartHeight / 2, scaleMargin); } else { box.update(minBoxSize.minSize.width, maxChartAreaHeight); } @@ -297,13 +343,13 @@ module.exports = function(Chart) { }); helpers.each(topBoxes, function(box) { - if (!box.options.fullWidth) { + if (!box.fullWidth) { box.width = newMaxChartAreaWidth; } }); helpers.each(bottomBoxes, function(box) { - if (!box.options.fullWidth) { + if (!box.fullWidth) { box.width = newMaxChartAreaWidth; } }); @@ -318,8 +364,8 @@ module.exports = function(Chart) { function placeBox(box) { if (box.isHorizontal()) { - box.left = box.options.fullWidth ? leftPadding : totalLeftBoxesWidth; - box.right = box.options.fullWidth ? width - rightPadding : totalLeftBoxesWidth + maxChartAreaWidth; + box.left = box.fullWidth ? leftPadding : totalLeftBoxesWidth; + box.right = box.fullWidth ? width - rightPadding : totalLeftBoxesWidth + maxChartAreaWidth; box.top = top; box.bottom = top + box.height; diff --git a/src/core/core.legend.js b/src/core/core.legend.js index 9e181193208..aace498bfe1 100644 --- a/src/core/core.legend.js +++ b/src/core/core.legend.js @@ -495,7 +495,13 @@ module.exports = function(Chart) { var legend = new Chart.Legend({ ctx: chart.ctx, options: legendOpts, - chart: chart + chart: chart, + + // ILayoutItem parameters for layout service + // pick a large number to ensure we are on the outside after any axes + weight: 1000, + position: legendOpts.position, + fullWidth: legendOpts.fullWidth, }); chart.legend = legend; Chart.layoutService.addBox(chart, legend); diff --git a/src/core/core.scaleService.js b/src/core/core.scaleService.js index 4f3dea242cb..9923bc922af 100644 --- a/src/core/core.scaleService.js +++ b/src/core/core.scaleService.js @@ -33,6 +33,9 @@ module.exports = function(Chart) { addScalesToLayout: function(chart) { // Adds each scale to the chart.boxes array to be sized accordingly helpers.each(chart.scales, function(scale) { + // Set ILayoutItem parameters for backwards compatibility + scale.fullWidth = scale.options.fullWidth; + scale.position = scale.options.position; Chart.layoutService.addBox(chart, scale); }); } diff --git a/src/core/core.title.js b/src/core/core.title.js index a1a3c8f4309..fe7674d4ae1 100644 --- a/src/core/core.title.js +++ b/src/core/core.title.js @@ -185,7 +185,12 @@ module.exports = function(Chart) { var title = new Chart.Title({ ctx: chart.ctx, options: titleOpts, - chart: chart + chart: chart, + + // ILayoutItem parameters + weight: 2000, // greater than legend to be above + position: titleOpts.position, + fullWidth: titleOpts.fullWidth, }); chart.titleBlock = title; Chart.layoutService.addBox(chart, title);