Files
Chamilo/web/assets/chart.js/src/scales/scale.radialLinear.js
2025-04-10 11:37:29 +02:00

559 lines
16 KiB
JavaScript

'use strict';
var defaults = require('../core/core.defaults');
var helpers = require('../helpers/index');
var LinearScaleBase = require('./scale.linearbase');
var Ticks = require('../core/core.ticks');
var valueOrDefault = helpers.valueOrDefault;
var valueAtIndexOrDefault = helpers.valueAtIndexOrDefault;
var resolve = helpers.options.resolve;
var defaultConfig = {
display: true,
// Boolean - Whether to animate scaling the chart from the centre
animate: true,
position: 'chartArea',
angleLines: {
display: true,
color: 'rgba(0,0,0,0.1)',
lineWidth: 1,
borderDash: [],
borderDashOffset: 0.0
},
gridLines: {
circular: false
},
// label settings
ticks: {
// Boolean - Show a backdrop to the scale label
showLabelBackdrop: true,
// String - The colour of the label backdrop
backdropColor: 'rgba(255,255,255,0.75)',
// Number - The backdrop padding above & below the label in pixels
backdropPaddingY: 2,
// Number - The backdrop padding to the side of the label in pixels
backdropPaddingX: 2,
callback: Ticks.formatters.linear
},
pointLabels: {
// Boolean - if true, show point labels
display: true,
// Number - Point label font size in pixels
fontSize: 10,
// Function - Used to convert point labels
callback: function(label) {
return label;
}
}
};
function getTickBackdropHeight(opts) {
var tickOpts = opts.ticks;
if (tickOpts.display && opts.display) {
return valueOrDefault(tickOpts.fontSize, defaults.global.defaultFontSize) + tickOpts.backdropPaddingY * 2;
}
return 0;
}
function measureLabelSize(ctx, lineHeight, label) {
if (helpers.isArray(label)) {
return {
w: helpers.longestText(ctx, ctx.font, label),
h: label.length * lineHeight
};
}
return {
w: ctx.measureText(label).width,
h: lineHeight
};
}
function determineLimits(angle, pos, size, min, max) {
if (angle === min || angle === max) {
return {
start: pos - (size / 2),
end: pos + (size / 2)
};
} else if (angle < min || angle > max) {
return {
start: pos - size,
end: pos
};
}
return {
start: pos,
end: pos + size
};
}
/**
* Helper function to fit a radial linear scale with point labels
*/
function fitWithPointLabels(scale) {
// Right, this is really confusing and there is a lot of maths going on here
// The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9
//
// Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif
//
// Solution:
//
// We assume the radius of the polygon is half the size of the canvas at first
// at each index we check if the text overlaps.
//
// Where it does, we store that angle and that index.
//
// After finding the largest index and angle we calculate how much we need to remove
// from the shape radius to move the point inwards by that x.
//
// We average the left and right distances to get the maximum shape radius that can fit in the box
// along with labels.
//
// Once we have that, we can find the centre point for the chart, by taking the x text protrusion
// on each side, removing that from the size, halving it and adding the left x protrusion width.
//
// This will mean we have a shape fitted to the canvas, as large as it can be with the labels
// and position it in the most space efficient manner
//
// https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif
var plFont = helpers.options._parseFont(scale.options.pointLabels);
// Get maximum radius of the polygon. Either half the height (minus the text width) or half the width.
// Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points
var furthestLimits = {
l: 0,
r: scale.width,
t: 0,
b: scale.height - scale.paddingTop
};
var furthestAngles = {};
var i, textSize, pointPosition;
scale.ctx.font = plFont.string;
scale._pointLabelSizes = [];
var valueCount = scale.chart.data.labels.length;
for (i = 0; i < valueCount; i++) {
pointPosition = scale.getPointPosition(i, scale.drawingArea + 5);
textSize = measureLabelSize(scale.ctx, plFont.lineHeight, scale.pointLabels[i]);
scale._pointLabelSizes[i] = textSize;
// Add quarter circle to make degree 0 mean top of circle
var angleRadians = scale.getIndexAngle(i);
var angle = helpers.toDegrees(angleRadians) % 360;
var hLimits = determineLimits(angle, pointPosition.x, textSize.w, 0, 180);
var vLimits = determineLimits(angle, pointPosition.y, textSize.h, 90, 270);
if (hLimits.start < furthestLimits.l) {
furthestLimits.l = hLimits.start;
furthestAngles.l = angleRadians;
}
if (hLimits.end > furthestLimits.r) {
furthestLimits.r = hLimits.end;
furthestAngles.r = angleRadians;
}
if (vLimits.start < furthestLimits.t) {
furthestLimits.t = vLimits.start;
furthestAngles.t = angleRadians;
}
if (vLimits.end > furthestLimits.b) {
furthestLimits.b = vLimits.end;
furthestAngles.b = angleRadians;
}
}
scale.setReductions(scale.drawingArea, furthestLimits, furthestAngles);
}
function getTextAlignForAngle(angle) {
if (angle === 0 || angle === 180) {
return 'center';
} else if (angle < 180) {
return 'left';
}
return 'right';
}
function fillText(ctx, text, position, lineHeight) {
var y = position.y + lineHeight / 2;
var i, ilen;
if (helpers.isArray(text)) {
for (i = 0, ilen = text.length; i < ilen; ++i) {
ctx.fillText(text[i], position.x, y);
y += lineHeight;
}
} else {
ctx.fillText(text, position.x, y);
}
}
function adjustPointPositionForLabelHeight(angle, textSize, position) {
if (angle === 90 || angle === 270) {
position.y -= (textSize.h / 2);
} else if (angle > 270 || angle < 90) {
position.y -= textSize.h;
}
}
function drawPointLabels(scale) {
var ctx = scale.ctx;
var opts = scale.options;
var pointLabelOpts = opts.pointLabels;
var tickBackdropHeight = getTickBackdropHeight(opts);
var outerDistance = scale.getDistanceFromCenterForValue(opts.ticks.reverse ? scale.min : scale.max);
var plFont = helpers.options._parseFont(pointLabelOpts);
ctx.save();
ctx.font = plFont.string;
ctx.textBaseline = 'middle';
for (var i = scale.chart.data.labels.length - 1; i >= 0; i--) {
// Extra pixels out for some label spacing
var extra = (i === 0 ? tickBackdropHeight / 2 : 0);
var pointLabelPosition = scale.getPointPosition(i, outerDistance + extra + 5);
// Keep this in loop since we may support array properties here
var pointLabelFontColor = valueAtIndexOrDefault(pointLabelOpts.fontColor, i, defaults.global.defaultFontColor);
ctx.fillStyle = pointLabelFontColor;
var angleRadians = scale.getIndexAngle(i);
var angle = helpers.toDegrees(angleRadians);
ctx.textAlign = getTextAlignForAngle(angle);
adjustPointPositionForLabelHeight(angle, scale._pointLabelSizes[i], pointLabelPosition);
fillText(ctx, scale.pointLabels[i], pointLabelPosition, plFont.lineHeight);
}
ctx.restore();
}
function drawRadiusLine(scale, gridLineOpts, radius, index) {
var ctx = scale.ctx;
var circular = gridLineOpts.circular;
var valueCount = scale.chart.data.labels.length;
var lineColor = valueAtIndexOrDefault(gridLineOpts.color, index - 1);
var lineWidth = valueAtIndexOrDefault(gridLineOpts.lineWidth, index - 1);
var pointPosition;
if ((!circular && !valueCount) || !lineColor || !lineWidth) {
return;
}
ctx.save();
ctx.strokeStyle = lineColor;
ctx.lineWidth = lineWidth;
if (ctx.setLineDash) {
ctx.setLineDash(gridLineOpts.borderDash || []);
ctx.lineDashOffset = gridLineOpts.borderDashOffset || 0.0;
}
ctx.beginPath();
if (circular) {
// Draw circular arcs between the points
ctx.arc(scale.xCenter, scale.yCenter, radius, 0, Math.PI * 2);
} else {
// Draw straight lines connecting each index
pointPosition = scale.getPointPosition(0, radius);
ctx.moveTo(pointPosition.x, pointPosition.y);
for (var i = 1; i < valueCount; i++) {
pointPosition = scale.getPointPosition(i, radius);
ctx.lineTo(pointPosition.x, pointPosition.y);
}
}
ctx.closePath();
ctx.stroke();
ctx.restore();
}
function numberOrZero(param) {
return helpers.isNumber(param) ? param : 0;
}
module.exports = LinearScaleBase.extend({
setDimensions: function() {
var me = this;
// Set the unconstrained dimension before label rotation
me.width = me.maxWidth;
me.height = me.maxHeight;
me.paddingTop = getTickBackdropHeight(me.options) / 2;
me.xCenter = Math.floor(me.width / 2);
me.yCenter = Math.floor((me.height - me.paddingTop) / 2);
me.drawingArea = Math.min(me.height - me.paddingTop, me.width) / 2;
},
determineDataLimits: function() {
var me = this;
var chart = me.chart;
var min = Number.POSITIVE_INFINITY;
var max = Number.NEGATIVE_INFINITY;
helpers.each(chart.data.datasets, function(dataset, datasetIndex) {
if (chart.isDatasetVisible(datasetIndex)) {
var meta = chart.getDatasetMeta(datasetIndex);
helpers.each(dataset.data, function(rawValue, index) {
var value = +me.getRightValue(rawValue);
if (isNaN(value) || meta.data[index].hidden) {
return;
}
min = Math.min(value, min);
max = Math.max(value, max);
});
}
});
me.min = (min === Number.POSITIVE_INFINITY ? 0 : min);
me.max = (max === Number.NEGATIVE_INFINITY ? 0 : max);
// Common base implementation to handle ticks.min, ticks.max, ticks.beginAtZero
me.handleTickRangeOptions();
},
// Returns the maximum number of ticks based on the scale dimension
_computeTickLimit: function() {
return Math.ceil(this.drawingArea / getTickBackdropHeight(this.options));
},
convertTicksToLabels: function() {
var me = this;
LinearScaleBase.prototype.convertTicksToLabels.call(me);
// Point labels
me.pointLabels = me.chart.data.labels.map(function() {
var label = helpers.callback(me.options.pointLabels.callback, arguments, me);
return label || label === 0 ? label : '';
});
},
getLabelForIndex: function(index, datasetIndex) {
return +this.getRightValue(this.chart.data.datasets[datasetIndex].data[index]);
},
fit: function() {
var me = this;
var opts = me.options;
if (opts.display && opts.pointLabels.display) {
fitWithPointLabels(me);
} else {
me.setCenterPoint(0, 0, 0, 0);
}
},
/**
* Set radius reductions and determine new radius and center point
* @private
*/
setReductions: function(largestPossibleRadius, furthestLimits, furthestAngles) {
var me = this;
var radiusReductionLeft = furthestLimits.l / Math.sin(furthestAngles.l);
var radiusReductionRight = Math.max(furthestLimits.r - me.width, 0) / Math.sin(furthestAngles.r);
var radiusReductionTop = -furthestLimits.t / Math.cos(furthestAngles.t);
var radiusReductionBottom = -Math.max(furthestLimits.b - (me.height - me.paddingTop), 0) / Math.cos(furthestAngles.b);
radiusReductionLeft = numberOrZero(radiusReductionLeft);
radiusReductionRight = numberOrZero(radiusReductionRight);
radiusReductionTop = numberOrZero(radiusReductionTop);
radiusReductionBottom = numberOrZero(radiusReductionBottom);
me.drawingArea = Math.min(
Math.floor(largestPossibleRadius - (radiusReductionLeft + radiusReductionRight) / 2),
Math.floor(largestPossibleRadius - (radiusReductionTop + radiusReductionBottom) / 2));
me.setCenterPoint(radiusReductionLeft, radiusReductionRight, radiusReductionTop, radiusReductionBottom);
},
setCenterPoint: function(leftMovement, rightMovement, topMovement, bottomMovement) {
var me = this;
var maxRight = me.width - rightMovement - me.drawingArea;
var maxLeft = leftMovement + me.drawingArea;
var maxTop = topMovement + me.drawingArea;
var maxBottom = (me.height - me.paddingTop) - bottomMovement - me.drawingArea;
me.xCenter = Math.floor(((maxLeft + maxRight) / 2) + me.left);
me.yCenter = Math.floor(((maxTop + maxBottom) / 2) + me.top + me.paddingTop);
},
getIndexAngle: function(index) {
var chart = this.chart;
var angleMultiplier = 360 / chart.data.labels.length;
var options = chart.options || {};
var startAngle = options.startAngle || 0;
// Start from the top instead of right, so remove a quarter of the circle
var angle = (index * angleMultiplier + startAngle) % 360;
return (angle < 0 ? angle + 360 : angle) * Math.PI * 2 / 360;
},
getDistanceFromCenterForValue: function(value) {
var me = this;
if (helpers.isNullOrUndef(value)) {
return NaN;
}
// Take into account half font size + the yPadding of the top value
var scalingFactor = me.drawingArea / (me.max - me.min);
if (me.options.ticks.reverse) {
return (me.max - value) * scalingFactor;
}
return (value - me.min) * scalingFactor;
},
getPointPosition: function(index, distanceFromCenter) {
var me = this;
var thisAngle = me.getIndexAngle(index) - (Math.PI / 2);
return {
x: Math.cos(thisAngle) * distanceFromCenter + me.xCenter,
y: Math.sin(thisAngle) * distanceFromCenter + me.yCenter
};
},
getPointPositionForValue: function(index, value) {
return this.getPointPosition(index, this.getDistanceFromCenterForValue(value));
},
getBasePosition: function(index) {
var me = this;
var min = me.min;
var max = me.max;
return me.getPointPositionForValue(index || 0,
me.beginAtZero ? 0 :
min < 0 && max < 0 ? max :
min > 0 && max > 0 ? min :
0);
},
/**
* @private
*/
_drawGrid: function() {
var me = this;
var ctx = me.ctx;
var opts = me.options;
var gridLineOpts = opts.gridLines;
var angleLineOpts = opts.angleLines;
var lineWidth = valueOrDefault(angleLineOpts.lineWidth, gridLineOpts.lineWidth);
var lineColor = valueOrDefault(angleLineOpts.color, gridLineOpts.color);
var i, offset, position;
if (opts.pointLabels.display) {
drawPointLabels(me);
}
if (gridLineOpts.display) {
helpers.each(me.ticks, function(label, index) {
if (index !== 0) {
offset = me.getDistanceFromCenterForValue(me.ticksAsNumbers[index]);
drawRadiusLine(me, gridLineOpts, offset, index);
}
});
}
if (angleLineOpts.display && lineWidth && lineColor) {
ctx.save();
ctx.lineWidth = lineWidth;
ctx.strokeStyle = lineColor;
if (ctx.setLineDash) {
ctx.setLineDash(resolve([angleLineOpts.borderDash, gridLineOpts.borderDash, []]));
ctx.lineDashOffset = resolve([angleLineOpts.borderDashOffset, gridLineOpts.borderDashOffset, 0.0]);
}
for (i = me.chart.data.labels.length - 1; i >= 0; i--) {
offset = me.getDistanceFromCenterForValue(opts.ticks.reverse ? me.min : me.max);
position = me.getPointPosition(i, offset);
ctx.beginPath();
ctx.moveTo(me.xCenter, me.yCenter);
ctx.lineTo(position.x, position.y);
ctx.stroke();
}
ctx.restore();
}
},
/**
* @private
*/
_drawLabels: function() {
var me = this;
var ctx = me.ctx;
var opts = me.options;
var tickOpts = opts.ticks;
if (!tickOpts.display) {
return;
}
var startAngle = me.getIndexAngle(0);
var tickFont = helpers.options._parseFont(tickOpts);
var tickFontColor = valueOrDefault(tickOpts.fontColor, defaults.global.defaultFontColor);
var offset, width;
ctx.save();
ctx.font = tickFont.string;
ctx.translate(me.xCenter, me.yCenter);
ctx.rotate(startAngle);
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
helpers.each(me.ticks, function(label, index) {
if (index === 0 && !tickOpts.reverse) {
return;
}
offset = me.getDistanceFromCenterForValue(me.ticksAsNumbers[index]);
if (tickOpts.showLabelBackdrop) {
width = ctx.measureText(label).width;
ctx.fillStyle = tickOpts.backdropColor;
ctx.fillRect(
-width / 2 - tickOpts.backdropPaddingX,
-offset - tickFont.size / 2 - tickOpts.backdropPaddingY,
width + tickOpts.backdropPaddingX * 2,
tickFont.size + tickOpts.backdropPaddingY * 2
);
}
ctx.fillStyle = tickFontColor;
ctx.fillText(label, 0, -offset);
});
ctx.restore();
},
/**
* @private
*/
_drawTitle: helpers.noop
});
// INTERNAL: static default options, registered in src/index.js
module.exports._defaults = defaultConfig;