313 lines
10 KiB
JavaScript
313 lines
10 KiB
JavaScript
|
|
/**
|
||
|
|
* [Chart.PieceLabel.js]{@link https://github.com/emn178/Chart.PieceLabel.js}
|
||
|
|
*
|
||
|
|
* @version 0.9.0
|
||
|
|
* @author Chen, Yi-Cyuan [emn178@gmail.com]
|
||
|
|
* @copyright Chen, Yi-Cyuan 2017
|
||
|
|
* @license MIT
|
||
|
|
*/
|
||
|
|
(function () {
|
||
|
|
if (typeof Chart === 'undefined') {
|
||
|
|
console.warn('Can not find Chart object.');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
function PieceLabel() {
|
||
|
|
this.drawDataset = this.drawDataset.bind(this);
|
||
|
|
}
|
||
|
|
|
||
|
|
PieceLabel.prototype.beforeDatasetsUpdate = function (chartInstance) {
|
||
|
|
if (this.parseOptions(chartInstance) && this.position === 'outside') {
|
||
|
|
var padding = this.fontSize * 1.5 + 2;
|
||
|
|
chartInstance.chartArea.top += padding;
|
||
|
|
chartInstance.chartArea.bottom -= padding;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
PieceLabel.prototype.afterDatasetsDraw = function (chartInstance) {
|
||
|
|
if (!this.parseOptions(chartInstance)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this.labelBounds = [];
|
||
|
|
chartInstance.config.data.datasets.forEach(this.drawDataset);
|
||
|
|
};
|
||
|
|
|
||
|
|
PieceLabel.prototype.drawDataset = function (dataset) {
|
||
|
|
var ctx = this.ctx;
|
||
|
|
var chartInstance = this.chartInstance;
|
||
|
|
var meta = dataset._meta[Object.keys(dataset._meta)[0]];
|
||
|
|
var totalPercentage = 0;
|
||
|
|
for (var i = 0; i < meta.data.length; i++) {
|
||
|
|
var element = meta.data[i],
|
||
|
|
view = element._view, text;
|
||
|
|
|
||
|
|
//Skips label creation if value is zero and showZero is set
|
||
|
|
if (view.circumference === 0 && !this.showZero) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
switch (this.render) {
|
||
|
|
case 'value':
|
||
|
|
var value = dataset.data[i];
|
||
|
|
if (this.format) {
|
||
|
|
value = this.format(value);
|
||
|
|
}
|
||
|
|
text = value.toString();
|
||
|
|
break;
|
||
|
|
case 'label':
|
||
|
|
text = chartInstance.config.data.labels[i];
|
||
|
|
break;
|
||
|
|
case 'image':
|
||
|
|
text = this.images[i] ? this.loadImage(this.images[i]) : '';
|
||
|
|
break;
|
||
|
|
case 'percentage':
|
||
|
|
default:
|
||
|
|
var percentage = view.circumference / this.options.circumference * 100;
|
||
|
|
percentage = parseFloat(percentage.toFixed(this.precision));
|
||
|
|
if (!this.showActualPercentages) {
|
||
|
|
totalPercentage += percentage;
|
||
|
|
if (totalPercentage > 100) {
|
||
|
|
percentage -= totalPercentage - 100;
|
||
|
|
// After adjusting the percentage, need to trim the numbers after decimal points again, otherwise it may not show
|
||
|
|
// on chart due to very long number after decimal point.
|
||
|
|
percentage = parseFloat(percentage.toFixed(this.precision));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
text = percentage + '%';
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
if (typeof this.render === 'function') {
|
||
|
|
text = this.render({
|
||
|
|
label: chartInstance.config.data.labels[i],
|
||
|
|
value: dataset.data[i],
|
||
|
|
percentage: percentage,
|
||
|
|
dataset: dataset,
|
||
|
|
index: i
|
||
|
|
});
|
||
|
|
|
||
|
|
if (typeof text === 'object') {
|
||
|
|
text = this.loadImage(text);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!text) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
ctx.save();
|
||
|
|
ctx.beginPath();
|
||
|
|
ctx.font = Chart.helpers.fontString(this.fontSize, this.fontStyle, this.fontFamily);
|
||
|
|
var position, innerRadius, arcOffset;
|
||
|
|
if (this.position === 'outside' ||
|
||
|
|
this.position === 'border' && chartInstance.config.type === 'pie') {
|
||
|
|
innerRadius = view.outerRadius / 2;
|
||
|
|
var rangeFromCentre, offset = this.fontSize + 2,
|
||
|
|
centreAngle = view.startAngle + ((view.endAngle - view.startAngle) / 2);
|
||
|
|
if (this.position === 'border') {
|
||
|
|
rangeFromCentre = (view.outerRadius - innerRadius) / 2 + innerRadius;
|
||
|
|
} else if (this.position === 'outside') {
|
||
|
|
rangeFromCentre = (view.outerRadius - innerRadius) + innerRadius + offset;
|
||
|
|
}
|
||
|
|
position = {
|
||
|
|
x: view.x + (Math.cos(centreAngle) * rangeFromCentre),
|
||
|
|
y: view.y + (Math.sin(centreAngle) * rangeFromCentre)
|
||
|
|
};
|
||
|
|
if (this.position === 'outside') {
|
||
|
|
if (position.x < view.x) {
|
||
|
|
position.x -= offset;
|
||
|
|
} else {
|
||
|
|
position.x += offset;
|
||
|
|
}
|
||
|
|
arcOffset = view.outerRadius + offset;
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
innerRadius = view.innerRadius;
|
||
|
|
position = element.tooltipPosition();
|
||
|
|
}
|
||
|
|
|
||
|
|
var fontColor = this.fontColor;
|
||
|
|
if (typeof fontColor === 'function') {
|
||
|
|
fontColor = fontColor({
|
||
|
|
label: chartInstance.config.data.labels[i],
|
||
|
|
value: dataset.data[i],
|
||
|
|
percentage: percentage,
|
||
|
|
text: text,
|
||
|
|
backgroundColor: dataset.backgroundColor[i],
|
||
|
|
dataset: dataset,
|
||
|
|
index: i
|
||
|
|
});
|
||
|
|
} else if (typeof fontColor !== 'string') {
|
||
|
|
fontColor = fontColor[i] || this.options.defaultFontColor;
|
||
|
|
}
|
||
|
|
if (this.arc) {
|
||
|
|
if (!arcOffset) {
|
||
|
|
arcOffset = (innerRadius + view.outerRadius) / 2;
|
||
|
|
}
|
||
|
|
ctx.fillStyle = fontColor;
|
||
|
|
ctx.textBaseline = 'middle';
|
||
|
|
this.drawArcText(text, arcOffset, view, this.overlap);
|
||
|
|
} else {
|
||
|
|
var drawable, mertrics = this.measureText(text),
|
||
|
|
left = position.x - mertrics.width / 2,
|
||
|
|
right = position.x + mertrics.width / 2,
|
||
|
|
top = position.y - this.fontSize / 2,
|
||
|
|
bottom = position.y + this.fontSize / 2;
|
||
|
|
if (this.overlap) {
|
||
|
|
drawable = true;
|
||
|
|
} else if (this.position === 'outside') {
|
||
|
|
drawable = this.checkTextBound(left, right, top, bottom);
|
||
|
|
} else {
|
||
|
|
drawable = element.inRange(left, top) && element.inRange(left, bottom) &&
|
||
|
|
element.inRange(right, top) && element.inRange(right, bottom);
|
||
|
|
}
|
||
|
|
if (drawable) {
|
||
|
|
this.fillText(text, position, fontColor);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
ctx.restore();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
PieceLabel.prototype.parseOptions = function (chartInstance) {
|
||
|
|
var pieceLabel = chartInstance.options.pieceLabel;
|
||
|
|
if (pieceLabel) {
|
||
|
|
this.chartInstance = chartInstance;
|
||
|
|
this.ctx = chartInstance.chart.ctx;
|
||
|
|
this.options = chartInstance.config.options;
|
||
|
|
this.render = pieceLabel.render || pieceLabel.mode;
|
||
|
|
this.position = pieceLabel.position || 'default';
|
||
|
|
this.arc = pieceLabel.arc;
|
||
|
|
this.format = pieceLabel.format;
|
||
|
|
this.precision = pieceLabel.precision || 0;
|
||
|
|
this.fontSize = pieceLabel.fontSize || this.options.defaultFontSize;
|
||
|
|
this.fontColor = pieceLabel.fontColor || this.options.defaultFontColor;
|
||
|
|
this.fontStyle = pieceLabel.fontStyle || this.options.defaultFontStyle;
|
||
|
|
this.fontFamily = pieceLabel.fontFamily || this.options.defaultFontFamily;
|
||
|
|
this.hasTooltip = chartInstance.tooltip._active && chartInstance.tooltip._active.length;
|
||
|
|
this.showZero = pieceLabel.showZero;
|
||
|
|
this.overlap = pieceLabel.overlap;
|
||
|
|
this.images = pieceLabel.images || [];
|
||
|
|
this.showActualPercentages = pieceLabel.showActualPercentages || false;
|
||
|
|
return true;
|
||
|
|
} else {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
PieceLabel.prototype.checkTextBound = function (left, right, top, bottom) {
|
||
|
|
var labelBounds = this.labelBounds;
|
||
|
|
for (var i = 0;i < labelBounds.length;++i) {
|
||
|
|
var bound = labelBounds[i];
|
||
|
|
var potins = [
|
||
|
|
[left, top],
|
||
|
|
[left, bottom],
|
||
|
|
[right, top],
|
||
|
|
[right, bottom]
|
||
|
|
];
|
||
|
|
for (var j = 0;j < potins.length;++j) {
|
||
|
|
var x = potins[j][0];
|
||
|
|
var y = potins[j][1];
|
||
|
|
if (x >= bound.left && x <= bound.right && y >= bound.top && y <= bound.bottom) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
potins = [
|
||
|
|
[bound.left, bound.top],
|
||
|
|
[bound.left, bound.bottom],
|
||
|
|
[bound.right, bound.top],
|
||
|
|
[bound.right, bound.bottom]
|
||
|
|
];
|
||
|
|
for (var j = 0;j < potins.length;++j) {
|
||
|
|
var x = potins[j][0];
|
||
|
|
var y = potins[j][1];
|
||
|
|
if (x >= left && x <= right && y >= top && y <= bottom) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
labelBounds.push({
|
||
|
|
left: left,
|
||
|
|
right: right,
|
||
|
|
top: top,
|
||
|
|
bottom: bottom
|
||
|
|
});
|
||
|
|
return true;
|
||
|
|
};
|
||
|
|
|
||
|
|
PieceLabel.prototype.measureText = function (text) {
|
||
|
|
if (typeof text === 'object') {
|
||
|
|
return { width: text.width, height: text.height };
|
||
|
|
} else {
|
||
|
|
return this.ctx.measureText(text);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
PieceLabel.prototype.fillText = function (text, position, fontColor) {
|
||
|
|
var ctx = this.ctx;
|
||
|
|
if (typeof text === 'object') {
|
||
|
|
ctx.drawImage(text, position.x - text.width / 2, position.y - text.height / 2, text.width, text.height);
|
||
|
|
} else {
|
||
|
|
ctx.fillStyle = fontColor;
|
||
|
|
ctx.textBaseline = 'top';
|
||
|
|
ctx.textAlign = 'center';
|
||
|
|
ctx.fillText(text, position.x, position.y - this.fontSize / 2);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
PieceLabel.prototype.loadImage = function (obj) {
|
||
|
|
var image = new Image();
|
||
|
|
image.src = obj.src;
|
||
|
|
image.width = obj.width;
|
||
|
|
image.height = obj.height;
|
||
|
|
return image;
|
||
|
|
};
|
||
|
|
|
||
|
|
PieceLabel.prototype.drawArcText = function (str, radius, view, overlap) {
|
||
|
|
var ctx = this.ctx,
|
||
|
|
centerX = view.x,
|
||
|
|
centerY = view.y,
|
||
|
|
startAngle = view.startAngle,
|
||
|
|
endAngle = view.endAngle;
|
||
|
|
|
||
|
|
ctx.save();
|
||
|
|
ctx.translate(centerX, centerY);
|
||
|
|
var angleSize = endAngle - startAngle;
|
||
|
|
startAngle += Math.PI / 2;
|
||
|
|
endAngle += Math.PI / 2;
|
||
|
|
var origStartAngle = startAngle;
|
||
|
|
var mertrics = this.measureText(str);
|
||
|
|
startAngle += (endAngle - (mertrics.width / radius + startAngle)) / 2;
|
||
|
|
if (!overlap && endAngle - startAngle > angleSize) {
|
||
|
|
ctx.restore();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (typeof str === 'string') {
|
||
|
|
ctx.rotate(startAngle);
|
||
|
|
for (var i = 0; i < str.length; i++) {
|
||
|
|
var char = str.charAt(i);
|
||
|
|
mertrics = ctx.measureText(char);
|
||
|
|
ctx.save();
|
||
|
|
ctx.translate(0, -1 * radius);
|
||
|
|
ctx.fillText(char, 0, 0);
|
||
|
|
ctx.restore();
|
||
|
|
ctx.rotate(mertrics.width / radius);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
ctx.rotate((origStartAngle + endAngle) / 2);
|
||
|
|
ctx.translate(0, -1 * radius);
|
||
|
|
this.fillText(str, { x: 0, y: 0 });
|
||
|
|
}
|
||
|
|
ctx.restore();
|
||
|
|
};
|
||
|
|
|
||
|
|
Chart.pluginService.register({
|
||
|
|
beforeInit: function(chartInstance) {
|
||
|
|
chartInstance.pieceLabel = new PieceLabel();
|
||
|
|
},
|
||
|
|
beforeDatasetsUpdate: function (chartInstance) {
|
||
|
|
chartInstance.pieceLabel.beforeDatasetsUpdate(chartInstance);
|
||
|
|
},
|
||
|
|
afterDatasetsDraw: function (chartInstance) {
|
||
|
|
chartInstance.pieceLabel.afterDatasetsDraw(chartInstance);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
})();
|