Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ios-charts How to invalidate/redraw after setting data

See Updates At Bottom (4/30/2015)

I'm implementing a Pie Chart in Swift for iOS using ios-charts, and have chosen to customize the legend. Of note, the chart is displayed within a cell of a UICollectionView. The problem is that on first display, the custom legend content is not being displayed. Instead, I get legend content generated from the data.

If I scroll the view off-screen, and then scroll it back onto the screen, the proper custom legend is displayed. So, I'm guessing that I need to force a redraw/relayout/re-something after setting my custom legend. I haven't figured out how to do that. Does anyone have an idea? Am I completely missing something? Thanks!

Chart on initial display - data-generated (wrong) legend Chart on initial display - generated legend

Chart after scrolling off and back onto the screen - (proper legend) Chart with proper legend

Here's my code for drawing this chart:

func initChart(pieChart: PieChartView) {
    numFormatter.maximumFractionDigits = 0

    pieChart.backgroundColor = UIColor.whiteColor()

    pieChart.usePercentValuesEnabled = false
    pieChart.drawHoleEnabled = true
    pieChart.holeTransparent = true
    pieChart.descriptionText = ""
    pieChart.centerText = "30%\nComplete"

    pieChart.data = getMyData()

    // Setting custom legend info, called AFTER setting data
    pieChart.legend.position = ChartLegend.ChartLegendPosition.LeftOfChartCenter
    pieChart.legend.colors = [clrGreenDk, clrGold, clrBlue]
    pieChart.legend.labels = ["Complete","Enrolled","Future"]
    pieChart.legend.enabled = true
}

func getMyData() -> ChartData {

    var xVals = ["Q201","R202","S203","T204","U205", "V206"]

    var courses: [ChartDataEntry] = []
    courses.append(ChartDataEntry(value: 3, xIndex: 0))
    courses.append(ChartDataEntry(value: 3, xIndex: 1))
    courses.append(ChartDataEntry(value: 4, xIndex: 2))
    courses.append(ChartDataEntry(value: 4, xIndex: 3))
    courses.append(ChartDataEntry(value: 3, xIndex: 4))
    courses.append(ChartDataEntry(value: 3, xIndex: 5))

    let dsColors = [clrGreenDk, clrGreenDk, clrBlue, clrBlue, clrGold, clrGold]

    let pcds = PieChartDataSet(yVals: courses, label: "")
    pcds.sliceSpace = CGFloat(4)
    pcds.colors = dsColors
    pcds.valueFont = labelFont!
    pcds.valueFormatter = numFormatter
    pcds.valueTextColor = UIColor.whiteColor()

    return ChartData(xVals: xVals, dataSet: pcds)
}

Update 4/30/2015

Based on discussion with author of MPAndroidChart (on which ios-charts is based), it appears there is not a point in the chart display lifecycle where one can override the legend on "first draw". Basically, the chart is rendered when it is created, no matter what. If you set data on the chart, the chart uses that data to create the legend and then renders. It isn't possible to change the legend between the point of setting data, and the point of chart rendering.

setNeedsDisplay()

Potentially, one can wait for the chart to render, update the legend, and then call chart.setNeedsDisplay() to signal the chart to redraw. Sadly, there's a timing problem with this. If you call this method immediately after rendering the chart, it either doesn't fire or (more likely) it fires too soon and is effectively ignored. In my code, placing this call within viewDidLoad or viewDidAppear had no effect. However...

Building the same chart in Java for Android (using MPAndroidChart) results in the same issue. After messing around for a bit, I noted that if I called the chart.invalidate() after a delay (using Handler.postDelayed()), it would fire properly. It turns out a similar approach works for ios-charts on iOS.

If one uses GCD to delay the call to setNeedsDisplay(), for even a few milliseconds after the rendering, it seems to do the trick. I've added the following code immediately after initializing the chart's view in my ViewController ("cell" is the UICollectionViewCell containing the chart view):

delay(0.05) {
    cell.pieChartView.legend.colors = [self.clrGreenDk, self.clrGold, self.clrBlue]
    cell.pieChartView.legend.labels = ["Complete","Enrolled","Future"]
    // Re-calc legend dimensions for proper position (Added 5/2/2015)
    cell.pieChartView.legend.calculateDimensions(cell.pieChartView.labelFont!)
    cell.pieChartView.setNeedsDisplay()
}

Using the awesome "delay" method from this SO post: https://stackoverflow.com/a/24318861

Obviously, this is a nasty hack, but it seems to do the trick. I'm not sure I like the idea of using this hack in production, though.

For any Android folk who stumble on this post:

The following code achieves the same effect using MPAndroidChart:

    // Inside onCreate()
    pie = (PieChart) findViewById(R.id.chart1);
    configPieChart(pie);

    new Handler().postDelayed(new Runnable() {
        @Override
        public void run() {
            String[] legLabels = new String[]{"Complete","Enrolled","Future"};
            ArrayList<Integer> legColors = new ArrayList<Integer>();
            legColors.add(blue);
            legColors.add(gold);
            legColors.add(green);

            pie.getLegend().setPosition(Legend.LegendPosition.LEFT_OF_CHART_CENTER);
            pie.getLegend().setColors(legColors);
            pie.getLegend().setLabels(legLabels);

            pie.invalidate();
        }
    }, 20);
like image 659
MojoTosh Avatar asked Apr 30 '15 00:04

MojoTosh


1 Answers

I am the author of ios-charts, and we're working on features for customizing the legend data, without those "hacks".

In the latest commits to ios-charts, you can already see extraLabels and extraColors properties that add extra lines to the legend, and a setLegend function that allows you to set a custom data entirely.

This will soon be added to the Android version as well.

Enjoy :-)

like image 118
daniel.gindi Avatar answered Nov 13 '22 00:11

daniel.gindi