mirror of
https://github.com/chartjs/Chart.js.git
synced 2025-12-08 20:36:08 +00:00
50% Canvas Legend Support
This commit is contained in:
parent
e8165348db
commit
a91dba3e8a
196
samples/line-legend.html
Normal file
196
samples/line-legend.html
Normal file
@ -0,0 +1,196 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Line Chart</title>
|
||||
<script src="../Chart.js"></script>
|
||||
<script src="../node_modules/jquery/dist/jquery.min.js"></script>
|
||||
<style>
|
||||
canvas {
|
||||
-webkit-box-shadow: 0 0 20px 0 rgba(0, 0, 0, .5);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div style="width:100%;">
|
||||
<canvas id="canvas" style="width:100%;height:100%"></canvas>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
<button id="randomizeData">Randomize Data</button>
|
||||
<button id="addDataset">Add Dataset</button>
|
||||
<button id="removeDataset">Remove Dataset</button>
|
||||
<button id="addData">Add Data</button>
|
||||
<button id="removeData">Remove Data</button>
|
||||
<script>
|
||||
var randomScalingFactor = function() {
|
||||
return Math.round(Math.random() * 100 * (Math.random() > 0.5 ? -1 : 1));
|
||||
};
|
||||
var randomColorFactor = function() {
|
||||
return Math.round(Math.random() * 255);
|
||||
};
|
||||
var randomColor = function(opacity) {
|
||||
return 'rgba(' + randomColorFactor() + ',' + randomColorFactor() + ',' + randomColorFactor() + ',' + (opacity || '.3') + ')';
|
||||
};
|
||||
|
||||
var config = {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ["January", "February", "March", "April", "May", "June", "July"],
|
||||
datasets: [{
|
||||
label: "My First dataset",
|
||||
data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()],
|
||||
fill: false,
|
||||
}, {
|
||||
label: "My Second dataset",
|
||||
data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()],
|
||||
fill: false,
|
||||
}, {
|
||||
label: "My Third dataset",
|
||||
data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()],
|
||||
fill: false,
|
||||
}, {
|
||||
label: "My Fourth dataset",
|
||||
data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()],
|
||||
fill: false,
|
||||
}, {
|
||||
label: "My First dataset",
|
||||
data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()],
|
||||
fill: false,
|
||||
}, {
|
||||
label: "My Second dataset",
|
||||
data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()],
|
||||
fill: false,
|
||||
}, {
|
||||
label: "My Third dataset",
|
||||
data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()],
|
||||
fill: false,
|
||||
}, {
|
||||
label: "My Fourth dataset",
|
||||
data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()],
|
||||
fill: false,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
legend: {
|
||||
position: 'top',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Our 4 Favorite Datasets'
|
||||
}
|
||||
},
|
||||
hover: {
|
||||
mode: 'label'
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
display: true,
|
||||
scaleLabel: {
|
||||
show: true,
|
||||
labelString: 'Month'
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
display: true,
|
||||
scaleLabel: {
|
||||
show: true,
|
||||
labelString: 'Value'
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$.each(config.data.datasets, function(i, dataset) {
|
||||
var background = randomColor(0.5);
|
||||
dataset.borderColor = background;
|
||||
dataset.backgroundColor = background;
|
||||
dataset.pointBorderColor = background;
|
||||
dataset.pointBackgroundColor = background;
|
||||
dataset.pointBorderWidth = 1;
|
||||
});
|
||||
|
||||
console.log(config.data);
|
||||
|
||||
window.onload = function() {
|
||||
var ctx = document.getElementById("canvas").getContext("2d");
|
||||
window.myLine = new Chart(ctx, config);
|
||||
|
||||
updateLegend();
|
||||
};
|
||||
|
||||
function updateLegend() {
|
||||
$legendContainer = $('#legendContainer');
|
||||
$legendContainer.empty();
|
||||
$legendContainer.append(window.myLine.generateLegend());
|
||||
}
|
||||
|
||||
$('#randomizeData').click(function() {
|
||||
$.each(config.data.datasets, function(i, dataset) {
|
||||
dataset.data = dataset.data.map(function() {
|
||||
return randomScalingFactor();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
window.myLine.update();
|
||||
updateLegend();
|
||||
});
|
||||
|
||||
$('#addDataset').click(function() {
|
||||
var background = randomColor(0.5);
|
||||
var newDataset = {
|
||||
label: 'Dataset ' + config.data.datasets.length,
|
||||
borderColor: background,
|
||||
backgroundColor: background,
|
||||
pointBorderColor: background,
|
||||
pointBackgroundColor: background,
|
||||
pointBorderWidth: 1,
|
||||
fill: false,
|
||||
data: [],
|
||||
};
|
||||
|
||||
for (var index = 0; index < config.data.labels.length; ++index) {
|
||||
newDataset.data.push(randomScalingFactor());
|
||||
}
|
||||
|
||||
config.data.datasets.push(newDataset);
|
||||
window.myLine.update();
|
||||
updateLegend();
|
||||
});
|
||||
|
||||
$('#addData').click(function() {
|
||||
if (config.data.datasets.length > 0) {
|
||||
config.data.labels.push('dataset #' + config.data.labels.length);
|
||||
|
||||
$.each(config.data.datasets, function(i, dataset) {
|
||||
dataset.data.push(randomScalingFactor());
|
||||
});
|
||||
|
||||
window.myLine.update();
|
||||
updateLegend();
|
||||
}
|
||||
});
|
||||
|
||||
$('#removeDataset').click(function() {
|
||||
config.data.datasets.splice(0, 1);
|
||||
window.myLine.update();
|
||||
updateLegend();
|
||||
});
|
||||
|
||||
$('#removeData').click(function() {
|
||||
config.data.labels.splice(-1, 1); // remove the label first
|
||||
|
||||
config.data.datasets.forEach(function(dataset, datasetIndex) {
|
||||
dataset.data.pop();
|
||||
});
|
||||
|
||||
window.myLine.update();
|
||||
updateLegend();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -54,6 +54,8 @@
|
||||
this.ensureScalesHaveIDs();
|
||||
this.buildOrUpdateControllers();
|
||||
this.buildScales();
|
||||
this.buildLegends();
|
||||
this.updateLayout();
|
||||
this.resetElements();
|
||||
this.initToolTip();
|
||||
this.update();
|
||||
@ -161,7 +163,25 @@
|
||||
this.scales.radialScale = scale;
|
||||
}
|
||||
|
||||
Chart.scaleService.update(this, this.chart.width, this.chart.height);
|
||||
Chart.scaleService.addScalesToLayout(this);
|
||||
},
|
||||
|
||||
buildLegends: function() {
|
||||
if (!this.options.legend) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.legend = new Chart.Legend({
|
||||
ctx: this.chart.ctx,
|
||||
options: this.options.legend,
|
||||
chart: this,
|
||||
});
|
||||
|
||||
Chart.layoutService.addBox(this, this.legend);
|
||||
},
|
||||
|
||||
updateLayout: function() {
|
||||
Chart.layoutService.update(this, this.chart.width, this.chart.height);
|
||||
},
|
||||
|
||||
buildOrUpdateControllers: function buildOrUpdateControllers(resetNewControllers) {
|
||||
@ -199,7 +219,7 @@
|
||||
},
|
||||
|
||||
update: function update(animationDuration, lazy) {
|
||||
Chart.scaleService.update(this, this.chart.width, this.chart.height);
|
||||
Chart.layoutService.update(this, this.chart.width, this.chart.height);
|
||||
|
||||
// Make sure dataset controllers are updated and new controllers are reset
|
||||
this.buildOrUpdateControllers(true);
|
||||
@ -251,8 +271,8 @@
|
||||
this.clear();
|
||||
|
||||
// Draw all the scales
|
||||
helpers.each(this.scales, function(scale) {
|
||||
scale.draw(this.chartArea);
|
||||
helpers.each(this.boxes, function(box) {
|
||||
box.draw(this.chartArea);
|
||||
}, this);
|
||||
if (this.scale) {
|
||||
this.scale.draw();
|
||||
|
||||
347
src/core/core.layoutService.js
Normal file
347
src/core/core.layoutService.js
Normal file
@ -0,0 +1,347 @@
|
||||
(function() {
|
||||
"use strict";
|
||||
|
||||
var root = this,
|
||||
Chart = root.Chart,
|
||||
helpers = Chart.helpers;
|
||||
|
||||
// The layout service is ver 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";
|
||||
});
|
||||
|
||||
// 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; // min 50%
|
||||
var chartHeight = height / 2; // min 50%
|
||||
|
||||
chartWidth -= (2 * xPadding);
|
||||
chartHeight -= (2 * yPadding);
|
||||
|
||||
// Step 2
|
||||
var verticalBoxWidth = (width - chartWidth) / (leftBoxes.length + rightBoxes.length);
|
||||
|
||||
// Step 3
|
||||
var horizontalBoxHeight = (height - chartHeight) / (topBoxes.length + bottomBoxes.length);
|
||||
|
||||
// Step 4;
|
||||
var minBoxSizes = [];
|
||||
|
||||
// vertical boxes
|
||||
helpers.each(leftBoxes, verticalBoxMinSizeFunction);
|
||||
helpers.each(rightBoxes, verticalBoxMinSizeFunction);
|
||||
|
||||
function verticalBoxMinSizeFunction(box) {
|
||||
var minSize = box.update(verticalBoxWidth, chartHeight);
|
||||
minBoxSizes.push({
|
||||
horizontal: false,
|
||||
minSize: minSize,
|
||||
box: box,
|
||||
});
|
||||
}
|
||||
|
||||
// horizontal boxes
|
||||
helpers.each(topBoxes, horizontalBoxMinSizeFunction);
|
||||
helpers.each(bottomBoxes, horizontalBoxMinSizeFunction);
|
||||
|
||||
function horizontalBoxMinSizeFunction(box) {
|
||||
var minSize = box.update(chartWidth, horizontalBoxHeight);
|
||||
minBoxSizes.push({
|
||||
horizontal: true,
|
||||
minSize: minSize,
|
||||
box: box,
|
||||
});
|
||||
}
|
||||
|
||||
// Step 5
|
||||
var maxChartHeight = height - (2 * yPadding);
|
||||
var maxChartWidth = width - (2 * xPadding);
|
||||
|
||||
helpers.each(minBoxSizes, function(minimumBoxSize) {
|
||||
if (minimumBoxSize.horizontal) {
|
||||
maxChartHeight -= minimumBoxSize.minSize.height;
|
||||
} else {
|
||||
maxChartWidth -= minimumBoxSize.minSize.width;
|
||||
}
|
||||
});
|
||||
|
||||
// At this point, maxChartHeight and maxChartWidth are the size the chart area could
|
||||
// be if the axes are drawn at their minimum sizes.
|
||||
|
||||
// Step 6
|
||||
var totalLeftWidth = xPadding;
|
||||
var totalRightWidth = xPadding;
|
||||
var totalTopHeight = yPadding;
|
||||
var totalBottomHeight = yPadding;
|
||||
|
||||
helpers.each(leftBoxes, verticalBoxFitFunction);
|
||||
helpers.each(rightBoxes, verticalBoxFitFunction);
|
||||
|
||||
function verticalBoxFitFunction(box) {
|
||||
var minBoxSize = helpers.findNextWhere(minBoxSizes, function(minBoxSize) {
|
||||
return minBoxSize.box === box;
|
||||
});
|
||||
|
||||
if (minBoxSize) {
|
||||
box.update(minBoxSize.minSize.width, maxChartHeight);
|
||||
}
|
||||
}
|
||||
|
||||
// Figure out how much margin is on the left and right of the horizontal axes
|
||||
helpers.each(leftBoxes, function(box) {
|
||||
totalLeftWidth += box.width;
|
||||
});
|
||||
|
||||
helpers.each(rightBoxes, function(box) {
|
||||
totalRightWidth += box.width;
|
||||
});
|
||||
|
||||
helpers.each(topBoxes, horizontalBoxFitFunction);
|
||||
helpers.each(bottomBoxes, horizontalBoxFitFunction);
|
||||
|
||||
function horizontalBoxFitFunction(box) {
|
||||
var minBoxSize = helpers.findNextWhere(minBoxSizes, function(minBoxSize) {
|
||||
return minBoxSize.box === box;
|
||||
});
|
||||
|
||||
var scaleMargin = {
|
||||
left: totalLeftWidth,
|
||||
right: totalRightWidth,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
};
|
||||
|
||||
if (minBoxSize) {
|
||||
box.update(maxChartWidth, minBoxSize.minSize.height, scaleMargin);
|
||||
}
|
||||
}
|
||||
|
||||
helpers.each(topBoxes, function(box) {
|
||||
totalTopHeight += box.height;
|
||||
});
|
||||
|
||||
helpers.each(bottomBoxes, function(box) {
|
||||
totalBottomHeight += box.height;
|
||||
});
|
||||
|
||||
// Let the left layout know the final margin
|
||||
helpers.each(leftBoxes, function(box) {
|
||||
var wrapper = helpers.findNextWhere(minBoxSizes, function(wrapper) {
|
||||
return wrapper.box === box;
|
||||
});
|
||||
|
||||
var scaleMargin = {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: totalTopHeight,
|
||||
bottom: totalBottomHeight
|
||||
};
|
||||
|
||||
if (wrapper) {
|
||||
box.update(wrapper.minSize.width, maxChartHeight, scaleMargin);
|
||||
}
|
||||
});
|
||||
|
||||
helpers.each(rightBoxes, function(box) {
|
||||
var wrapper = helpers.findNextWhere(minBoxSizes, function(wrapper) {
|
||||
return wrapper.box === box;
|
||||
});
|
||||
|
||||
var scaleMargin = {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: totalTopHeight,
|
||||
bottom: totalBottomHeight
|
||||
};
|
||||
|
||||
if (wrapper) {
|
||||
box.update(wrapper.minSize.width, maxChartHeight, scaleMargin);
|
||||
}
|
||||
});
|
||||
|
||||
// Recalculate because the size of each layout might have changed slightly due to the margins (label rotation for instance)
|
||||
totalLeftWidth = xPadding;
|
||||
totalRightWidth = xPadding;
|
||||
totalTopHeight = yPadding;
|
||||
totalBottomHeight = yPadding;
|
||||
|
||||
helpers.each(leftBoxes, function(box) {
|
||||
totalLeftWidth += box.width;
|
||||
});
|
||||
|
||||
helpers.each(rightBoxes, function(box) {
|
||||
totalRightWidth += box.width;
|
||||
});
|
||||
|
||||
helpers.each(topBoxes, function(box) {
|
||||
totalTopHeight += box.height;
|
||||
});
|
||||
helpers.each(bottomBoxes, function(box) {
|
||||
totalBottomHeight += 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 newMaxChartHeight = height - totalTopHeight - totalBottomHeight;
|
||||
var newMaxChartWidth = width - totalLeftWidth - totalRightWidth;
|
||||
|
||||
if (newMaxChartWidth !== maxChartWidth || newMaxChartHeight !== maxChartHeight) {
|
||||
helpers.each(leftBoxes, function(box) {
|
||||
box.height = newMaxChartHeight;
|
||||
});
|
||||
|
||||
helpers.each(rightBoxes, function(box) {
|
||||
box.height = newMaxChartHeight;
|
||||
});
|
||||
|
||||
helpers.each(topBoxes, function(box) {
|
||||
box.width = newMaxChartWidth;
|
||||
});
|
||||
|
||||
helpers.each(bottomBoxes, function(box) {
|
||||
box.width = newMaxChartWidth;
|
||||
});
|
||||
|
||||
maxChartHeight = newMaxChartHeight;
|
||||
maxChartWidth = newMaxChartWidth;
|
||||
}
|
||||
|
||||
// Step 7 - Position the boxes
|
||||
var left = xPadding;
|
||||
var top = yPadding;
|
||||
var right = 0;
|
||||
var bottom = 0;
|
||||
|
||||
helpers.each(leftBoxes, verticalBoxPlacer);
|
||||
helpers.each(topBoxes, horizontalBoxPlacer);
|
||||
|
||||
// Account for chart width and height
|
||||
left += maxChartWidth;
|
||||
top += maxChartHeight;
|
||||
|
||||
helpers.each(rightBoxes, verticalBoxPlacer);
|
||||
helpers.each(bottomBoxes, horizontalBoxPlacer);
|
||||
|
||||
function verticalBoxPlacer(box) {
|
||||
box.left = left;
|
||||
box.right = left + box.width;
|
||||
box.top = totalTopHeight;
|
||||
box.bottom = totalTopHeight + maxChartHeight;
|
||||
|
||||
// Move to next point
|
||||
left = box.right;
|
||||
}
|
||||
|
||||
function horizontalBoxPlacer(box) {
|
||||
box.left = totalLeftWidth;
|
||||
box.right = totalLeftWidth + maxChartWidth;
|
||||
box.top = top;
|
||||
box.bottom = top + box.height;
|
||||
|
||||
// Move to next point
|
||||
top = box.bottom;
|
||||
}
|
||||
|
||||
// Step 8
|
||||
chartInstance.chartArea = {
|
||||
left: totalLeftWidth,
|
||||
top: totalTopHeight,
|
||||
right: totalLeftWidth + maxChartWidth,
|
||||
bottom: totalTopHeight + maxChartHeight,
|
||||
};
|
||||
|
||||
// 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(maxChartWidth, maxChartHeight);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}).call(this);
|
||||
271
src/core/core.legend.js
Normal file
271
src/core/core.legend.js
Normal file
@ -0,0 +1,271 @@
|
||||
(function() {
|
||||
"use strict";
|
||||
|
||||
var root = this,
|
||||
Chart = root.Chart,
|
||||
helpers = Chart.helpers;
|
||||
|
||||
Chart.defaults.legend = {
|
||||
|
||||
display: true,
|
||||
position: 'top',
|
||||
onClick: false, // a callback will override the default behavior of toggling the datasets
|
||||
|
||||
title: {
|
||||
position: 'top',
|
||||
fontColor: '#666',
|
||||
fontFamily: 'Helvetica Neue',
|
||||
fontSize: 12,
|
||||
fontStyle: 'bold',
|
||||
padding: 10,
|
||||
|
||||
// actual title
|
||||
text: '',
|
||||
|
||||
// display property
|
||||
display: false,
|
||||
},
|
||||
|
||||
labels: {
|
||||
fontSize: 12,
|
||||
fontStyle: "normal",
|
||||
fontColor: "#666",
|
||||
fontFamily: "Helvetica Neue",
|
||||
padding: 10,
|
||||
reverse: false,
|
||||
display: true,
|
||||
callback: function(value) {
|
||||
return '' + value;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Chart.Legend = Chart.Element.extend({
|
||||
|
||||
initialize: function(config) {
|
||||
helpers.extend(this, config);
|
||||
this.options = helpers.configMerge(Chart.defaults.legend, config.options);
|
||||
},
|
||||
|
||||
// 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: helpers.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: helpers.noop,
|
||||
|
||||
//
|
||||
|
||||
beforeSetDimensions: helpers.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;
|
||||
},
|
||||
afterSetDimensions: helpers.noop,
|
||||
|
||||
//
|
||||
|
||||
beforeBuildLabels: helpers.noop,
|
||||
buildLabels: function() {
|
||||
// Convert ticks to strings
|
||||
this.labels = this.chart.data.datasets.map(function(dataset) {
|
||||
return this.options.labels.callback.call(this, dataset.label);
|
||||
}, this);
|
||||
},
|
||||
afterBuildLabels: helpers.noop,
|
||||
|
||||
//
|
||||
|
||||
beforeFit: helpers.noop,
|
||||
fit: function() {
|
||||
|
||||
var ctx = this.ctx;
|
||||
var titleFont = helpers.fontString(this.options.title.fontSize, this.options.title.fontStyle, this.options.title.fontFamily);
|
||||
var labelFont = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily);
|
||||
|
||||
this.minSize = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
|
||||
// Legends can use all available space by default as no alignment to the chartArea is necessary
|
||||
|
||||
// Width
|
||||
if (this.isHorizontal()) {
|
||||
this.minSize.width = this.maxWidth; // fill all the width
|
||||
} else {
|
||||
this.minSize.width = this.options.display ? 10 : 0;
|
||||
}
|
||||
|
||||
// height
|
||||
if (this.isHorizontal()) {
|
||||
this.minSize.height = this.options.display ? 10 : 0;
|
||||
} else {
|
||||
this.minSize.height = this.maxHeight; // fill all the height
|
||||
}
|
||||
|
||||
// Increase sizes here
|
||||
if (this.isHorizontal()) {
|
||||
|
||||
// Title
|
||||
if (this.options.title.display) {
|
||||
this.minSize.height += this.options.title.fontSize + (this.options.title.padding * 2);
|
||||
}
|
||||
|
||||
// Labels
|
||||
|
||||
var totalWidth = 0;
|
||||
var totalHeight = this.labels.length ? this.options.labels.fontSize + (this.options.labels.padding) : 0;
|
||||
|
||||
ctx.font = labelFont;
|
||||
|
||||
helpers.each(this.labels, function(label, i) {
|
||||
var width = ctx.measureText(label).width;
|
||||
if (totalWidth + width > this.width) {
|
||||
totalHeight += this.options.labels.fontSize + (this.options.labels.padding);
|
||||
totalWidth = 0;
|
||||
}
|
||||
totalWidth += width + this.options.labels.padding;
|
||||
}, this);
|
||||
|
||||
this.minSize.height += totalHeight;
|
||||
|
||||
} else {
|
||||
// TODO vertical
|
||||
}
|
||||
|
||||
this.width = this.minSize.width;
|
||||
this.height = this.minSize.height;
|
||||
|
||||
},
|
||||
afterFit: helpers.noop,
|
||||
|
||||
// Shared Methods
|
||||
isHorizontal: function() {
|
||||
return this.options.position == "top" || this.options.position == "bottom";
|
||||
},
|
||||
|
||||
// Actualy draw the legend on the canvas
|
||||
draw: function() {
|
||||
if (this.options.display) {
|
||||
|
||||
var ctx = this.ctx;
|
||||
var cursor = {
|
||||
x: this.left,
|
||||
y: this.top,
|
||||
};
|
||||
|
||||
// Horizontal
|
||||
if (this.isHorizontal()) {
|
||||
|
||||
// Title Spacing if on top
|
||||
if (this.options.title.display && this.options.title.position == 'top') {
|
||||
cursor.y += this.options.title.fontSize + (this.options.title.padding * 2);
|
||||
}
|
||||
|
||||
// Labels
|
||||
ctx.textAlign = "left";
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillStyle = this.options.labels.fontColor; // render in correct colour
|
||||
ctx.font = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily);
|
||||
|
||||
helpers.each(this.labels, function(label, i) {
|
||||
var width = ctx.measureText(label).width;
|
||||
if (cursor.x + width > this.width) {
|
||||
cursor.y += this.options.labels.fontSize + (this.options.labels.padding);
|
||||
cursor.x = this.left;
|
||||
}
|
||||
ctx.fillText(label, cursor.x, cursor.y);
|
||||
cursor.x += width + (this.options.labels.padding);
|
||||
}, this);
|
||||
|
||||
|
||||
// Title
|
||||
if (this.options.title.display) {
|
||||
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillStyle = this.options.title.fontColor; // render in correct colour
|
||||
ctx.font = helpers.fontString(this.options.title.fontSize, this.options.title.fontStyle, this.options.title.fontFamily);
|
||||
|
||||
var titleX = this.left + ((this.right - this.left) / 2); // midpoint of the width
|
||||
var titleY = this.options.position == 'bottom' ? this.bottom - (this.options.title.fontSize / 2) - this.options.title.padding : this.top + (this.options.title.fontSize / 2) + this.options.title.padding;
|
||||
|
||||
ctx.fillText(this.options.title.text, titleX, titleY);
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
|
||||
// Title
|
||||
if (this.options.title.display) {
|
||||
|
||||
// Draw the legend label
|
||||
titleX = this.options.position == 'left' ? this.left + (this.options.title.fontSize / 2) : this.right - (this.options.title.fontSize / 2);
|
||||
titleY = this.top + ((this.bottom - this.top) / 2);
|
||||
var rotation = this.options.position == 'left' ? -0.5 * Math.PI : 0.5 * Math.PI;
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(titleX, titleY);
|
||||
ctx.rotate(rotation);
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillStyle = this.options.title.fontColor; // render in correct colour
|
||||
ctx.font = helpers.fontString(this.options.title.fontSize, this.options.title.fontStyle, this.options.title.fontFamily);
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(this.options.title.text, 0, 0);
|
||||
ctx.restore();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}).call(this);
|
||||
@ -5,9 +5,6 @@
|
||||
Chart = root.Chart,
|
||||
helpers = Chart.helpers;
|
||||
|
||||
// The scale service is used to resize charts along with all of their axes. We make this as
|
||||
// a service where scales are registered with their respective charts so that changing the
|
||||
// scales does not require
|
||||
Chart.scaleService = {
|
||||
// Scale registration object. Extensions can register new scale types (such as log or DB scales) and then
|
||||
// use the new chart options to grab the correct scale
|
||||
@ -28,308 +25,12 @@
|
||||
// Return the scale defaults merged with the global settings so that we always use the latest ones
|
||||
return this.defaults.hasOwnProperty(type) ? helpers.scaleMerge(Chart.defaults.scale, this.defaults[type]) : {};
|
||||
},
|
||||
// The interesting function
|
||||
update: function(chartInstance, width, height) {
|
||||
var xPadding = width > 30 ? 5 : 2;
|
||||
var yPadding = height > 30 ? 5 : 2;
|
||||
|
||||
if (chartInstance) {
|
||||
var leftScales = helpers.where(chartInstance.scales, function(scaleInstance) {
|
||||
return scaleInstance.options.position == "left";
|
||||
});
|
||||
var rightScales = helpers.where(chartInstance.scales, function(scaleInstance) {
|
||||
return scaleInstance.options.position == "right";
|
||||
});
|
||||
var topScales = helpers.where(chartInstance.scales, function(scaleInstance) {
|
||||
return scaleInstance.options.position == "top";
|
||||
});
|
||||
var bottomScales = helpers.where(chartInstance.scales, function(scaleInstance) {
|
||||
return scaleInstance.options.position == "bottom";
|
||||
});
|
||||
|
||||
// Scales that overlay the chartarea such as the radialLinear scale
|
||||
var chartAreaScales = helpers.where(chartInstance.scales, function(scaleInstance) {
|
||||
return scaleInstance.options.position == "chartArea";
|
||||
});
|
||||
|
||||
// Essentially we now have any number of scales 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
|
||||
// |------------------------------------------------------|
|
||||
// | | T1 | |
|
||||
// |----|-----|-------------------------------------|-----|
|
||||
// | | | | |
|
||||
// | L1 | L2 | Chart area | R1 |
|
||||
// | | | | |
|
||||
// | | | | |
|
||||
// |----|-----|-------------------------------------|-----|
|
||||
// | | B1 | |
|
||||
// | | | |
|
||||
// |------------------------------------------------------|
|
||||
|
||||
// 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 scale the maximum size it can be. The scale 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; // min 50%
|
||||
var chartHeight = height / 2; // min 50%
|
||||
|
||||
chartWidth -= (2 * xPadding);
|
||||
chartHeight -= (2 * yPadding);
|
||||
|
||||
// Step 2
|
||||
var verticalScaleWidth = (width - chartWidth) / (leftScales.length + rightScales.length);
|
||||
|
||||
// Step 3
|
||||
var horizontalScaleHeight = (height - chartHeight) / (topScales.length + bottomScales.length);
|
||||
|
||||
// Step 4;
|
||||
var minimumScaleSizes = [];
|
||||
|
||||
var verticalScaleMinSizeFunction = function(scaleInstance) {
|
||||
var minSize = scaleInstance.update(verticalScaleWidth, chartHeight);
|
||||
minimumScaleSizes.push({
|
||||
horizontal: false,
|
||||
minSize: minSize,
|
||||
scale: scaleInstance,
|
||||
});
|
||||
};
|
||||
|
||||
var horizontalScaleMinSizeFunction = function(scaleInstance) {
|
||||
var minSize = scaleInstance.update(chartWidth, horizontalScaleHeight);
|
||||
minimumScaleSizes.push({
|
||||
horizontal: true,
|
||||
minSize: minSize,
|
||||
scale: scaleInstance,
|
||||
});
|
||||
};
|
||||
|
||||
// vertical scales
|
||||
helpers.each(leftScales, verticalScaleMinSizeFunction);
|
||||
helpers.each(rightScales, verticalScaleMinSizeFunction);
|
||||
|
||||
// horizontal scales
|
||||
helpers.each(topScales, horizontalScaleMinSizeFunction);
|
||||
helpers.each(bottomScales, horizontalScaleMinSizeFunction);
|
||||
|
||||
// Step 5
|
||||
var maxChartHeight = height - (2 * yPadding);
|
||||
var maxChartWidth = width - (2 * xPadding);
|
||||
|
||||
helpers.each(minimumScaleSizes, function(wrapper) {
|
||||
if (wrapper.horizontal) {
|
||||
maxChartHeight -= wrapper.minSize.height;
|
||||
} else {
|
||||
maxChartWidth -= wrapper.minSize.width;
|
||||
}
|
||||
});
|
||||
|
||||
// At this point, maxChartHeight and maxChartWidth are the size the chart area could
|
||||
// be if the axes are drawn at their minimum sizes.
|
||||
|
||||
// Step 6
|
||||
var verticalScaleFitFunction = function(scaleInstance) {
|
||||
var wrapper = helpers.findNextWhere(minimumScaleSizes, function(wrapper) {
|
||||
return wrapper.scale === scaleInstance;
|
||||
});
|
||||
|
||||
if (wrapper) {
|
||||
scaleInstance.update(wrapper.minSize.width, maxChartHeight);
|
||||
}
|
||||
};
|
||||
|
||||
var horizontalScaleFitFunction = function(scaleInstance) {
|
||||
var wrapper = helpers.findNextWhere(minimumScaleSizes, function(wrapper) {
|
||||
return wrapper.scale === scaleInstance;
|
||||
});
|
||||
|
||||
var scaleMargin = {
|
||||
left: totalLeftWidth,
|
||||
right: totalRightWidth,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
};
|
||||
|
||||
if (wrapper) {
|
||||
scaleInstance.update(maxChartWidth, wrapper.minSize.height, scaleMargin);
|
||||
}
|
||||
};
|
||||
|
||||
var totalLeftWidth = xPadding;
|
||||
var totalRightWidth = xPadding;
|
||||
var totalTopHeight = yPadding;
|
||||
var totalBottomHeight = yPadding;
|
||||
|
||||
helpers.each(leftScales, verticalScaleFitFunction);
|
||||
helpers.each(rightScales, verticalScaleFitFunction);
|
||||
|
||||
// Figure out how much margin is on the left and right of the horizontal axes
|
||||
helpers.each(leftScales, function(scaleInstance) {
|
||||
totalLeftWidth += scaleInstance.width;
|
||||
});
|
||||
|
||||
helpers.each(rightScales, function(scaleInstance) {
|
||||
totalRightWidth += scaleInstance.width;
|
||||
});
|
||||
|
||||
helpers.each(topScales, horizontalScaleFitFunction);
|
||||
helpers.each(bottomScales, horizontalScaleFitFunction);
|
||||
|
||||
helpers.each(topScales, function(scaleInstance) {
|
||||
totalTopHeight += scaleInstance.height;
|
||||
});
|
||||
helpers.each(bottomScales, function(scaleInstance) {
|
||||
totalBottomHeight += scaleInstance.height;
|
||||
});
|
||||
|
||||
// Let the left scale know the final margin
|
||||
helpers.each(leftScales, function(scaleInstance) {
|
||||
var wrapper = helpers.findNextWhere(minimumScaleSizes, function(wrapper) {
|
||||
return wrapper.scale === scaleInstance;
|
||||
});
|
||||
|
||||
var scaleMargin = {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: totalTopHeight,
|
||||
bottom: totalBottomHeight
|
||||
};
|
||||
|
||||
if (wrapper) {
|
||||
scaleInstance.update(wrapper.minSize.width, maxChartHeight, scaleMargin);
|
||||
}
|
||||
});
|
||||
|
||||
helpers.each(rightScales, function(scaleInstance) {
|
||||
var wrapper = helpers.findNextWhere(minimumScaleSizes, function(wrapper) {
|
||||
return wrapper.scale === scaleInstance;
|
||||
});
|
||||
|
||||
var scaleMargin = {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: totalTopHeight,
|
||||
bottom: totalBottomHeight
|
||||
};
|
||||
|
||||
if (wrapper) {
|
||||
scaleInstance.update(wrapper.minSize.width, maxChartHeight, scaleMargin);
|
||||
}
|
||||
});
|
||||
|
||||
// Recalculate because the size of each scale might have changed slightly due to the margins (label rotation for instance)
|
||||
totalLeftWidth = xPadding;
|
||||
totalRightWidth = xPadding;
|
||||
totalTopHeight = yPadding;
|
||||
totalBottomHeight = yPadding;
|
||||
|
||||
helpers.each(leftScales, function(scaleInstance) {
|
||||
totalLeftWidth += scaleInstance.width;
|
||||
});
|
||||
|
||||
helpers.each(rightScales, function(scaleInstance) {
|
||||
totalRightWidth += scaleInstance.width;
|
||||
});
|
||||
|
||||
helpers.each(topScales, function(scaleInstance) {
|
||||
totalTopHeight += scaleInstance.height;
|
||||
});
|
||||
helpers.each(bottomScales, function(scaleInstance) {
|
||||
totalBottomHeight += scaleInstance.height;
|
||||
});
|
||||
|
||||
// Figure out if our chart area changed. This would occur if the dataset scale 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 newMaxChartHeight = height - totalTopHeight - totalBottomHeight;
|
||||
var newMaxChartWidth = width - totalLeftWidth - totalRightWidth;
|
||||
|
||||
if (newMaxChartWidth !== maxChartWidth || newMaxChartHeight !== maxChartHeight) {
|
||||
helpers.each(leftScales, function(scale) {
|
||||
scale.height = newMaxChartHeight;
|
||||
});
|
||||
|
||||
helpers.each(rightScales, function(scale) {
|
||||
scale.height = newMaxChartHeight;
|
||||
});
|
||||
|
||||
helpers.each(topScales, function(scale) {
|
||||
scale.width = newMaxChartWidth;
|
||||
});
|
||||
|
||||
helpers.each(bottomScales, function(scale) {
|
||||
scale.width = newMaxChartWidth;
|
||||
});
|
||||
|
||||
maxChartHeight = newMaxChartHeight;
|
||||
maxChartWidth = newMaxChartWidth;
|
||||
}
|
||||
|
||||
// Step 7
|
||||
// Position the scales
|
||||
var left = xPadding;
|
||||
var top = yPadding;
|
||||
var right = 0;
|
||||
var bottom = 0;
|
||||
|
||||
var verticalScalePlacer = function(scaleInstance) {
|
||||
scaleInstance.left = left;
|
||||
scaleInstance.right = left + scaleInstance.width;
|
||||
scaleInstance.top = totalTopHeight;
|
||||
scaleInstance.bottom = totalTopHeight + maxChartHeight;
|
||||
|
||||
// Move to next point
|
||||
left = scaleInstance.right;
|
||||
};
|
||||
|
||||
var horizontalScalePlacer = function(scaleInstance) {
|
||||
scaleInstance.left = totalLeftWidth;
|
||||
scaleInstance.right = totalLeftWidth + maxChartWidth;
|
||||
scaleInstance.top = top;
|
||||
scaleInstance.bottom = top + scaleInstance.height;
|
||||
|
||||
// Move to next point
|
||||
top = scaleInstance.bottom;
|
||||
};
|
||||
|
||||
helpers.each(leftScales, verticalScalePlacer);
|
||||
helpers.each(topScales, horizontalScalePlacer);
|
||||
|
||||
// Account for chart width and height
|
||||
left += maxChartWidth;
|
||||
top += maxChartHeight;
|
||||
|
||||
helpers.each(rightScales, verticalScalePlacer);
|
||||
helpers.each(bottomScales, horizontalScalePlacer);
|
||||
|
||||
// Step 8
|
||||
chartInstance.chartArea = {
|
||||
left: totalLeftWidth,
|
||||
top: totalTopHeight,
|
||||
right: totalLeftWidth + maxChartWidth,
|
||||
bottom: totalTopHeight + maxChartHeight,
|
||||
};
|
||||
|
||||
// Step 9
|
||||
helpers.each(chartAreaScales, function(scaleInstance) {
|
||||
scaleInstance.left = chartInstance.chartArea.left;
|
||||
scaleInstance.top = chartInstance.chartArea.top;
|
||||
scaleInstance.right = chartInstance.chartArea.right;
|
||||
scaleInstance.bottom = chartInstance.chartArea.bottom;
|
||||
|
||||
scaleInstance.update(maxChartWidth, maxChartHeight);
|
||||
});
|
||||
}
|
||||
}
|
||||
addScalesToLayout: function(chartInstance) {
|
||||
// Adds each scale to the chart.boxes array to be sized accordingly
|
||||
helpers.each(chartInstance.scales, function(scale) {
|
||||
Chart.layoutService.addBox(chartInstance, scale);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user