ChartsOrg / Charts

Beautiful charts for iOS/tvOS/OSX! The Apple side of the crossplatform MPAndroidChart.
Apache License 2.0
27.6k stars 6k forks source link

Horizontal Bar Bounds #2515

Open AaronVinh opened 7 years ago

AaronVinh commented 7 years ago

I believe that the getBarBounds function is bugged. I am trying to draw a gradient layer over each bar by grabbing the bounds for each bar. Here is my code `
var chartEntries = [BarChartDataEntry]()

    var times = [50,120,150,250,250,350,430,500]
    let shows = ["The Wire \n \n  ","Game of Thrones \n \n  ","The Sopranos \n \n  ","Breaking Bad \n \n  ","Billions \n \n  ","House of Cards \n \n  ","Attack on Titan \n \n  ", "Mad Men \n \n  "]

    for i in 0 ..< times.count  {
        let chartEntry = BarChartDataEntry(x:Double(i), y: Double(times[i]))
        chartEntries.append(chartEntry)
        //categories[i] = shows[i] + "\n" + String(times[i])
    }

    let chartDataSet = BarChartDataSet(values: chartEntries, label: "Categories")

    let chartData = BarChartData(dataSet: chartDataSet)

    chartData.barWidth = 0.2
    barChart.leftAxis.axisMinimum = 0
    barChart.data = chartData
    barChart.chartDescription?.text = ""
    barChart.legend.enabled = false
    barChart.xAxis.labelPosition = .bottomInside
    barChart.leftAxis.enabled = false
    barChart.rightAxis.enabled = false
    barChart.drawValueAboveBarEnabled = true
    barChart.xAxis.valueFormatter = IndexAxisValueFormatter(values:shows)
    barChart.xAxis.granularity = 1
    barChart.xAxis.drawAxisLineEnabled = false
    barChart.drawGridBackgroundEnabled = false
    for e in chartEntries{
        let bounds  = barChart.getBarBounds(entry: e)
        let gradientLayer = CAGradientLayer()
        gradientLayer.frame = bounds
        gradientLayer.colors = [UIColor(red:2, green:201, blue:255).cgColor,UIColor(red: 0, green:30,blue:189).cgColor]
        gradientLayer.locations = [0.5, 1.0]
        barChart.layer.addSublayer(gradientLayer)

    }`
screen shot 2017-06-11 at 5 47 27 pm

you can see in the screenshot the actualy bars in the light blue color and the gradient layer on top of them. The gradient layers' positions are off as well as the wrong size. Am I misunderstanding how the getBounds functions works or is it bugged?

liuxuan30 commented 7 years ago

Actually getBarBounds is not used by the library...

First, where do you set your chartView.data = chartData? make sure it's after your chart setup or call notifyDataSetChanged()

I compared horizontal bar chart's getBarBounds and prepareBuffer, it had a diff, basically getBarBounds does not handle stacked bar case, and when it's not stacked,

prepareBuffer is like:

            if !containsStacks || vals == nil
            {
                let bottom = CGFloat(x - barWidthHalf)
                let top = CGFloat(x + barWidthHalf)
                var right = isInverted
                    ? (y <= 0.0 ? CGFloat(y) : 0)
                    : (y >= 0.0 ? CGFloat(y) : 0)
                var left = isInverted
                    ? (y >= 0.0 ? CGFloat(y) : 0)
                    : (y <= 0.0 ? CGFloat(y) : 0)

                // multiply the height of the rect with the phase
                if right > 0
                {
                    right *= CGFloat(phaseY)
                }
                else
                {
                    left *= CGFloat(phaseY)
                }

                barRect.origin.x = left
                barRect.size.width = right - left
                barRect.origin.y = top
                barRect.size.height = bottom - top

                buffer.rects[bufferIndex] = barRect
                bufferIndex += 1
            }

vs. getBarBounds

    open override func getBarBounds(entry e: BarChartDataEntry) -> CGRect
    {
        guard
            let data = _data as? BarChartData,
            let set = data.getDataSetForEntry(e) as? IBarChartDataSet
            else { return CGRect.null }

        let y = e.y
        let x = e.x

        let barWidth = data.barWidth

        let top = x - 0.5 + barWidth / 2.0
        let bottom = x + 0.5 - barWidth / 2.0
        let left = y >= 0.0 ? y : 0.0
        let right = y <= 0.0 ? y : 0.0

        var bounds = CGRect(x: left, y: top, width: right - left, height: bottom - top)

        getTransformer(forAxis: set.axisDependency).rectValueToPixel(&bounds)

        return bounds
    }

I think it's a bug, but I don't know why there is a method for getting bar bounds. What you can try is override the method with prepareBuffer content to see if it fix your issue, or just remove +- 0.5

liuxuan30 commented 7 years ago

@danielgindi do you remember why +- 0.5 in horizontal bar chart getBarBounds? legacy code issue? Seems not necessary right now. Don't see other place do this +- 0.5

@AaronVinh can you help verify, if you just remove +-0.5 in getBarBounds, will it fix your issue? If so, seems we have a simple clean fix for this issue.

AaronVinh commented 7 years ago

After removing the 0.5 the bars appear to be the correct size, but in the wrong place.screen shot 2017-06-11 at 9 34 44 pm

liuxuan30 commented 7 years ago

After some digging, found out you have to add the isInverted condition as well.

So yes it's buggy using getBarBounds as it's not the same as what in renderer code. I will check how to make a full fix.

    open override func getBarBounds(entry e: BarChartDataEntry) -> CGRect
    {
        guard
            let data = _data as? BarChartData,
            let set = data.getDataSetForEntry(e) as? IBarChartDataSet
            else { return CGRect.null }

        let y = e.y
        let x = e.x
        let isInverted = self.isInverted(axis: set.axisDependency)

        let barWidth = data.barWidth

        let top = x  + barWidth / 2.0
        let bottom = x  - barWidth / 2.0
        let right = isInverted
            ? (y <= 0.0 ? y : 0.0)
            : (y >= 0.0 ? y : 0.0)
        let left = isInverted
            ? (y >= 0.0 ? y : 0.0)
            : (y <= 0.0 ? y : 0.0)

        var bounds = CGRect(x: left, y: top, width: right - left, height: bottom - top)

        getTransformer(forAxis: set.axisDependency).rectValueToPixel(&bounds)

        return bounds
    }

also, you have to be careful that you must add the layers after the valueToPixelMatrix is fully initialized. if you just put your code right after something like view.chartData = data, it will be wrong position.

You can setup a delay like:

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    for (int i = 0; i < yVals.count; i++) {
        ChartDataEntry *entry = [set1 entryForIndex:i];
        if (entry != nil) {
            CGRect bound = [_chartView getBarBoundsWithEntry:entry];
            CAGradientLayer *layer = [CAGradientLayer layer];
            [layer setFrame:bound];
            layer.colors = @[(id)[UIColor redColor].CGColor, (id)[UIColor greenColor].CGColor];
            layer.startPoint = CGPointMake(0.0, 0.5);
            layer.endPoint = CGPointMake(1.0, 0.5);
            [_chartView.layer addSublayer:layer];
        }
    }
});

or set a observer when the matrix is fully set after calling prepareOffsetMatrix or somewhere, or you just override the renderer code to add the gradient in real time using CoreGraphics (as layers may introduce side effect, I recommend not using CALayer inside the library)

image
gustavo-yy commented 7 years ago

is this possible using CombinedChartView()?