ChartsOrg / Charts

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

Grid background #645

Open woutergoossens opened 8 years ago

woutergoossens commented 8 years ago

Hi,

Great library!

I have to create a custom chart like this. Everything is working except for the different grid background colours. Is that possible with this library?

Thanks in advance,

Wouter

screen shot 2016-01-03 at 20 10 30
liuxuan30 commented 8 years ago

I would say yes, but it requires you to do some custom rendering.

e.g. everytime get the four bouding points and fill it. The code can help calculate the point coordinates, but you have to render it.

@danielgindi would you think it's a feature that will be used by many people?

richwagner commented 8 years ago

@liuxuan30 How would you recommend getting the four bounding points and filling it? (What part of the API?) Like the above request, I would like to shade a section of the background (e.g., in a line chart that shows the years 1990-2015 in x-axis, I want to be able to background shade 1998-2002 and then 2011-13).

liuxuan30 commented 8 years ago

combine yMax, yMin, XMin, XMax, plus each data value, you will know the bounding points in data value coordinate space, and you can easily convert it to pixel coordinate space using chart transformer. ContentRect may also help, but it is in pixel space.

richwagner commented 8 years ago

@liuxuan30 Are there any good examples you point out of using ChartTransformer (ios-chart) or Transformer (MPAndroidChart)? Especially for drawing on a section of a chart? Or is a matter of digging through the source? Thanks.

liuxuan30 commented 8 years ago

Almost every renderer will use chart transformer (aka trans in the code) to convert data value into pixel value and vice versa. You can look at any renderer or just search 'let trans =' to find an example. It's just a bridge to connect data value space and pixel value space, then you can easily manipulate data value and later transform it into pixel value.

A good example is x axis render's drawGridLines. If you know how to draw the gird lines, then you will easily know the four bounding points of the rect between two grid lines, and this rect is the rect you want to fill colors in the screenshot.

richwagner commented 8 years ago

@woutergoossens Not sure if you implemented anything yet, but based on guidance of @liuxuan30, I came up with the following solution for ChartXAxisRenderer and its subclasses - to add a renderGridAreas method that is based on and called after renderGridLines. Shown below is the implementation for ChartXAxisRendererBarChart.

    public override func renderGridAreas(context context: CGContext)
    {

        // New isDrawGridAreasEnabled property parallels isDrawGridLinesEnableld 

        // xAxis.filledAreas is an array of ChartXAxisAreaData instances, a new class
        // which has startX and endY properties

        if (!_xAxis.isDrawGridAreasEnabled || !_xAxis.isEnabled || _xAxis.filledAreas.count == 0)
        {
            return
        }

        let barData = _chart.data as! BarChartData
        let step = barData.dataSetCount

        CGContextSaveGState(context)

        var position = CGPoint(x: 0.0, y: 0.0)
        var endPosition = CGPoint(x: 0.0, y: 0.0)
        let valueToPixelMatrix = transformer.valueToPixelMatrix

        // xAxis.filledAreas is an array of a new class called ChartXAxisAreaData
        // which has startX and endY properties

        // Iterate through filled areas
        let c = _xAxis.filledAreas.count
        for (var i=0; i < c; i++) {
            let areaData = _xAxis.filledAreas[i];
            // Get start position, using the same logic as used in rendering gridlines
            let sx = Int(areaData.startX)
            position.x = CGFloat(sx * step) + CGFloat(sx) * barData.groupSpace - 0.5
            position = CGPointApplyAffineTransform(position, valueToPixelMatrix)
            // Get end position
            let ex = Int(areaData.endX)
            endPosition.x = CGFloat(ex * step) + CGFloat(ex) * barData.groupSpace - 0.5
            endPosition = CGPointApplyAffineTransform(endPosition, valueToPixelMatrix)
            // Draw rectangle - TODO externalize color
            let rectangle = CGRect(x: position.x, y: viewPortHandler.contentTop, width: CGFloat(endPosition.x-position.x), height: viewPortHandler.contentBottom)
            let blue = UIColor(red:215/255, green: 231/255, blue: 241/255, alpha: 0.5);
            CGContextSetFillColorWithColor(context, blue.CGColor)
            CGContextSetStrokeColorWithColor(context, blue.CGColor)
            CGContextSetLineWidth(context, 1)
            CGContextAddRect(context, rectangle)
            CGContextDrawPath(context, .FillStroke)
        }

        CGContextRestoreGState(context)
    }

I then added the following line to the drawRect method of BarLineChartViewBase, just after grid lines is rendered:

        _xAxisRenderer?.renderGridAreas(context: context)

In my case, I need arbitrary grid areas that is based on the data. However, this could be modified to render a background for every other grid column.

