Search code examples
iosios-charts

iOS Charts: Showing actual value with percentage inside PieChart?


I'm currently using Charts - by Daniel Gindi, and following his demo, I've implemented a PieChart like so:

enter image description here

It was achieve using the following code:

func createPieChart(dataPoints: [String: Double])
{
    for index in 0..<sortedDates.count
    {
        let year = sortedDates[index]

        guard let costs = dataPoints[year] else {
            return
        }

        let dataEntry = PieChartDataEntry(value: costs, label: year)

        dataEntries.append(dataEntry)
    }

    chartDataSet = PieChartDataSet(values: dataEntries, label: "")

    chartData = PieChartData(dataSet: chartDataSet)

    chartDataSet.sliceSpace = 2.0

    chartDataSet.yValuePosition = PieChartDataSet.ValuePosition.outsideSlice

    ...

    pieChartView.usePercentValuesEnabled = true
    pieChartView.drawSlicesUnderHoleEnabled = false
    pieChartView.holeRadiusPercent = 0.40
    pieChartView.transparentCircleRadiusPercent = 0.43
    pieChartView.drawHoleEnabled = true
    pieChartView.rotationAngle = 0.0
    pieChartView.rotationEnabled = true
    pieChartView.highlightPerTapEnabled = false

    let pieChartLegend = pieChartView.legend
    pieChartLegend.horizontalAlignment = Legend.HorizontalAlignment.right
    pieChartLegend.verticalAlignment = Legend.VerticalAlignment.top
    pieChartLegend.orientation = Legend.Orientation.vertical
    pieChartLegend.drawInside = false
    pieChartLegend.yOffset = 10.0

    pieChartView.legend.enabled = true

    pieChartView.data = chartData
}

What I'm trying to achieve is to show the actual costs value inside the pie slice, but also include the percentage outside the slice. The percentage is shown using chartDataSet.yValuePosition = PieChartDataSet.ValuePosition.outsideSlice.

Inspecting the code, I noticed that in PieChartRenderer, there is there function open override func drawValues(context: CGContext) and within it, the code calculates the percentage of each pie slice in valueText:

let valueText = formatter.stringForValue(
                    value,
                    entry: e,
                    dataSetIndex: i,
                    viewPortHandler: viewPortHandler)

Then using an if check, it draws the percentage inside or outside the slice, which in my case is outside:

else if drawYOutside
{
    ChartUtils.drawText(
                        context: context,
                        text: valueText,
                        point: CGPoint(x: 0, y: labelPoint.y + lineHeight / 2.0),
                        align: align,
                        attributes: [NSFontAttributeName: valueFont, NSForegroundColorAttributeName: valueTextColor]
                        )
}

However, I can't find a way to actual include the costs value that is passed into PieChartDataEntry(value: costs, label: year).

I was told to subclass the Pie Chart and override the methods I found but I'm not exactly sure how to do that.

For example, the value of costs at each index in dataPoints is a double value of 100.0 for the label 2017 and 50.0 for the label 2016. I would like to include both those values in the associated pie slice.

Can anyone experienced with this library assist me?

Thanks!


Solution

  • Create a class that inherits from the PieChartRenderer and override the drawValues from the parent class like so:

    public class CustomClass: PieChartRenderer
    {
        override public func drawValues(context: CGContext)
        {
            // In this body, copy ALL code from parent class drawValues function
    
            ...
            let decimalFormatter = NumberFormatter()
            decimalFormatter.minimumFractionDigits = 2
            decimalFormatter.maximumFractionDigits = 2
            decimalFormatter.positivePrefix = "$"
    
            ...
    
            for i in 0 ..< dataSets.count
            {
                ...
    
                for j in 0 ..< dataSet.entryCount
                {
                    ...
    
                    guard let e = dataSet.entryForIndex(j) else { continue }
                    let pe = e as? PieChartDataEntry
    
                    // e.y = the actual value in the costs array, not the percentage
                    let twoDecimalValue = NSDecimalNumber(value: e.y)
                    let totalCost = decimalFormatter.string(from: twoDecimalValue)
    
                    ...
    
                    if drawXInside || drawYInside
                    {
                        ...
    
                        if drawXInside && drawYInside
                        {
                            ...
                        }
                        else if drawXInside
                        {
                            if j < data.entryCount && pe?.label != nil
                            {
                                ChartUtils.drawText(
                                    context: context,
                                    text: pe!.label!,
                                    point: CGPoint(x: x,
                                                   y: y),
                                    align: .center,
                                    attributes: [NSFontAttributeName: yearValueFont,
                                                 NSForegroundColorAttributeName: yearTextColor ?? percentageTextColor]
                                )
    
                                ChartUtils.drawText(
                                    context: context,
                                    text: totalCost!,
                                    point: CGPoint(x: x,
                                                   y: y + lineHeight),
                                    align: .center,
                                    attributes: [NSFontAttributeName: yearCostsValueFont,
                                                 NSForegroundColorAttributeName: yearCostsTextColor]
                                )
                            }
                        }
                    }
                }
            }
        }
    }
    

    Within the else if drawXInside, the first existing drawText function draws the label value, in my case, the year. Add another drawText function and pass in the totalCost value, which represents the actual value in the array `costs.

    Then within the createPieChart function:

    func createPieChart(dataPoints: [String: Double])
    {
        ...
    
        pieChartView.data = chartData
    
        let customClass = CustomClass(chart: pieChartView, 
                                      animator: pieChartView.chartAnimator,
                                      viewPortHandler: pieChartView.viewPortHandler)
        pieChartView.renderer = customRenderer   
    }
    

    The result:

    enter image description here