2016-02-12 04:30:53 +01:00
|
|
|
"use strict";
|
|
|
|
|
|
|
|
module.exports = function(Chart) {
|
|
|
|
|
2016-02-12 07:16:43 +01:00
|
|
|
var helpers = Chart.helpers
|
2016-02-12 04:30:53 +01:00
|
|
|
|
|
|
|
// 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 chartInstance. A box is simply a reference to an object that requires layout. eg. Scales, Legend, Plugins.
|
|
|
|
addBox: function(chartInstance, box) {
|
|
|
|
if (!chartInstance.boxes) {
|
|
|
|
chartInstance.boxes = [];
|
|
|
|
}
|
|
|
|
chartInstance.boxes.push(box);
|
|
|
|
},
|
|
|
|
|
|
|
|
removeBox: function(chartInstance, box) {
|
|
|
|
if (!chartInstance.boxes) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
chartInstance.boxes.splice(chartInstance.boxes.indexOf(box), 1);
|
|
|
|
},
|
|
|
|
|
|
|
|
// The most important function
|
|
|
|
update: function(chartInstance, width, height) {
|
|
|
|
|
|
|
|
if (!chartInstance) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var xPadding = width > 30 ? 5 : 2;
|
|
|
|
var yPadding = height > 30 ? 5 : 2;
|
|
|
|
|
|
|
|
var leftBoxes = helpers.where(chartInstance.boxes, function(box) {
|
|
|
|
return box.options.position == "left";
|
|
|
|
});
|
|
|
|
var rightBoxes = helpers.where(chartInstance.boxes, function(box) {
|
|
|
|
return box.options.position == "right";
|
|
|
|
});
|
|
|
|
var topBoxes = helpers.where(chartInstance.boxes, function(box) {
|
|
|
|
return box.options.position == "top";
|
|
|
|
});
|
|
|
|
var bottomBoxes = helpers.where(chartInstance.boxes, function(box) {
|
|
|
|
return box.options.position == "bottom";
|
|
|
|
});
|
|
|
|
|
|
|
|
// Boxes that overlay the chartarea such as the radialLinear scale
|
|
|
|
var chartAreaBoxes = helpers.where(chartInstance.boxes, function(box) {
|
|
|
|
return box.options.position == "chartArea";
|
|
|
|
});
|
|
|
|
|
|
|
|
function fullWidthSorter(a, b) {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Essentially we now have any number of boxes on each of the 4 sides.
|
|
|
|
// Our canvas looks like the following.
|
|
|
|
// The areas L1 and L2 are the left axes. R1 is the right axis, T1 is the top axis and
|
|
|
|
// B1 is the bottom axis
|
|
|
|
// There are also 4 quadrant-like locations (left to right instead of clockwise) reserved for chart overlays
|
|
|
|
// These locations are single-box locations only, when trying to register a chartArea location that is already taken,
|
|
|
|
// an error will be thrown.
|
|
|
|
//
|
|
|
|
// |----------------------------------------------------|
|
|
|
|
// | T1 (Full Width) |
|
|
|
|
// |----------------------------------------------------|
|
|
|
|
// | | | T2 | |
|
|
|
|
// | |----|-------------------------------------|----|
|
|
|
|
// | | | C1 | | C2 | |
|
|
|
|
// | | |----| |----| |
|
|
|
|
// | | | | |
|
|
|
|
// | L1 | L2 | ChartArea (C0) | R1 |
|
|
|
|
// | | | | |
|
|
|
|
// | | |----| |----| |
|
|
|
|
// | | | C3 | | C4 | |
|
|
|
|
// | |----|-------------------------------------|----|
|
|
|
|
// | | | B1 | |
|
|
|
|
// |----------------------------------------------------|
|
|
|
|
// | B2 (Full Width) |
|
|
|
|
// |----------------------------------------------------|
|
|
|
|
//
|
|
|
|
// What we do to find the best sizing, we do the following
|
|
|
|
// 1. Determine the minimum size of the chart area.
|
|
|
|
// 2. Split the remaining width equally between each vertical axis
|
|
|
|
// 3. Split the remaining height equally between each horizontal axis
|
|
|
|
// 4. Give each layout the maximum size it can be. The layout will return it's minimum size
|
|
|
|
// 5. Adjust the sizes of each axis based on it's minimum reported size.
|
|
|
|
// 6. Refit each axis
|
|
|
|
// 7. Position each axis in the final location
|
|
|
|
// 8. Tell the chart the final location of the chart area
|
|
|
|
// 9. Tell any axes that overlay the chart area the positions of the chart area
|
|
|
|
|
|
|
|
// Step 1
|
|
|
|
var chartWidth = width - (2 * xPadding);
|
|
|
|
var chartHeight = height - (2 * yPadding);
|
|
|
|
var chartAreaWidth = chartWidth / 2; // min 50%
|
|
|
|
var chartAreaHeight = chartHeight / 2; // min 50%
|
|
|
|
|
|
|
|
// Step 2
|
|
|
|
var verticalBoxWidth = (width - chartAreaWidth) / (leftBoxes.length + rightBoxes.length);
|
|
|
|
|
|
|
|
// Step 3
|
|
|
|
var horizontalBoxHeight = (height - chartAreaHeight) / (topBoxes.length + bottomBoxes.length);
|
|
|
|
|
|
|
|
// Step 4
|
|
|
|
var maxChartAreaWidth = chartWidth;
|
|
|
|
var maxChartAreaHeight = chartHeight;
|
|
|
|
var minBoxSizes = [];
|
|
|
|
|
|
|
|
helpers.each(leftBoxes.concat(rightBoxes, topBoxes, bottomBoxes), getMinimumBoxSize);
|
|
|
|
|
|
|
|
function getMinimumBoxSize(box) {
|
|
|
|
var minSize;
|
|
|
|
var isHorizontal = box.isHorizontal();
|
|
|
|
|
|
|
|
if (isHorizontal) {
|
|
|
|
minSize = box.update(box.options.fullWidth ? chartWidth : maxChartAreaWidth, horizontalBoxHeight);
|
|
|
|
maxChartAreaHeight -= minSize.height;
|
|
|
|
} else {
|
|
|
|
minSize = box.update(verticalBoxWidth, chartAreaHeight);
|
|
|
|
maxChartAreaWidth -= minSize.width;
|
|
|
|
}
|
|
|
|
|
|
|
|
minBoxSizes.push({
|
|
|
|
horizontal: isHorizontal,
|
|
|
|
minSize: minSize,
|
|
|
|
box: box,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// At this point, maxChartAreaHeight and maxChartAreaWidth are the size the chart area could
|
|
|
|
// be if the axes are drawn at their minimum sizes.
|
|
|
|
|
|
|
|
// Steps 5 & 6
|
|
|
|
var totalLeftBoxesWidth = xPadding;
|
|
|
|
var totalRightBoxesWidth = xPadding;
|
|
|
|
var totalTopBoxesHeight = yPadding;
|
|
|
|
var totalBottomBoxesHeight = yPadding;
|
|
|
|
|
|
|
|
// Update, and calculate the left and right margins for the horizontal boxes
|
|
|
|
helpers.each(leftBoxes.concat(rightBoxes), fitBox);
|
|
|
|
|
|
|
|
helpers.each(leftBoxes, function(box) {
|
|
|
|
totalLeftBoxesWidth += box.width;
|
|
|
|
});
|
|
|
|
|
|
|
|
helpers.each(rightBoxes, function(box) {
|
|
|
|
totalRightBoxesWidth += box.width;
|
|
|
|
});
|
|
|
|
|
|
|
|
// Set the Left and Right margins for the horizontal boxes
|
|
|
|
helpers.each(topBoxes.concat(bottomBoxes), fitBox);
|
|
|
|
|
|
|
|
// Function to fit a box
|
|
|
|
function fitBox(box) {
|
|
|
|
var minBoxSize = helpers.findNextWhere(minBoxSizes, function(minBoxSize) {
|
|
|
|
return minBoxSize.box === box;
|
|
|
|
});
|
|
|
|
|
|
|
|
if (minBoxSize) {
|
|
|
|
if (box.isHorizontal()) {
|
|
|
|
var scaleMargin = {
|
|
|
|
left: totalLeftBoxesWidth,
|
|
|
|
right: totalRightBoxesWidth,
|
|
|
|
top: 0,
|
|
|
|
bottom: 0,
|
|
|
|
};
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
} else {
|
|
|
|
box.update(minBoxSize.minSize.width, maxChartAreaHeight);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Figure out how much margin is on the top and bottom of the vertical boxes
|
|
|
|
helpers.each(topBoxes, function(box) {
|
|
|
|
totalTopBoxesHeight += box.height;
|
|
|
|
});
|
|
|
|
|
|
|
|
helpers.each(bottomBoxes, function(box) {
|
|
|
|
totalBottomBoxesHeight += box.height;
|
|
|
|
});
|
|
|
|
|
|
|
|
// Let the left layout know the final margin
|
|
|
|
helpers.each(leftBoxes.concat(rightBoxes), finalFitVerticalBox);
|
|
|
|
|
|
|
|
function finalFitVerticalBox(box) {
|
|
|
|
var minBoxSize = helpers.findNextWhere(minBoxSizes, function(minBoxSize) {
|
|
|
|
return minBoxSize.box === box;
|
|
|
|
});
|
|
|
|
|
|
|
|
var scaleMargin = {
|
|
|
|
left: 0,
|
|
|
|
right: 0,
|
|
|
|
top: totalTopBoxesHeight,
|
|
|
|
bottom: totalBottomBoxesHeight
|
|
|
|
};
|
|
|
|
|
|
|
|
if (minBoxSize) {
|
|
|
|
box.update(minBoxSize.minSize.width, maxChartAreaHeight, scaleMargin);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Recalculate because the size of each layout might have changed slightly due to the margins (label rotation for instance)
|
|
|
|
totalLeftBoxesWidth = xPadding;
|
|
|
|
totalRightBoxesWidth = xPadding;
|
|
|
|
totalTopBoxesHeight = yPadding;
|
|
|
|
totalBottomBoxesHeight = yPadding;
|
|
|
|
|
|
|
|
helpers.each(leftBoxes, function(box) {
|
|
|
|
totalLeftBoxesWidth += box.width;
|
|
|
|
});
|
|
|
|
|
|
|
|
helpers.each(rightBoxes, function(box) {
|
|
|
|
totalRightBoxesWidth += box.width;
|
|
|
|
});
|
|
|
|
|
|
|
|
helpers.each(topBoxes, function(box) {
|
|
|
|
totalTopBoxesHeight += box.height;
|
|
|
|
});
|
|
|
|
helpers.each(bottomBoxes, function(box) {
|
|
|
|
totalBottomBoxesHeight += box.height;
|
|
|
|
});
|
|
|
|
|
|
|
|
// Figure out if our chart area changed. This would occur if the dataset layout label rotation
|
|
|
|
// changed due to the application of the margins in step 6. Since we can only get bigger, this is safe to do
|
|
|
|
// without calling `fit` again
|
|
|
|
var newMaxChartAreaHeight = height - totalTopBoxesHeight - totalBottomBoxesHeight;
|
|
|
|
var newMaxChartAreaWidth = width - totalLeftBoxesWidth - totalRightBoxesWidth;
|
|
|
|
|
|
|
|
if (newMaxChartAreaWidth !== maxChartAreaWidth || newMaxChartAreaHeight !== maxChartAreaHeight) {
|
|
|
|
helpers.each(leftBoxes, function(box) {
|
|
|
|
box.height = newMaxChartAreaHeight;
|
|
|
|
});
|
|
|
|
|
|
|
|
helpers.each(rightBoxes, function(box) {
|
|
|
|
box.height = newMaxChartAreaHeight;
|
|
|
|
});
|
|
|
|
|
|
|
|
helpers.each(topBoxes, function(box) {
|
|
|
|
box.width = newMaxChartAreaWidth;
|
|
|
|
});
|
|
|
|
|
|
|
|
helpers.each(bottomBoxes, function(box) {
|
|
|
|
box.width = newMaxChartAreaWidth;
|
|
|
|
});
|
|
|
|
|
|
|
|
maxChartAreaHeight = newMaxChartAreaHeight;
|
|
|
|
maxChartAreaWidth = newMaxChartAreaWidth;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Step 7 - Position the boxes
|
|
|
|
var left = xPadding;
|
|
|
|
var top = yPadding;
|
|
|
|
var right = 0;
|
|
|
|
var bottom = 0;
|
|
|
|
|
|
|
|
helpers.each(leftBoxes.concat(topBoxes), placeBox);
|
|
|
|
|
|
|
|
// Account for chart width and height
|
|
|
|
left += maxChartAreaWidth;
|
|
|
|
top += maxChartAreaHeight;
|
|
|
|
|
|
|
|
helpers.each(rightBoxes, placeBox);
|
|
|
|
helpers.each(bottomBoxes, placeBox);
|
|
|
|
|
|
|
|
function placeBox(box) {
|
|
|
|
if (box.isHorizontal()) {
|
|
|
|
box.left = box.options.fullWidth ? xPadding : totalLeftBoxesWidth;
|
|
|
|
box.right = box.options.fullWidth ? width - xPadding : totalLeftBoxesWidth + maxChartAreaWidth;
|
|
|
|
box.top = top;
|
|
|
|
box.bottom = top + box.height;
|
|
|
|
|
|
|
|
// Move to next point
|
|
|
|
top = box.bottom;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
box.left = left;
|
|
|
|
box.right = left + box.width;
|
|
|
|
box.top = totalTopBoxesHeight;
|
|
|
|
box.bottom = totalTopBoxesHeight + maxChartAreaHeight;
|
|
|
|
|
|
|
|
// Move to next point
|
|
|
|
left = box.right;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Step 8
|
|
|
|
chartInstance.chartArea = {
|
|
|
|
left: totalLeftBoxesWidth,
|
|
|
|
top: totalTopBoxesHeight,
|
|
|
|
right: totalLeftBoxesWidth + maxChartAreaWidth,
|
|
|
|
bottom: totalTopBoxesHeight + maxChartAreaHeight,
|
|
|
|
};
|
|
|
|
|
|
|
|
// Step 9
|
|
|
|
helpers.each(chartAreaBoxes, function(box) {
|
|
|
|
box.left = chartInstance.chartArea.left;
|
|
|
|
box.top = chartInstance.chartArea.top;
|
|
|
|
box.right = chartInstance.chartArea.right;
|
|
|
|
box.bottom = chartInstance.chartArea.bottom;
|
|
|
|
|
|
|
|
box.update(maxChartAreaWidth, maxChartAreaHeight);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
};
|