ComponentOne FlexChart for WinForms
Creating Callouts
FlexChart > Working with FlexChart > FlexChart Elements > Annotations > Creating Callouts

Callouts in charts are used to display the details of a data series or individual data points in an easy-to-read format. Callouts being connected with data points, help better visualize and comprehend chart data by minimizing visual disturbances in the chart area. In FlexChart, Polygon type annotations can be customized to create chart callouts with line or arrow connectors.

In this example, we are using sample created in the Quick Start topic to further create an arrow callout and polygon annotation with line connection. This is done with the help of the Points property and the ContentCenter property that define the coordinates of polygon vertices and annotation content center respectively.

To create callouts connected with respective data points, follow these steps:

The following image illustrates polygon annotations connected to data points through arrow and line connectors.

Annotation connector

Step 1: Create annotation with line connector

To create a line callout, use the following code.

    ...
    ' Create a line callout annotation of polygon type               
    Dim lineCallout = New Polygon("High") With {
.Attachment = AnnotationAttachment.DataIndex,
.SeriesIndex = 1,
.PointIndex = 1,
.ContentCenter = New Point(25, -40)
}
    ' Create a list of points for the line callout annotation      
    lineCallout.Points.Add(New PointF(0, 0))
    lineCallout.Points.Add(New PointF(25, -25))
    lineCallout.Points.Add(New PointF(50, -25))
    lineCallout.Points.Add(New PointF(50, -50))
    lineCallout.Points.Add(New PointF(25, -75))
    lineCallout.Points.Add(New PointF(0, -50))
    lineCallout.Points.Add(New PointF(0, -25))
    lineCallout.Points.Add(New PointF(25, -25))
    lineCallout.Points.Add(New PointF(0, 0))

    ' Stylise the line callout annotation of polygon type
    lineCallout.ContentStyle.StrokeColor = Color.Black
    lineCallout.Style.FillColor = Color.FromArgb(200, Color.Red)
    lineCallout.Style.StrokeColor = Color.Red
    ...
...
// Create a line callout annotation of polygon type
var lineCallout = new Polygon("High")
{
    Attachment = AnnotationAttachment.DataIndex,
    SeriesIndex = 1,
    PointIndex = 1,
    ContentCenter = new Point(25, -40),
    // Create a list of points for the line callout annotation                
    Points = { new Point(0, 0), new Point(25, -25),
        new Point(50, -25), new Point(50, -50), new Point(25, -75),
        new Point(0, -50), new Point(0, -25), new Point(25, -25), new Point(0, 0) }
};
//Stylise the line callout annotation of polygon type
lineCallout.ContentStyle.StrokeColor = Color.Black;
lineCallout.Style.FillColor = Color.FromArgb(200, Color.Red);
lineCallout.Style.StrokeColor = Color.Red;
...

Back to Top

