0

Using PieSeries Oxyplot, is it possible to either change the label orientation of a pie chart, or to put the label in front of a slice, so that the label is not hidden when a slice is too small? For example on this pie chart, it is not possible to see the 5% label:

enter image description here

This could either be fixed by changing the orientation of the label text, but more ideal is to put the text in front of the slice in such manner that the text is always visible, no matter the size of the slice in the Pie Chart.

Code:

public static string Draw()
{
    var chartModel = new PlotModel
    {
        TitleFontSize = 5,         
        DefaultFont = "Arial",
        DefaultFontSize = 8,
        IsLegendVisible = true,
        PlotType = PlotType.Polar,
    };    
            
    PieSeries seriesP1 = new PieSeries
    {
        StrokeThickness = 2.0,
        InsideLabelPosition = 0.5,
        AngleSpan = 360,
        StartAngle = -90,
        AngleIncrement = 1.0,
        Background = OxyColor.FromRgb(255, 255, 255),
        EdgeRenderingMode = EdgeRenderingMode.Automatic,
        FontSize = 8,
        TickHorizontalLength = 0,
        TickRadialLength = 0,
        OutsideLabelFormat = "",
        InsideLabelFormat = "{2:0}%" /*disables labels inside a pie chart*/
    };

    seriesP1.Slices.Add(new PieSlice("Text1", 5) { IsExploded = false, Fill = OxyColors.DarkSeaGreen });

    seriesP1.Slices.Add(new PieSlice("Text2", 55) { IsExploded = false, Fill = OxyColors.Green });

    seriesP1.Slices.Add(new PieSlice("Text3", 20) { IsExploded = false, Fill = OxyColors.DarkGreen });

    seriesP1.Slices.Add(new PieSlice("Text4", 20) { Fill = OxyColors.LightGray });

    chartModel.Series.Add(seriesP1);

    var exporter = new SvgExporter
    {
        Width = 130,
        Height = 130,
        IsDocument = false/*  creates <svg> tag*/
    };

    return exporter.ExportToString(chartModel).Substring(38);
}

UPDATE:

We found out that it is possible to change the orientation of the text by adding the "AreInsideLabelsAngled = true," to the PieSeries object, but since the label text is still hidden in the slice, we get this result:

enter image description here

Text is still not visible, and therefore the conclusion is that we need a property to set the label text in FRONT of the slice. Is this possible? We are aware that we can set the label outside of the chart, but that is not desired

Buster3650
  • 470
  • 2
  • 8
  • 19

1 Answers1

1

The 5% text is actually rendered completely but then covered by the other sliced that are subsequently rendered on top of it. That can easily be checked by changing the fill property of the slices to semi-transparent colors.

The problem is that the Render method of the PieSeries draws all slices and their labels in one loop. The issue can be fixed (with a small performance loss) by doing the rendering in two subsequent loops. This can either be done by checking out the source code of OxyPlot and modifying the Render method or - if you don't want to do that - by deriving your own FixedPieSeries and overriding it's Render method like this:

using System;
using System.Collections.Generic;
using System.Linq;

namespace OxyPlot.Series
{
    public class FixedPieSeries : PieSeries
    {
        /// <summary>
        /// The actual points of the slices.
        /// </summary>
        private readonly List<IList<ScreenPoint>> slicePoints = new List<IList<ScreenPoint>>();

        /// <summary>
        /// The total value of all the pie slices.
        /// </summary>
        private double total;


        /// <summary>
        /// Gets the point on the series that is nearest the specified point.
        /// </summary>
        /// <param name="point">The point.</param>
        /// <param name="interpolate">Interpolate the series if this flag is set to <c>true</c> .</param>
        /// <returns>A TrackerHitResult for the current hit.</returns>
        public override TrackerHitResult GetNearestPoint(ScreenPoint point, bool interpolate)
        {
            for (int i = 0; i < this.slicePoints.Count; i++)
            {
                if (ScreenPointHelper.IsPointInPolygon(point, this.slicePoints[i]))
                {
                    var slice = this.Slices[i];
                    var item = this.GetItem(i);
                    return new TrackerHitResult
                    {
                        Series = this,
                        Position = point,
                        Item = item,
                        Index = i,
                        Text = StringHelper.Format(this.ActualCulture, this.TrackerFormatString, slice, this.Title, slice.Label, slice.Value, slice.Value / this.total)
                    };
                }
            }

            return null;
        }

