ComponentOne FlexChart for UWP
FlexChart / Working with FlexChart / FlexChart Elements / Annotations / Creating Callouts
In This Topic
    Creating Callouts
    In This Topic

    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.

        ...
        lineCallouts.SeriesIndex = 2
        lineCallouts.PointIndex = 2
        lineCallouts.ContentCenter = New Point(-50, 75)
        Dim lineCalloutsPoints As PointCollection = New PointCollection()
        lineCalloutsPoints.Add(New Point(0, 0))
        lineCalloutsPoints.Add(New Point(25, -25))
        lineCalloutsPoints.Add(New Point(50, -25))
        lineCalloutsPoints.Add(New Point(50, -50))
        lineCalloutsPoints.Add(New Point(25, -75))
        lineCalloutsPoints.Add(New Point(0, -50))
        lineCalloutsPoints.Add(New Point(0, -25))
        lineCalloutsPoints.Add(New Point(25, -25))
        lineCalloutsPoints.Add(New Point(0, 0))
        lineCallouts.Points = lineCalloutsPoints
        flexChart.Invalidate()
    End Sub
    
        ...
        // Create a line callout annotation of polygon type
        lineCallout.SeriesIndex = 2;
        lineCallout.PointIndex = 2;
        lineCallout.ContentCenter = new Point(25, -40);
        // Create a list of points for the line callout annotation 
        var lineConnectorPoints = new PointCollection();
        lineConnectorPoints.Add(new Point(0, 0));
        lineConnectorPoints.Add(new Point(25, -25));
        lineConnectorPoints.Add(new Point(50, -25));
        lineConnectorPoints.Add(new Point(50, -50));
        lineConnectorPoints.Add(new Point(25, -75));
        lineConnectorPoints.Add(new Point(0, -50));
        lineConnectorPoints.Add(new Point(0, -25));
        lineConnectorPoints.Add(new Point(25, -25));
        lineConnectorPoints.Add(new Point(0, 0));
        lineCallout.Points = lineConnectorPoints;
        flexChart.Invalidate();
    }
    

    Back to Top

    Step 2: Create arrow annotation callout

    1. To create an arrow callout use the following code.
      Private Sub SetUpAttotations()
          'Create an arrow callout annotation of polygon type
          Dim arrowCalloutContentCenter As Point = New Point(25, -50)
          arrowCallouts.ContentCenter = arrowCalloutContentCenter
      
          'Create list of points for arrow callout by calling GetPointsForArrowCallout()
          arrowCallouts.Points = GetPointsForArrowCallout(arrowCalloutContentCenter.X, arrowCalloutContentCenter.Y, "Low")
          arrowCallouts.SeriesIndex = 1
          arrowCallouts.PointIndex = 2
          ...
      
      private void SetUpAnnotations()
      {
          
          //Create an arrow callout annotation of polygon type
          var arrowCalloutContentCenter = new Point(25, -50);
          arrowCallout.ContentCenter = arrowCalloutContentCenter;
      
          //Create list of points for arrow callout by calling GetPointsForArrowCallout()
          arrowCallout.Points = GetPointsForArrowCallout(arrowCalloutContentCenter.X,
              arrowCalloutContentCenter.Y, "Low");            
          arrowCallout.SeriesIndex = 1;
          arrowCallout.PointIndex = 2;
          ...
      
    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 Double, centerY As Double, content As String) As PointCollection
            Dim size As _Size = _engine.MeasureString(content)
            Return GetPointsForArrowCallout(centerX, centerY, size.Width + 10, size.Height + 10)
        End Function
        
        PointCollection GetPointsForArrowCallout(double centerX, double centerY, string content)
        {
            _Size size = _engine.MeasureString(content);
            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 Double, centerY As Double, rectWidth As Double, rectHeight As Double) As PointCollection
            Dim points As PointCollection = New PointCollection()
        
            Dim rectLeft As Double = centerX - rectWidth / 2
            Dim rectRight As Double = centerX + rectWidth / 2
            Dim rectTop As Double = centerY - rectHeight / 2
            Dim rectBottom As Double = centerY + rectHeight / 2
        
            Dim angle As Double = Math.Atan2(-centerY, centerX)
            Dim angleOffset1 As Double = 0.4
            Dim angleOffset2 As Double = 0.04
            Dim arrowHeight As Double = 0.4 * rectHeight
            Dim hypotenuse As Double = arrowHeight / Math.Cos(angleOffset1)
            Dim subHypotenuse As Double = arrowHeight / Math.Cos(angleOffset2)
            Dim isNearBottom As Boolean = Math.Abs(rectTop) > Math.Abs(rectBottom)
        
            Dim nearHorizontalEdge As Double
            If (isNearBottom) Then
                nearHorizontalEdge = rectBottom
            Else
                nearHorizontalEdge = rectTop
            End If
        
            Dim isNearRight As Boolean = Math.Abs(rectLeft) > Math.Abs(rectRight)
        
            Dim nearVerticalEdge As Double
            If (isNearRight) Then
                nearVerticalEdge = rectRight
            Else
                nearVerticalEdge = rectLeft
            End If
        
            Dim isHorizontalCrossed As Boolean = Math.Abs(nearHorizontalEdge) > Math.Abs(nearVerticalEdge)
            Dim nearEdge As Double
            If (isHorizontalCrossed) Then
                nearEdge = nearHorizontalEdge
            Else
                nearEdge = nearVerticalEdge
            End If
        
            Dim factor As Int16
            If (nearEdge > 0) Then
                factor = -1
            Else
                factor = 1
            End If
        
            Dim crossedPointOffsetToCenter As Double
            If (isHorizontalCrossed) Then
                crossedPointOffsetToCenter = rectHeight / (2 * Math.Tan(angle)) * factor
            Else
                crossedPointOffsetToCenter = rectWidth * Math.Tan(angle) * factor / 2
            End If
        
            'Arrow points
            points.Add(New Point(0, 0))
            points.Add(New Point(Math.Cos(angle + angleOffset1) * hypotenuse, -Math.Sin(angle + angleOffset1) * hypotenuse))
            points.Add(New Point(Math.Cos(angle + angleOffset2) * subHypotenuse, -Math.Sin(angle + angleOffset2) * subHypotenuse))
        
            'Rectangle points
            If (isHorizontalCrossed) Then
                points.Add(New Point(-nearEdge / Math.Tan(angle + angleOffset2), nearEdge))
                If (isNearBottom) Then
                    points.Add(New Point(rectLeft, rectBottom))
                    points.Add(New Point(rectLeft, rectTop))
                    points.Add(New Point(rectRight, rectTop))
                    points.Add(New Point(rectRight, rectBottom))
                Else
                    points.Add(New Point(rectRight, rectTop))
                    points.Add(New Point(rectRight, rectBottom))
                    points.Add(New Point(rectLeft, rectBottom))
                    points.Add(New Point(rectLeft, rectTop))
                End If
        
                points.Add(New Point(-nearEdge / Math.Tan(angle - angleOffset2), nearEdge))
            Else
                points.Add(New Point(nearEdge, -nearEdge * Math.Tan(angle + angleOffset2)))
                If (isNearRight) Then
                    points.Add(New Point(rectRight, rectBottom))
                    points.Add(New Point(rectLeft, rectBottom))
                    points.Add(New Point(rectLeft, rectTop))
                    points.Add(New Point(rectRight, rectTop))
                Else
                    points.Add(New Point(rectLeft, rectTop))
                    points.Add(New Point(rectRight, rectTop))
                    points.Add(New Point(rectRight, rectBottom))
                    points.Add(New Point(rectLeft, rectBottom))
                End If
        
                points.Add(New Point(nearEdge, -nearEdge * Math.Tan(angle - angleOffset2)))
            End If
        
            'Arrow points
            points.Add(New Point(Math.Cos(angle - angleOffset2) * subHypotenuse, -Math.Sin(angle - angleOffset2) * subHypotenuse))
            points.Add(New Point(Math.Cos(angle - angleOffset1) * hypotenuse, -Math.Sin(angle - angleOffset1) * hypotenuse))
            Return points
        End Function
        
        PointCollection GetPointsForArrowCallout(double centerX, double centerY, double rectWidth, double rectHeight)
        {
            var points = new PointCollection();
        
            double rectLeft = centerX - rectWidth / 2;
            double rectRight = centerX + rectWidth / 2;
            double rectTop = centerY - rectHeight / 2;
            double rectBottom = centerY + rectHeight / 2;
        
            double angle = Math.Atan2(-centerY, centerX);
            double angleOffset1 = 0.4;
            double angleOffset2 = 0.04;
            double arrowHeight = 0.4 * rectHeight;
            double hypotenuse = arrowHeight / Math.Cos(angleOffset1);
            double subHypotenuse = arrowHeight / Math.Cos(angleOffset2);
        
            bool isNearBottom = Math.Abs(rectTop) > Math.Abs(rectBottom);
            double nearHorizontalEdge = isNearBottom ? rectBottom : rectTop;
            bool isNearRight = Math.Abs(rectLeft) > Math.Abs(rectRight);
            double nearVerticalEdge = isNearRight ? rectRight : rectLeft;
            bool isHorizontalCrossed = Math.Abs(nearHorizontalEdge) > Math.Abs(nearVerticalEdge);
            double nearEdge = isHorizontalCrossed ? nearHorizontalEdge : nearVerticalEdge;
        
            int factor = nearEdge > 0 ? -1 : 1;
            double crossedPointOffsetToCenter = isHorizontalCrossed ?
                rectHeight / (2 * Math.Tan(angle)) * factor : rectWidth * Math.Tan(angle) * factor / 2;
        
            // Arrow points
            points.Add(new Point(0, 0));
            points.Add(new Point(Math.Cos(angle + angleOffset1) * hypotenuse, -Math.Sin(angle + angleOffset1) * hypotenuse));
            points.Add(new Point(Math.Cos(angle + angleOffset2) * subHypotenuse, -Math.Sin(angle + angleOffset2) * subHypotenuse));
        
            // Rectangle points
            if (isHorizontalCrossed)
            {
                points.Add(new Point(-nearEdge / Math.Tan(angle + angleOffset2), nearEdge));
                if (isNearBottom)
                {
                    points.Add(new Point(rectLeft, rectBottom));
                    points.Add(new Point(rectLeft, rectTop));
                    points.Add(new Point(rectRight, rectTop));
                    points.Add(new Point(rectRight, rectBottom));
                }
                else
                {
                    points.Add(new Point(rectRight, rectTop));
                    points.Add(new Point(rectRight, rectBottom));
                    points.Add(new Point(rectLeft, rectBottom));
                    points.Add(new Point(rectLeft, rectTop));
                }
                points.Add(new Point(-nearEdge / Math.Tan(angle - angleOffset2), nearEdge));
            }
            else
            {
                points.Add(new Point(nearEdge, -nearEdge * Math.Tan(angle + angleOffset2)));
                if (isNearRight)
                {
                    points.Add(new Point(rectRight, rectBottom));
                    points.Add(new Point(rectLeft, rectBottom));
                    points.Add(new Point(rectLeft, rectTop));
                    points.Add(new Point(rectRight, rectTop));
                }
                else
                {
                    points.Add(new Point(rectLeft, rectTop));
                    points.Add(new Point(rectRight, rectTop));
                    points.Add(new Point(rectRight, rectBottom));
                    points.Add(new Point(rectLeft, rectBottom));
                }
                points.Add(new Point(nearEdge, -nearEdge * Math.Tan(angle - angleOffset2)));
            }
        
            // Arrow points
            points.Add(new Point(Math.Cos(angle - angleOffset2) * subHypotenuse, -Math.Sin(angle - angleOffset2) * subHypotenuse));
            points.Add(new Point(Math.Cos(angle - angleOffset1) * hypotenuse, -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 field of render engine.
      Dim _engine As IRenderEngine
      
      IRenderEngine _engine;
      

    2. To create an instance of AnnotationLayer and add the annotation callouts in it, use the following code.
          <Chart:C1FlexChart.Layers>
          <Annotation:AnnotationLayer>
              <Annotation:AnnotationLayer.Annotations>
                  <Annotation:Polygon x:Name="arrowCallout" Content="Low" 
                                      SeriesIndex="0" PointIndex="1" Attachment="DataIndex">
                      <Annotation:Polygon.Style>
                          <Chart:ChartStyle Fill="#C800FF00" Stroke="Green"/>
                      </Annotation:Polygon.Style>
                  </Annotation:Polygon>
                  
                  <Annotation:Polygon x:Name="lineCallout" Content="High"  
                                      SeriesIndex="0" PointIndex="4" Attachment="DataIndex">
                      <Annotation:Polygon.Style>
                          <Chart:ChartStyle Fill="#C8FF0000" Stroke="Red" />
                      </Annotation:Polygon.Style>
                  </Annotation:Polygon>
              </Annotation:AnnotationLayer.Annotations>
          </Annotation:AnnotationLayer>
      </Chart:C1FlexChart.Layers>
      
    3. To render the callouts use the following code in the Rendered event of chart.
      Private Sub flexChart_Rendered(sender As Object, e As RenderEventArgs)
          If (_engine Is Nothing) Then
              _engine = e.Engine
              SetUpAttotations()
          End If
      End Sub
      
      private void flexChart_Rendered(object sender, C1.Xaml.Chart.RenderEventArgs e)
      {
          if (_engine == null)
          {
              _engine = e.Engine;
              SetUpAnnotations();
          }
      }
      

    Back to Top