Skip to content

Commit

Permalink
Radial Linear Scale supports multiple lines for point labels
Browse files Browse the repository at this point in the history
  • Loading branch information
etimberg committed Nov 20, 2016
1 parent f1dd8b6 commit e1606f8
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 122 deletions.
2 changes: 1 addition & 1 deletion samples/radar/radar.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
var config = {
type: 'radar',
data: {
labels: ["Eating", "Drinking", "Sleeping", "Designing", "Coding", "Cycling", "Running"],
labels: [["Eating", "Dinner"], ["Drinking", "Water"], "Sleeping", ["Designing", "Graphics"], "Coding", "Cycling", "Running"],
datasets: [{
label: "My First dataset",
backgroundColor: color(window.chartColors.red).alpha(0.2).rgbString(),
Expand Down
266 changes: 161 additions & 105 deletions src/scales/scale.radialLinear.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,54 @@ module.exports = function(Chart) {
return !scale.options.lineArc ? scale.chart.data.labels.length : 0;
}

function getPointLabelFontOptions(scale) {
var pointLabelOptions = scale.options.pointLabels;
var fontSize = helpers.getValueOrDefault(pointLabelOptions.fontSize, globalDefaults.defaultFontSize);
var fontStyle = helpers.getValueOrDefault(pointLabelOptions.fontStyle, globalDefaults.defaultFontStyle);
var fontFamily = helpers.getValueOrDefault(pointLabelOptions.fontFamily, globalDefaults.defaultFontFamily);
var font = helpers.fontString(fontSize, fontStyle, fontFamily);

return {
size: fontSize,
style: fontStyle,
family: fontFamily,
font: font
};
}

function measureLabelSize(ctx, fontSize, label) {
if (helpers.isArray(label)) {
return {
w: helpers.longestText(ctx, ctx.font, label),
h: (label.length * fontSize) + ((label.length - 1) * 1.5 * fontSize)
};
}

return {
w: ctx.measureText(label).width,
h: fontSize
};
}

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 - 5,
end: pos
};
}

return {
start: pos,
end: pos + size + 5
};
}

/**
* Helper function to fit a radial linear scale with point labels
*/
Expand Down Expand Up @@ -83,82 +131,59 @@ module.exports = function(Chart) {
* https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif
*/

var pointLabels = scale.options.pointLabels;
var pointLabelFontSize = helpers.getValueOrDefault(pointLabels.fontSize, globalDefaults.defaultFontSize);
var pointLabeFontStyle = helpers.getValueOrDefault(pointLabels.fontStyle, globalDefaults.defaultFontStyle);
var pointLabeFontFamily = helpers.getValueOrDefault(pointLabels.fontFamily, globalDefaults.defaultFontFamily);
var pointLabeFont = helpers.fontString(pointLabelFontSize, pointLabeFontStyle, pointLabeFontFamily);
var plFont = getPointLabelFontOptions(scale);

// 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 largestPossibleRadius = Math.min(scale.height / 2 - pointLabelFontSize - 5, scale.width / 2),
pointPosition,
i,
textWidth,
halfTextWidth,
furthestRight = scale.width,
furthestRightIndex,
furthestRightAngle,
furthestLeft = 0,
furthestLeftIndex,
furthestLeftAngle,
xProtrusionLeft,
xProtrusionRight,
radiusReductionRight,
radiusReductionLeft;
scale.ctx.font = pointLabeFont;
var largestPossibleRadius = Math.min(scale.height / 2, scale.width / 2);
var furthestLimits = {
l: scale.width,
r: 0,
t: scale.height,
b: 0
};
var furthestAngles = {};
var i;
var textSize;
var pointPosition;

scale.ctx.font = plFont.font;
scale._pointLabelSizes = [];

var valueCount = getValueCount(scale);
for (i = 0; i < valueCount; i++) {
// 5px to space the text slightly out - similar to what we do in the draw function.
pointPosition = scale.getPointPosition(i, largestPossibleRadius);
textWidth = scale.ctx.measureText(scale.pointLabels[i] ? scale.pointLabels[i] : '').width + 5;
textSize = measureLabelSize(scale.ctx, plFont.size, scale.pointLabels[i] || '');
scale._pointLabelSizes[i] = textSize;

// Add quarter circle to make degree 0 mean top of circle
var angleRadians = scale.getIndexAngle(i) + (Math.PI / 2);
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 (angle === 0 || angle === 180) {
// At angle 0 and 180, we're at exactly the top/bottom
// of the radar chart, so text will be aligned centrally, so we'll half it and compare
// w/left and right text sizes
halfTextWidth = textWidth / 2;
if (pointPosition.x + halfTextWidth > furthestRight) {
furthestRight = pointPosition.x + halfTextWidth;
furthestRightIndex = i;
}
if (pointPosition.x - halfTextWidth < furthestLeft) {
furthestLeft = pointPosition.x - halfTextWidth;
furthestLeftIndex = i;
}
} else if (angle < 180) {
// Less than half the values means we'll left align the text
if (pointPosition.x + textWidth > furthestRight) {
furthestRight = pointPosition.x + textWidth;
furthestRightIndex = i;
}
// More than half the values means we'll right align the text
} else if (pointPosition.x - textWidth < furthestLeft) {
furthestLeft = pointPosition.x - textWidth;
furthestLeftIndex = i;
if (hLimits.start < furthestLimits.l) {
furthestLimits.l = hLimits.start;
furthestAngles.l = angleRadians;
}
}

xProtrusionLeft = furthestLeft;
xProtrusionRight = Math.ceil(furthestRight - scale.width);

furthestRightAngle = scale.getIndexAngle(furthestRightIndex);
furthestLeftAngle = scale.getIndexAngle(furthestLeftIndex);
if (hLimits.end > furthestLimits.r) {
furthestLimits.r = hLimits.end;
furthestAngles.r = angleRadians;
}

radiusReductionRight = xProtrusionRight / Math.sin(furthestRightAngle + Math.PI / 2);
radiusReductionLeft = xProtrusionLeft / Math.sin(furthestLeftAngle + Math.PI / 2);
if (vLimits.start < furthestLimits.t) {
furthestLimits.t = vLimits.start;
furthestAngles.t = angleRadians;
}

// Ensure we actually need to reduce the size of the chart
radiusReductionRight = helpers.isNumber(radiusReductionRight) ? radiusReductionRight : 0;
radiusReductionLeft = helpers.isNumber(radiusReductionLeft) ? radiusReductionLeft : 0;
if (vLimits.end > furthestLimits.b) {
furthestLimits.b = vLimits.end;
furthestAngles.b = angleRadians;
}
}

scale.drawingArea = Math.round(largestPossibleRadius - (radiusReductionLeft + radiusReductionRight) / 2);
scale.setCenterPoint(radiusReductionLeft, radiusReductionRight);
scale.setReductions(largestPossibleRadius, furthestLimits, furthestAngles);
}

/**
Expand All @@ -167,32 +192,39 @@ module.exports = function(Chart) {
function fit(scale) {
var largestPossibleRadius = Math.min(scale.height / 2, scale.width / 2);
scale.drawingArea = Math.round(largestPossibleRadius);
scale.setCenterPoint(0, 0);
scale.setCenterPoint(0, 0, 0, 0);
}

function getTextAlignForAngle(angle) {
var textAlign;
if (angle === 0 || angle === 180) {
textAlign = 'center';
return 'center';
} else if (angle < 180) {
textAlign = 'left';
} else {
textAlign = 'right';
return 'left';
}

return textAlign;
return 'right';
}

function getTextBaselineForAngle(angle) {
var textBaseline;
function fillText(ctx, text, position, fontSize) {
if (helpers.isArray(text)) {
var y = position.y;
var spacing = 1.5 * fontSize;

for (var i = 0; i < text.length; ++i) {
ctx.fillText(text[i], position.x, y);
y+= spacing;
}
} else {
ctx.fillText(text, position.x, position.y);
}
}

function adjustPointPositionForLabelHeight(angle, textSize, position) {
if (angle === 90 || angle === 270) {
textBaseline = 'middle';
position.y -= (textSize.h / 2);
} else if (angle > 270 || angle < 90) {
textBaseline = 'bottom';
} else {
textBaseline = 'top';
position.y -= textSize.h;
}
return textBaseline;
}

function drawPointLabels(scale) {
Expand All @@ -208,10 +240,9 @@ module.exports = function(Chart) {
var outerDistance = scale.getDistanceFromCenterForValue(opts.reverse ? scale.min : scale.max);

// Point Label Font
var pointLabelFontSize = getValueOrDefault(pointLabelOpts.fontSize, globalDefaults.defaultFontSize);
var pointLabeFontStyle = getValueOrDefault(pointLabelOpts.fontStyle, globalDefaults.defaultFontStyle);
var pointLabeFontFamily = getValueOrDefault(pointLabelOpts.fontFamily, globalDefaults.defaultFontFamily);
var pointLabeFont = helpers.fontString(pointLabelFontSize, pointLabeFontStyle, pointLabeFontFamily);
var plFont = getPointLabelFontOptions(scale);

ctx.textBaseline = 'top';

for (var i = getValueCount(scale) - 1; i >= 0; i--) {
if (angleLineOpts.display) {
Expand All @@ -227,19 +258,14 @@ module.exports = function(Chart) {

// Keep this in loop since we may support array properties here
var pointLabelFontColor = getValueOrDefault(pointLabelOpts.fontColor, globalDefaults.defaultFontColor);
ctx.font = pointLabeFont;
ctx.font = plFont.font;
ctx.fillStyle = pointLabelFontColor;

var pointLabels = scale.pointLabels;

// Add quarter circle to make degree 0 mean top of circle
var angleRadians = scale.getIndexAngle(i) + (Math.PI / 2);
var angle = (angleRadians * 360 / (2 * Math.PI)) % 360;

var angleRadians = scale.getIndexAngle(i);
var angle = helpers.toDegrees(angleRadians);
ctx.textAlign = getTextAlignForAngle(angle);
ctx.textBaseline = getTextBaselineForAngle(angle);

ctx.fillText(pointLabels[i] || '', pointLabelPosition.x, pointLabelPosition.y);
adjustPointPositionForLabelHeight(angle, scale._pointLabelSizes[i], pointLabelPosition);
fillText(ctx, scale.pointLabels[i] || '', pointLabelPosition, plFont.size);
}
}

Expand All @@ -256,21 +282,30 @@ module.exports = function(Chart) {
ctx.stroke();
} else {
// Draw straight lines connecting each index
ctx.beginPath();
var valueCount = getValueCount(scale);
for (var i = 0; i < valueCount; i++) {
var pointPosition = scale.getPointPosition(i, radius);
if (i === 0) {
ctx.moveTo(pointPosition.x, pointPosition.y);
} else {
ctx.lineTo(pointPosition.x, pointPosition.y);
}

if (valueCount === 0) {
return;
}

ctx.beginPath();
var 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();
}
}

function numberOrZero(param) {
return helpers.isNumber(param) ? param : 0;
}

var LinearRadialScale = Chart.LinearScaleBase.extend({
setDimensions: function() {
var me = this;
Expand All @@ -292,7 +327,6 @@ module.exports = function(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);
Expand All @@ -309,8 +343,8 @@ module.exports = function(Chart) {
}
});

me.min = min;
me.max = 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();
Expand All @@ -337,14 +371,36 @@ module.exports = function(Chart) {
fitWithPointLabels(this);
}
},
setCenterPoint: function(leftMovement, rightMovement) {
/**
* 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, 0) / Math.cos(furthestAngles.b);

radiusReductionLeft = numberOrZero(radiusReductionLeft);
radiusReductionRight = numberOrZero(radiusReductionRight);
radiusReductionTop = numberOrZero(radiusReductionTop);
radiusReductionBottom = numberOrZero(radiusReductionBottom);

me.drawingArea = Math.min(
Math.round(largestPossibleRadius - (radiusReductionLeft + radiusReductionRight) / 2),
Math.round(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,
maxLeft = leftMovement + me.drawingArea;
maxLeft = leftMovement + me.drawingArea,
maxTop = topMovement + me.drawingArea,
maxBottom = me.height - bottomMovement - me.drawingArea;

me.xCenter = Math.round(((maxLeft + maxRight) / 2) + me.left);
// Always vertically in the centre as the text height doesn't change
me.yCenter = Math.round((me.height / 2) + me.top);
me.yCenter = Math.round(((maxTop + maxBottom) / 2) + me.top);
},

getIndexAngle: function(index) {
Expand All @@ -356,7 +412,7 @@ module.exports = function(Chart) {
var startAngleRadians = startAngle * Math.PI * 2 / 360;

// Start from the top instead of right, so remove a quarter of the circle
return index * angleMultiplier - (Math.PI / 2) + startAngleRadians;
return index * angleMultiplier + startAngleRadians;
},
getDistanceFromCenterForValue: function(value) {
var me = this;
Expand All @@ -374,7 +430,7 @@ module.exports = function(Chart) {
},
getPointPosition: function(index, distanceFromCenter) {
var me = this;
var thisAngle = me.getIndexAngle(index);
var thisAngle = me.getIndexAngle(index) - (Math.PI / 2);
return {
x: Math.round(Math.cos(thisAngle) * distanceFromCenter) + me.xCenter,
y: Math.round(Math.sin(thisAngle) * distanceFromCenter) + me.yCenter
Expand Down

0 comments on commit e1606f8

Please sign in to comment.