        /// <summary>
        /// Renders the series on the specified render context.
        /// </summary>
        /// <param name="rc">The rendering context.</param>
        public override void Render(IRenderContext rc)
        {
            this.slicePoints.Clear();

            if (this.Slices.Count == 0)
            {
                return;
            }

            this.total = this.Slices.Sum(slice => slice.Value);
            if (Math.Abs(this.total) < double.Epsilon)
            {
                return;
            }

            double radius = Math.Min(this.PlotModel.PlotArea.Width, this.PlotModel.PlotArea.Height) / 2;

            double outerRadius = radius * (this.Diameter - this.ExplodedDistance);
            double innerRadius = radius * this.InnerDiameter;

            double angle = this.StartAngle;
            var midPoint = new ScreenPoint(
                (this.PlotModel.PlotArea.Left + this.PlotModel.PlotArea.Right) * 0.5, (this.PlotModel.PlotArea.Top + this.PlotModel.PlotArea.Bottom) * 0.5);

            foreach (var slice in this.Slices)
            {
                var outerPoints = new List<ScreenPoint>();
                var innerPoints = new List<ScreenPoint>();

                double sliceAngle = slice.Value / this.total * this.AngleSpan;
                double endAngle = angle + sliceAngle;
                double explodedRadius = slice.IsExploded ? this.ExplodedDistance * radius : 0.0;

                double midAngle = angle + (sliceAngle / 2);
                double midAngleRadians = midAngle * Math.PI / 180;
                var mp = new ScreenPoint(
                    midPoint.X + (explodedRadius * Math.Cos(midAngleRadians)),
                    midPoint.Y + (explodedRadius * Math.Sin(midAngleRadians)));

                // Create the pie sector points for both outside and inside arcs
                while (true)
                {
                    bool stop = false;
                    if (angle >= endAngle)
                    {
                        angle = endAngle;
                        stop = true;
                    }

                    double a = angle * Math.PI / 180;
                    var op = new ScreenPoint(mp.X + (outerRadius * Math.Cos(a)), mp.Y + (outerRadius * Math.Sin(a)));
                    outerPoints.Add(op);
                    var ip = new ScreenPoint(mp.X + (innerRadius * Math.Cos(a)), mp.Y + (innerRadius * Math.Sin(a)));
                    if (innerRadius + explodedRadius > 0)
                    {
                        innerPoints.Add(ip);
                    }

                    if (stop)
                    {
                        break;
                    }

                    angle += this.AngleIncrement;
                }

                innerPoints.Reverse();
                if (innerPoints.Count == 0)
                {
                    innerPoints.Add(mp);
                }

                innerPoints.Add(outerPoints[0]);

                var points = outerPoints;
                points.AddRange(innerPoints);

                rc.DrawPolygon(points, slice.ActualFillColor, this.Stroke, this.StrokeThickness, this.EdgeRenderingMode, null, LineJoin.Bevel);

                // keep the point for hit testing
                this.slicePoints.Add(points);

                // Render label outside the slice
                if (this.OutsideLabelFormat != null)
                {
                    string label = string.Format(
                        this.OutsideLabelFormat, slice.Value, slice.Label, slice.Value / this.total * 100);
                    int sign = Math.Sign(Math.Cos(midAngleRadians));

                    // tick points
                    var tp0 = new ScreenPoint(
                        mp.X + ((outerRadius + this.TickDistance) * Math.Cos(midAngleRadians)),
                        mp.Y + ((outerRadius + this.TickDistance) * Math.Sin(midAngleRadians)));
                    var tp1 = new ScreenPoint(
                        tp0.X + (this.TickRadialLength * Math.Cos(midAngleRadians)),
                        tp0.Y + (this.TickRadialLength * Math.Sin(midAngleRadians)));
                    var tp2 = new ScreenPoint(tp1.X + (this.TickHorizontalLength * sign), tp1.Y);

                    // draw the tick line with the same color as the text
                    rc.DrawLine(new[] { tp0, tp1, tp2 }, this.ActualTextColor, 1, this.EdgeRenderingMode, null, LineJoin.Bevel);

                    // label
                    var labelPosition = new ScreenPoint(tp2.X + (this.TickLabelDistance * sign), tp2.Y);
                    rc.DrawText(
                        labelPosition,
                        label,
                        this.ActualTextColor,
                        this.ActualFont,
                        this.ActualFontSize,
                        this.ActualFontWeight,
                        0,
                        sign > 0 ? HorizontalAlignment.Left : HorizontalAlignment.Right,
                        VerticalAlignment.Middle);
                }
            }

            angle = this.StartAngle;

            foreach (var slice in this.Slices)
            {
                double sliceAngle = slice.Value / this.total * this.AngleSpan;
                double endAngle = angle + sliceAngle;
                double explodedRadius = slice.IsExploded ? this.ExplodedDistance * radius : 0.0;

                double midAngle = angle + (sliceAngle / 2);
                double midAngleRadians = midAngle * Math.PI / 180;
                var mp = new ScreenPoint(
                    midPoint.X + (explodedRadius * Math.Cos(midAngleRadians)),
                    midPoint.Y + (explodedRadius * Math.Sin(midAngleRadians)));

                // Create the pie sector points for both outside and inside arcs
                while (true)
                {
                    bool stop = false;
                    if (angle >= endAngle)
                    {
                        angle = endAngle;
                        stop = true;
                    }

                    if (stop)
                    {
                        break;
                    }

                    angle += this.AngleIncrement;
                }

                // Render a label inside the slice
                if (this.InsideLabelFormat != null && !this.InsideLabelColor.IsUndefined())
                {
                    string label = string.Format(
                        this.InsideLabelFormat, slice.Value, slice.Label, slice.Value / this.total * 100);
                    double r = (innerRadius * (1 - this.InsideLabelPosition)) + (outerRadius * this.InsideLabelPosition);
                    var labelPosition = new ScreenPoint(
                        mp.X + (r * Math.Cos(midAngleRadians)), mp.Y + (r * Math.Sin(midAngleRadians)));
                    double textAngle = 0;
                    if (this.AreInsideLabelsAngled)
                    {
                        textAngle = midAngle;
                        if (Math.Cos(midAngleRadians) < 0)
                        {
                            textAngle += 180;
                        }
                    }

                    var actualInsideLabelColor = this.InsideLabelColor.IsAutomatic() ? this.ActualTextColor : this.InsideLabelColor;

                    rc.DrawText(
                        labelPosition,
                        label,
                        actualInsideLabelColor,
                        this.ActualFont,
                        this.ActualFontSize,
                        this.ActualFontWeight,
                        textAngle,
                        HorizontalAlignment.Center,
                        VerticalAlignment.Middle);
                }
            }
        }
    }
}

Note that I have also overridden the GetNearestPoint method to keep the existing interactive behavior if required.

Döharrrck
  • 687
  • 1
  • 4
  • 15