Skip to content

Commit 5eb9287

Browse files
committed
Better label positioning (#31)
Labels should track with the line slope, and should not fall out of the edge of the canvas.
1 parent 6c1c925 commit 5eb9287

File tree

5 files changed

+153
-76
lines changed

5 files changed

+153
-76
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,28 +65,44 @@ Vertical or horizontal lines are supported.
6565
label: {
6666
// Background color of label, default below
6767
backgroundColor: 'rgba(0,0,0,0.8)',
68+
6869
// Font family of text, inherits from global
6970
fontFamily: "sans-serif",
71+
7072
// Font size of text, inherits from global
7173
fontSize: 12,
74+
7275
// Font style of text, default below
7376
fontStyle: "bold",
77+
7478
// Font color of text, default below
7579
fontColor: "#fff",
80+
7681
// Padding of label to add left/right, default below
7782
xPadding: 6,
83+
7884
// Padding of label to add top/bottom, default below
7985
yPadding: 6,
86+
8087
// Radius of label rectangle, default below
8188
cornerRadius: 6,
89+
8290
// Anchor position of label on line, can be one of: top, bottom, left, right, center. Default below.
8391
position: "center",
92+
8493
// Adjustment along x-axis (left-right) of label relative to above number (can be negative)
94+
// For horizontal lines positioned left or right, negative values move
95+
// the label toward the edge, and negative values toward the center.
8596
xAdjust: 0,
97+
8698
// Adjustment along y-axis (top-bottom) of label relative to above number (can be negative)
99+
// For vertical lines positioned top or bottom, negative values move
100+
// the label toward the edge, and negative values toward the center.
87101
yAdjust: 0,
102+
88103
// Whether the label is enabled and should be displayed
89104
enabled: false,
105+
90106
// Text to display in label - default is null
91107
content: "Test label"
92108
}

chartjs-plugin-annotation.js

Lines changed: 66 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -250,8 +250,8 @@ module.exports = function(Chart) {
250250
// Draw the tooltip
251251
helpers.drawRoundedRectangle(
252252
ctx,
253-
view.labelX - view.labelXPadding, // x
254-
view.labelY - view.labelYPadding, // y
253+
view.labelX, // x
254+
view.labelY, // y
255255
view.labelWidth, // width
256256
view.labelHeight, // height
257257
view.labelCornerRadius // radius
@@ -265,12 +265,12 @@ module.exports = function(Chart) {
265265
view.labelFontFamily
266266
);
267267
ctx.fillStyle = view.labelFontColor;
268-
ctx.textAlign = 'left';
269-
ctx.textBaseline = 'top';
268+
ctx.textAlign = 'center';
269+
ctx.textBaseline = 'middle';
270270
ctx.fillText(
271271
view.labelContent,
272-
view.labelX,
273-
view.labelY
272+
view.labelX + (view.labelWidth / 2),
273+
view.labelY + (view.labelHeight / 2)
274274
);
275275
}
276276
}
@@ -280,27 +280,62 @@ module.exports = function(Chart) {
280280
return !isNaN(num) && isFinite(num);
281281
}
282282

283-
function calculateLabelPosition(view, width, height) {
284-
var ret = {
285-
x: ((view.x1 + view.x2 - width) / 2),
286-
y: ((view.y1 + view.y2 - height) / 2)
283+
function calculateLabelPosition(view, width, height, padWidth, padHeight) {
284+
// Describe the line in slope-intercept form (y = mx + b).
285+
// Note that the axes are rotated 90° CCW, which causes the
286+
// x- and y-axes to be swapped.
287+
var m = (view.x2 - view.x1) / (view.y2 - view.y1);
288+
var b = view.x1 || 0;
289+
290+
var fy = function(y) {
291+
// Coordinates are relative to the origin of the canvas
292+
return m * (y - view.y1) + b;
293+
};
294+
var fx = function(x) {
295+
return ((x - b) / m) + view.y1;
287296
};
288-
switch (view.labelPosition) {
289-
case "top":
290-
ret.y = view.y1 > view.y2 ? view.y2 : view.y1;
297+
298+
var ret = {}, xa = 0, ya = 0;
299+
300+
switch (true) {
301+
// top align
302+
case view.mode == verticalKeyword && view.labelPosition == "top":
303+
ya = padHeight + view.labelYAdjust;
304+
xa = (width / 2) + view.labelXAdjust;
305+
ret.y = view.y1 + ya;
306+
ret.x = (isFinite(m) ? fy(ret.y) : view.x1) - xa;
291307
break;
292-
case "left":
293-
ret.x = view.x1 > view.x2 ? view.x1 : view.x2;
308+
309+
// bottom align
310+
case view.mode == verticalKeyword && view.labelPosition == "bottom":
311+
ya = height + padHeight + view.labelYAdjust;
312+
xa = (width / 2) + view.labelXAdjust;
313+
ret.y = view.y2 - ya;
314+
ret.x = (isFinite(m) ? fy(ret.y) : view.x1) - xa;
294315
break;
295-
case "bottom":
296-
ret.y = view.y1 > view.y2 ? view.y1 : view.y2;
316+
317+
// left align
318+
case view.mode == horizontalKeyword && view.labelPosition == "left":
319+
xa = padWidth + view.labelXAdjust;
320+
ya = -(height / 2) + view.labelYAdjust;
321+
ret.x = view.x1 + xa;
322+
ret.y = fx(ret.x) + ya;
297323
break;
298-
case "right":
299-
ret.x = view.x1 > view.x2 ? view.x2 : view.x1;
324+
325+
// right align
326+
case view.mode == horizontalKeyword && view.labelPosition == "right":
327+
xa = width + padWidth + view.labelXAdjust;
328+
ya = -(height / 2) + view.labelYAdjust;
329+
ret.x = view.x2 - xa;
330+
ret.y = fx(ret.x) + ya;
300331
break;
332+
333+
// center align
334+
default:
335+
ret.x = ((view.x1 + view.x2 - width) / 2) + view.labelXAdjust;
336+
ret.y = ((view.y1 + view.y2 - height) / 2) + view.labelYAdjust;
301337
}
302-
ret.x += view.labelXAdjust;
303-
ret.y += view.labelYAdjust;
338+
304339
return ret;
305340
}
306341

@@ -329,6 +364,8 @@ module.exports = function(Chart) {
329364
}
330365
}
331366

367+
model.mode = options.mode;
368+
332369
// Figure out the label:
333370
model.labelBackgroundColor = options.label.backgroundColor;
334371
model.labelFontFamily = options.label.fontFamily;
@@ -344,21 +381,14 @@ module.exports = function(Chart) {
344381
model.labelEnabled = options.label.enabled;
345382
model.labelContent = options.label.content;
346383

347-
ctx.font = helpers.fontString(
348-
model.labelFontSize,
349-
model.labelFontStyle,
350-
model.labelFontFamily
351-
);
352-
var text = ctx.measureText(model.labelContent);
353-
var position = calculateLabelPosition(
354-
model,
355-
text.width,
356-
model.labelFontSize
357-
);
358-
model.labelX = position.x;
359-
model.labelY = position.y;
360-
model.labelWidth = text.width + (2 * model.labelXPadding);
361-
model.labelHeight = model.labelFontSize + (2 * model.labelYPadding)
384+
ctx.font = helpers.fontString(model.labelFontSize, model.labelFontStyle, model.labelFontFamily);
385+
var textWidth = ctx.measureText(model.labelContent).width;
386+
var textHeight = ctx.measureText('M').width;
387+
var labelPosition = calculateLabelPosition(model, textWidth, textHeight, model.labelXPadding, model.labelYPadding);
388+
model.labelX = labelPosition.x - model.labelXPadding;
389+
model.labelY = labelPosition.y - model.labelYPadding;
390+
model.labelWidth = textWidth + (2 * model.labelXPadding);
391+
model.labelHeight = textHeight + (2 * model.labelYPadding);
362392

363393
model.borderColor = options.borderColor;
364394
model.borderWidth = options.borderWidth;

chartjs-plugin-annotation.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

samples/labels.html

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,10 @@
132132
mode: 'horizontal',
133133
scaleID: 'y-axis-1',
134134
value: 25,
135-
endValue: 45,
136135
borderColor: 'black',
137136
borderWidth: 5,
138137
label: {
138+
yAdjust: -20,
139139
backgroundColor: "red",
140140
content: "This is a long test label",
141141
enabled: true
@@ -146,11 +146,12 @@
146146
mode: 'vertical',
147147
scaleID: 'x-axis-1',
148148
value: 25,
149+
endValue: 45,
149150
borderColor: 'red',
150151
borderWidth: 5,
151152
label: {
152-
position: "top",
153-
yAdjust: 15,
153+
position: "bottom",
154+
yAdjust: 100,
154155
content: "This is a another test label",
155156
enabled: true
156157
}

0 commit comments

Comments
 (0)