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.
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; ...
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; ...
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); }
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; }
To Render the annotations in chart, follow these steps:
Dim annotationLayer As AnnotationLayer Dim _engine As IRenderEngine
AnnotationLayer annotationLayer; IRenderEngine _engine;
' Create an instance of AnnotationLayer class annotationLayer = New AnnotationLayer(FlexChart1)
// Create an instance of AnnotationLayer class annotationLayer = new AnnotationLayer(flexChart1);
'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);
}
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(); } }