Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to position labels around circle?

Tags:

c#

wpf

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.

like image 444
SledgeHammer Avatar asked Mar 28 '16 18:03

SledgeHammer


2 Answers

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:

enter image description here

  1. Determine dimensions of your circle and desired margin (on image: blue dashed circle).
  2. Determine size of label text (on image: black frame around Label).
  3. Determine point on (black) label text frame which should touch the blue dashed circle. For each dimension (X, Y) separately:
    • If the center point of text label is within half of size of label from axis (on image: within orange double line - see the label above the circle), then your point is on the edge of label text frame
    • Otherwise, the point is at the corner of label text frame.
  4. 'Move' the label from the middle of the circle, so its black frame touches blue dashed line in the point you determined in step 3. (on image: red line from center of the circle to center of the Label)
  5. Based on the point which touches the circle, determine top left point of your label and draw the label at resulting position.

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.

like image 154
miroxlav Avatar answered Oct 18 '22 14:10

miroxlav


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.

like image 25
Evk Avatar answered Oct 18 '22 16:10

Evk