liuxuan30 commented 8 years ago

@richwagner good job!

danielgindi commented 8 years ago

We may add this as a feature - I'm looking into it! Seems like something that could add style (or clarity!) to some charts.

It's nice that we already have some code/logic here :-)

I have just pushed a feature for filling line/radar charts with gradients or images, and I'm asking myself if we should be using the new ChartFill object to draw the "grid areas". The new ChartFill object is something somewhat similar to Android's drawable, in the sense that it can represent an image, a color, a gradient, or a layer etc.

@PhilJay, @richwagner what do you think?

PhilJay commented 8 years ago

Yes, could definitely be a great new feature I would say.

We need to think this though and come up with a solution that is as easy as possible to "use".

danielgindi commented 8 years ago

Any propositions?

PhilJay commented 8 years ago

Well I guess we definitely need an extra object that stores the information. And then I guess to that object it should be possible to add some kind of region + color

richwagner commented 8 years ago

@danielgindi I definitely would recommend adding this feature.

Concerning an extra object: In my renderGridAreas implementation (above), I utilized a ChartXAxisAreaData object for x-axis-based grid areas (with color defaults for my specific purpose):

public class ChartXAxisAreaData: NSObject {
    public var startX = CGFloat(0)
    public var endX = CGFloat(0)
    public var color = UIColor(red:215/255, green: 231/255, blue: 241/255, alpha: 0.5)
}

I then add an area to a chart like:

    ChartXAxisAreaData *areaData2 = [[ChartXAxisAreaData alloc] init];
    areaData2.startX = 204.0;
    areaData2.endX = 227.0;
    [filledAreas addObject:areaData2];    
    _chartView.xAxis.filledAreas = filledAreas;
danielgindi commented 8 years ago

That would be an overkill as you want alternation, not to specify each and every stripe of color, am I missing something?

richwagner commented 8 years ago

@danielgindi In my case, I needed to define arbitrary data-dependent grid areas, so that level of detail was helpful. But in the above case by @woutergoossens of alternate grid areas, that would be overkill.

vojto commented 8 years ago

Is there any way to add custom drawing to the chart?

eg. I would like similar functionality to this - but a little different, I want different background color for weekends, ie. very custom functionality.

It would be great to add this as a class conforming to some Drawable interface.

jtweeks commented 7 years ago

Was this ever implemented? Drawing a fill in between values, limit lines, etc...

jtweeks commented 7 years ago

Here is my implementation if someone wishes to add to the library or do their own. Currently it fills in between 2 fill lines on the Y showing an area that would indicate an ideal range.

Added this code to YAxisRenderer

open func renderLimitFill(context: CGContext) {
        guard
            let yAxis = self.axis as? YAxis,
            let viewPortHandler = self.viewPortHandler,
            let transformer = self.transformer
            else { return }

        var limitLines = yAxis.limitLines

        if limitLines.count != 2
        {
            return
        }

        var upper = 0.0
        var lower = 0.0

        context.saveGState()

        let trans = transformer.valueToPixelMatrix

        var position = CGPoint(x: 0.0, y: 0.0)

        for i in 0 ..< limitLines.count
        {
            let l = limitLines[i]

            if !l.isEnabled
            {
                continue
            }

            if l.limit > upper {
                upper = l.limit
            }
            else {
                lower = l.limit
            }
        }

        if upper == lower {
            return
        }

        var startPosition = CGPoint(x: 0.0, y: 0.0)
        startPosition.x = 0.0
        startPosition.y = CGFloat(upper)
        startPosition = startPosition.applying(trans)

        var endPosition = CGPoint(x: 0.0, y: 0.0)
        endPosition.y = CGFloat(lower)
        endPosition = endPosition.applying(trans)
        endPosition.x = viewPortHandler.contentRight

        let rect = CGRect(x: min(startPosition.x, endPosition.x),
                          y: min(startPosition.y, endPosition.y),
                          width: fabs(startPosition.x - endPosition.x),
                          height: fabs(startPosition.y - endPosition.y));

        context.setFillColor(UIColor.green.withAlphaComponent(0.3).cgColor)
        context.setStrokeColor(UIColor.green.cgColor)
        context.setLineWidth(0.0)
        context.addRect(rect)
        context.drawPath(using: .fillStroke)

        context.restoreGState()
    }

then called it in BarLineChartViewBase

if _leftAxis.isEnabled && _leftAxis.isDrawLimitLinesBehindDataEnabled
        {
            _leftYAxisRenderer?.renderLimitLines(context: context)
            _leftYAxisRenderer?.renderLimitFill(context: context)
        }

