I’m trying to figure out how to calculate the position for text labels around a circle. This is slightly more complicated than you may think at first read .
I get the basics:
X = ptCenter.X + (dRadius * Math.Cos(dAngle * Math.PI / 180.0))
Y = ptCenter.Y + (dRadius * Math.Sin(dAngle * Math.PI / 180.0))
So, that will give me the point on the circle at angle dAngle with radius dRadius. Of course, DrawText (any variety, but I’m specifically using DrawingContext.DrawText if that makes any difference to your answer) draws with the given point as the upper left corner of the text.
Problem is, that’s not the correct position to draw the text at. Here is an illustration of the issue:
https://support.office.com/en-us/article/Present-your-data-in-a-radar-chart-16e20279-eed4-43c2-9bf5-29ff9b10601d
Jan is centered horizontally around the point Feb is drawn from the lower left instead Mar seems to be kind of in the middle horizontally Apr is centered vertically Etc.
The labels aren’t positioned around the point in a uniform way. It kind of depends on the angle your drawing at.
The number of labels I need to draw can vary, so hard coding fudge factors is out. Angles I need to draw at can also vary, so no fudge factors on there either. All this has to be calculated on the fly.
Kind of seems like the 0, 90, 180, 270 are special cases while the others seem to be semi centered vertically around the point, but drawn to the right or left based on which side of the circle you’re talking about?
Am I on the right track here? Or is there a “known” algorithm?
Thanks.
You are right, the problem is not so simple as it looks. After sitting down with pencil and paper I think the problem can be de-composed to simpler steps:
Sorry, no formulas here, I would need more time to compose them. But I wanted to show you that positioning labels here takes somewhat more than simple evaluation of sin()
and cos()
in two lines of code.
What you need is center of text to be positioned on the point you calculated. So you need to shift point by half-width in X direction and half-height in Y. This will position labels "inside" the circle. Like this:
public class CircleText : FrameworkElement {
public string[] Labels
{
get { return (string[])GetValue(LabelsProperty); }
set { SetValue(LabelsProperty, value); }
}
public static readonly DependencyProperty LabelsProperty =
DependencyProperty.Register("Labels", typeof(string[]), typeof(CircleText), new PropertyMetadata(null, OnLabelsChanged));
private static void OnLabelsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
((CircleText) d).InvalidateVisual();
}
protected override void OnRender(DrawingContext drawingContext) {
if (Labels == null || Labels.Length == 0)
return;
var centerX = this.ActualWidth / 2;
var centerY = this.ActualHeight / 2;
var rad = Math.Min(this.ActualWidth / 2, this.ActualHeight / 2);
for (int i = 0; i < Labels.Length; i++) {
var angle = 360 / (Labels.Length) * i;
var x = centerX + rad * Math.Cos(angle * Math.PI / 180.0);
var y = centerY + rad * Math.Sin(angle * Math.PI / 180.0);
FormattedText text = new FormattedText(
Labels[i],
CultureInfo.GetCultureInfo("en-us"),
FlowDirection.LeftToRight,
new Typeface("Verdana"),
12,
Brushes.Black);
x -= text.Width / 2;
y -= text.Height / 2;
drawingContext.DrawText(text, new Point(x, y));
}
}
}
If you want to draw lines to your points and you want labels to be outside those lines - you need to shift your labels based on cos and sin values you already calculated. This will position labels "outside", like this:
protected override void OnRender(DrawingContext drawingContext) {
if (Labels == null || Labels.Length == 0)
return;
var centerX = this.ActualWidth / 2;
var centerY = this.ActualHeight / 2;
var rad = Math.Min(this.ActualWidth / 2, this.ActualHeight / 2);
for (int i = 0; i < Labels.Length; i++) {
var angle = 360 / (Labels.Length) * i;
var xshift = Math.Cos(angle * Math.PI / 180.0);
var yshift = Math.Sin(angle * Math.PI / 180.0);
var x = centerX + rad * xshift;
var y = centerY + rad * yshift;
drawingContext.DrawLine(new Pen(Brushes.Black, 1), new Point(centerX, centerY), new Point(x,y));
FormattedText text = new FormattedText(
Labels[i],
CultureInfo.GetCultureInfo("en-us"),
FlowDirection.LeftToRight,
new Typeface("Verdana"),
12,
Brushes.Black);
x -= (1 - xshift) * text.Width / 2;
y -= (1 - yshift) * text.Height / 2;
drawingContext.DrawText(text, new Point(x, y));
}
}
Of course the above works for any number of labels.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With