Step 2: Create arrow annotation callout

  1. To create an arrow callout use the following code.
        Private Sub SetUpAnnotations()
            'Create an annotation of Polygon type with arrow connector
            Dim arrowCallout = New Polygon("Low") With {
        .Attachment = AnnotationAttachment.DataIndex,
        .SeriesIndex = 1,
        .PointIndex = 0,
        .ContentCenter = New PointF(25, -50)
    }
            'Create a list of points for the annotation with arrow connector
            Dim points As List(Of PointF) = GetPointsForArrowCallout(arrowCallout.ContentCenter.Value.X,
                                                                     arrowCallout.ContentCenter.Value.Y,
                                                                     "Low")
            For Each p As PointF In points
                arrowCallout.Points.Add(p)
            Next
            'Stylise the annotation with arrow connector
            arrowCallout.ContentStyle.StrokeColor = Color.Black
            arrowCallout.Style.FillColor = Color.FromArgb(200, Color.Green)
            arrowCallout.Style.StrokeColor = Color.Green
            ...
    
    private void SetUpAnnotations()
    {            
        // Create an arrow callout annotation of polygon type  
        var arrowCallout = new Polygon("Low")
        {
            // Specified the Annotation coordinates by  Data Series Index and Data Point Index
            Attachment = AnnotationAttachment.DataIndex,
            SeriesIndex = 1,
            PointIndex = 0,
            ContentCenter = new PointF(25, -50)
        };
    
        // Create a list of points for arrow callout by calling GetPointsForArrowCallout()
        List<PointF> points = GetPointsForArrowCallout(arrowCallout.ContentCenter.Value.X,
            arrowCallout.ContentCenter.Value.Y, "Low");
        foreach (PointF p in points)
        {
            arrowCallout.Points.Add(p);
        }
    
        //Stylise the arrow callout annotation
        arrowCallout.ContentStyle.StrokeColor = Color.Black;
        arrowCallout.Style.FillColor = Color.FromArgb(200, Color.Green);
        arrowCallout.Style.StrokeColor = Color.Green;
        ...
    
  2. Define the GetPointsForArrowCallout() method to specify the points for arrow callout.
    1. To measure the size of content string in arrow callout, and reuse it to calculate and set the dimensions of arrow annotation, use the following code.
      Private Function GetPointsForArrowCallout(centerX As Single,
                                                centerY As Single,
                                                content As String)
          As List(Of PointF)
          ' Measure the size of the content string in arrow callout
          Dim size As _Size = _engine.MeasureString(content)
      
          ' Call method to calculate dimensions of the arrow annotation
          Return GetPointsForArrowCallout(centerX, centerY,
                                          CSng(size.Width) + 10, CSng(size.Height) + 10)
      End Function
      
      private List<PointF> GetPointsForArrowCallout
          (float centerX, float centerY, string content)
      {
          // Measure the size of the content string in arrow callout
          _Size size = _engine.MeasureString(content);
      
          // Call method to calculate dimensions of the arrow annotation
          return GetPointsForArrowCallout(centerX, centerY,
              (float)size.Width + 10, (float)size.Height + 10); 
      }
      
    2. To calculate the dimensions and points for arrow annotations, define the method overload GetPointsForArrowCallout() as shown below.
      Private Function GetPointsForArrowCallout(centerX As Single,
                                                centerY As Single,
                                                rectWidth As Single,
                                                rectHeight As Single)
          As List(Of PointF)
          Dim points = New List(Of PointF)()
      
          Dim rectLeft As Single = centerX - rectWidth / 2
          Dim rectRight As Single = centerX + rectWidth / 2
          Dim rectTop As Single = centerY - rectHeight / 2
          Dim rectBottom As Single = centerY + rectHeight / 2
      
          Dim angle As Single = CSng(Math.Atan2(-centerY, centerX))
          Dim angleOffset1 As Single = 0.4F
          Dim angleOffset2 As Single = 0.04F
          Dim arrowHeight As Single = 0.4F * rectHeight
          Dim hypotenuse As Single = CSng(arrowHeight / Math.Cos(angleOffset1))
          Dim subHypotenuse As Single = CSng(arrowHeight / Math.Cos(angleOffset2))
      
          Dim isNearBottom As Boolean = Math.Abs(rectTop) > Math.Abs(rectBottom)
          Dim nearHorizontalEdge As Single = If(isNearBottom, rectBottom, rectTop)
          Dim isNearRight As Boolean = Math.Abs(rectLeft) > Math.Abs(rectRight)
          Dim nearVerticalEdge As Single = If(isNearRight, rectRight, rectLeft)
          Dim isHorizontalCrossed As Boolean = Math.Abs(nearHorizontalEdge) > Math.Abs(nearVerticalEdge)
          Dim nearEdge As Single = If(isHorizontalCrossed, nearHorizontalEdge, nearVerticalEdge)
      
          Dim factor As Integer = If(nearEdge > 0, -1, 1)
          Dim crossedPointOffsetToCenter As Single = If(isHorizontalCrossed,
              CSng(rectHeight / (2 * Math.Tan(angle)) * factor),
              CSng(rectWidth * Math.Tan(angle) * factor / 2))
      
          ' Specify Arrow points
          points.Add(New PointF(0, 0))
          points.Add(New PointF(CSng(Math.Cos(angle + angleOffset1) * hypotenuse),
                                CSng(-Math.Sin(angle + angleOffset1) * hypotenuse)))
          points.Add(New PointF(CSng(Math.Cos(angle + angleOffset2) * subHypotenuse),
                                CSng(-Math.Sin(angle + angleOffset2) * subHypotenuse)))
      
          ' Specify Rectangle points
          If isHorizontalCrossed Then
              points.Add(New PointF(CSng(-nearEdge / Math.Tan(angle + angleOffset2)), CSng(nearEdge)))
              If isNearBottom Then
                  points.Add(New PointF(rectLeft, rectBottom))
                  points.Add(New PointF(rectLeft, rectTop))
                  points.Add(New PointF(rectRight, rectTop))
                  points.Add(New PointF(rectRight, rectBottom))
              Else
                  points.Add(New PointF(rectRight, rectTop))
                  points.Add(New PointF(rectRight, rectBottom))
                  points.Add(New PointF(rectLeft, rectBottom))
                  points.Add(New PointF(rectLeft, rectTop))
              End If
              points.Add(New PointF(CSng(-nearEdge / Math.Tan(angle - angleOffset2)), nearEdge))
          Else
              points.Add(New PointF(nearEdge, CSng(-nearEdge * Math.Tan(angle + angleOffset2))))
              If isNearRight Then
                  points.Add(New PointF(rectRight, rectBottom))
                  points.Add(New PointF(rectLeft, rectBottom))
                  points.Add(New PointF(rectLeft, rectTop))
                  points.Add(New PointF(rectRight, rectTop))
              Else
                  points.Add(New PointF(rectLeft, rectTop))
                  points.Add(New PointF(rectRight, rectTop))
                  points.Add(New PointF(rectRight, rectBottom))
                  points.Add(New PointF(rectLeft, rectBottom))
              End If
              points.Add(New PointF(nearEdge, CSng(-nearEdge * Math.Tan(angle - angleOffset2))))
          End If
      
          ' Arrow points
          points.Add(New PointF(CSng(Math.Cos(angle - angleOffset2) * subHypotenuse),
                                CSng(-Math.Sin(angle - angleOffset2) * subHypotenuse)))
          points.Add(New PointF(CSng(Math.Cos(angle - angleOffset1) * hypotenuse),
                                CSng(-Math.Sin(angle - angleOffset1) * hypotenuse)))
          Return points
      End Function
      
      Private Sub FlexChart1_Click(sender As Object, e As EventArgs) Handles FlexChart1.Click
      
      End Sub
      
      private List<PointF> GetPointsForArrowCallout
          (float centerX, float centerY, float rectWidth, float rectHeight)
      {
          var points = new List<PointF>();
      
          float rectLeft = centerX - rectWidth / 2;
          float rectRight = centerX + rectWidth / 2;
          float rectTop = centerY - rectHeight / 2;
          float rectBottom = centerY + rectHeight / 2;
      
          float angle = (float)(Math.Atan2(-centerY, centerX));
          float angleOffset1 = 0.4f;
          float angleOffset2 = 0.04f;
          float arrowHeight = 0.4f * rectHeight;
          float hypotenuse = (float)(arrowHeight / Math.Cos(angleOffset1));
          float subHypotenuse = (float)(arrowHeight / Math.Cos(angleOffset2));
      
          bool isNearBottom = Math.Abs(rectTop) > Math.Abs(rectBottom);
          float nearHorizontalEdge = isNearBottom ? rectBottom : rectTop;
          bool isNearRight = Math.Abs(rectLeft) > Math.Abs(rectRight);
          float nearVerticalEdge = isNearRight ? rectRight : rectLeft;
          bool isHorizontalCrossed = Math.Abs(nearHorizontalEdge) > Math.Abs(nearVerticalEdge);
          float nearEdge = isHorizontalCrossed ? nearHorizontalEdge : nearVerticalEdge;
      
          int factor = nearEdge > 0 ? -1 : 1;
          float crossedPointOffsetToCenter = isHorizontalCrossed ?
              (float)(rectHeight / (2 * Math.Tan(angle)) * factor)
              : (float)(rectWidth * Math.Tan(angle) * factor / 2);
      
          // Specify Arrow points
          points.Add(new PointF(0, 0));
          points.Add(new PointF((float)(Math.Cos(angle + angleOffset1)
              * hypotenuse), (float)(-Math.Sin(angle + angleOffset1) * hypotenuse)));
          points.Add(new PointF((float)(Math.Cos(angle + angleOffset2)
              * subHypotenuse), (float)(-Math.Sin(angle + angleOffset2) * subHypotenuse)));
      
          // Specify Rectangle points
          if (isHorizontalCrossed)
          {
              points.Add(new PointF((float)(-nearEdge / Math.Tan(angle + angleOffset2)),
                  (float)nearEdge));
              if (isNearBottom)
              {
                  points.Add(new PointF(rectLeft, rectBottom));
                  points.Add(new PointF(rectLeft, rectTop));
                  points.Add(new PointF(rectRight, rectTop));
                  points.Add(new PointF(rectRight, rectBottom));
              }
              else
              {
                  points.Add(new PointF(rectRight, rectTop));
                  points.Add(new PointF(rectRight, rectBottom));
                  points.Add(new PointF(rectLeft, rectBottom));
                  points.Add(new PointF(rectLeft, rectTop));
              }
              points.Add(new PointF((float)(-nearEdge / Math.Tan(angle - angleOffset2)),
                  nearEdge));
          }
          else
          {
              points.Add(new PointF(nearEdge, (float)(-nearEdge * Math.Tan(angle + angleOffset2))));
              if (isNearRight)
              {
                  points.Add(new PointF(rectRight, rectBottom));
                  points.Add(new PointF(rectLeft, rectBottom));
                  points.Add(new PointF(rectLeft, rectTop));
                  points.Add(new PointF(rectRight, rectTop));
              }
              else
              {
                  points.Add(new PointF(rectLeft, rectTop));
                  points.Add(new PointF(rectRight, rectTop));
                  points.Add(new PointF(rectRight, rectBottom));
                  points.Add(new PointF(rectLeft, rectBottom));
              }
              points.Add(new PointF(nearEdge, (float)(-nearEdge * Math.Tan(angle - angleOffset2))));
          }
      
          // Arrow points
          points.Add(new PointF((float)(Math.Cos(angle - angleOffset2) * subHypotenuse),
              (float)(-Math.Sin(angle - angleOffset2) * subHypotenuse)));
          points.Add(new PointF((float)(Math.Cos(angle - angleOffset1) * hypotenuse),
              (float)(-Math.Sin(angle - angleOffset1) * hypotenuse)));
          return points;
      }
      

Back to Top

Step 3: Render the annotations in chart

To Render the annotations in chart, follow these steps:

  1. Define global fields of AnnotationLayer class and render engine in Form class of your application.
    Dim annotationLayer As AnnotationLayer
    Dim _engine As IRenderEngine
    
    AnnotationLayer annotationLayer;
    IRenderEngine _engine;
    

  2. Create an instance of AnnotationLayer class, in SetUpAnnotations() method.
    ' Create an instance of AnnotationLayer class
    annotationLayer = New AnnotationLayer(FlexChart1)
    
    // Create an instance of AnnotationLayer class
    annotationLayer = new AnnotationLayer(flexChart1);
    

  3. Add the line and arrow callout annotations to the annotationLayer, by using the following code.
        'Add the polygon annotations with line and arrow connectors to the annotationLayer
        annotationLayer.Annotations.Add(arrowCallout)
        annotationLayer.Annotations.Add(lineCallout)
    End Sub
    
        //Add the line and arrow callout annotations to the annotationLayer
        annotationLayer.Annotations.Add(arrowCallout);
        annotationLayer.Annotations.Add(lineCallout);           
    }
    
  4. To render the callouts use the following code in the Rendered event of FlexChart.
    Private Sub FlexChart1_Rendered(sender As Object, e As C1.Win.Chart.RenderEventArgs)
        Handles FlexChart1.Rendered
        If _engine Is Nothing Then
            _engine = e.Engine
            SetUpAnnotations()
            FlexChart1.Invalidate()
        End If
    End Sub
    
    private void flexChart1_Rendered(object sender, C1.Win.Chart.RenderEventArgs e)
    {
        if (_engine == null)
        {
            _engine = e.Engine;
            SetUpAnnotations();
            flexChart1.Invalidate();
        }
    }
    

Back to Top