if _leftAxis.isEnabled && !_leftAxis.isDrawLimitLinesBehindDataEnabled
        {
            _leftYAxisRenderer?.renderLimitLines(context: context)
            _leftYAxisRenderer?.renderLimitFill(context: context)
        }
marko-mni commented 6 years ago

@jtweeks Would you be kind enough to tell me how do you call the function for the graph view ?

Gaebuidhe commented 6 years ago

what is the variable context: CGContext a reference to? @jtweeks

acegreen commented 4 years ago

@richwagner I'm working of your implementation but tried modified it so that we just provide an [Int]

Screen Shot 2020-03-10 at 8 29 30 AM

    public override func renderGridAreas(context: CGContext, chart: ChartViewBase)
    {

        // New isDrawGridAreasEnabled property parallels isDrawGridLinesEnableld
        // xAxis.filledIndex is an array of Int values correspdoning to the index of entry to be highlighted

        guard
            let xAxis = self.axis as? XAxis,
            let transformer = self.transformer
            else { return }

        if (!xAxis.drawGridAreasEnabled || !xAxis.isEnabled || xAxis.filledIndexes.count == 0)
        {
            return
        }

        let barData = chart.data as! BarChartData
        let step = CGFloat(barData.dataSets.first?.entryCount ?? 0)

        context.saveGState()

        var position = CGPoint(x: 0.0, y: 0.0)
        let valueToPixelMatrix = transformer.valueToPixelMatrix
        let entries = xAxis.entries

        // Iterate through filled areas
        for index in xAxis.filledIndexes
        {
            // Get start position, using the same logic as used in rendering gridlines
            let width = (viewPortHandler.contentWidth / step)
            position.x = CGFloat(entries[index]) - 0.5
            position = position.applying(valueToPixelMatrix)

            // Draw rectangle - TODO externalize color
            let rectangle = CGRect(x: position.x, y: viewPortHandler.contentTop, width: width, height: viewPortHandler.contentBottom + xAxis.labelHeight)
            context.setFillColor(xAxis.gridAreaColor.cgColor)
            context.setStrokeColor(xAxis.gridAreaColor.cgColor)
            context.setLineWidth(1)
            context.addRect(rectangle)
            context.drawPath(using: .fillStroke)
        }

        context.restoreGState()
    }

EDIT: I ended up rewriting this to make it part of the BarChartRenderer rather than the axis, and allowing highlighting to be either .bar or .background through HighlightStyle

https://github.com/AudaxHealthInc/Charts/tree/feature/missions

dushansri commented 3 years ago

Hi @danielgindi

It's a perfect and great library.

Screenshot 2020-11-22 at 2 57 48 AM

Has the Chart library given this feature now?

lx-xi commented 2 years ago

Added this code to YAxisRenderer

open var fillAreaColor: NSUIColor = NSUIColor.clear
open var fillArea: (Float, Float)?

open func areaFill(context: CGContext) {
        guard let transformer = self.transformer else { return }
        // 如果颜色是clear,则不作填充处理
        guard fillAreaColor != NSUIColor.clear else { return }
        // 如果没配范围,则不作填充
        guard let area = fillArea else { return }

        let lower = area.0
        let upper = area.1
        if upper == lower {
            return
        }

        context.saveGState()

        let trans = transformer.valueToPixelMatrix

        var startPosition = CGPoint(x: 0.0, y: 0.0)
        startPosition.x = 0.0
        startPosition.y = CGFloat(upper)
        startPosition = startPosition.applying(trans)

        var endPosition = CGPoint(x: 0.0, y: 0.0)
        endPosition.y = CGFloat(lower)
        endPosition = endPosition.applying(trans)
        endPosition.x = viewPortHandler.contentRight

        let rect = CGRect(x: min(startPosition.x, endPosition.x),
                          y: min(startPosition.y, endPosition.y),
                          width: abs(startPosition.x - endPosition.x),
                          height: abs(startPosition.y - endPosition.y));

        context.setFillColor(fillAreaColor.cgColor)
//        context.setStrokeColor(UIColor.green.cgColor)
        context.setLineWidth(0.0)
        context.addRect(rect)
        context.drawPath(using: .fillStroke)

        context.restoreGState()
    }

called it in BarLineChartViewBase -> func draw(_ rect: CGRect) leftYAxisRenderer.areaFill(context: context)

add an area to a chart like: lineChartView.leftYAxisRenderer.fillAreaColor = UIColor.white.withAlphaComponent(0.2) lineChartView.leftYAxisRenderer.fillArea = (50, 60)

Thanks @jtweeks

badrinathvm commented 2 years ago

@danielgindi is this feature available as part of latest library version ?