Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Grid background #645

Open
woutergoossens opened this issue Jan 3, 2016 · 23 comments
Open

Grid background #645

woutergoossens opened this issue Jan 3, 2016 · 23 comments

Comments

@woutergoossens
Copy link

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
Copy link
Member

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
Copy link

@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
Copy link
Member

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
Copy link

@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
Copy link
Member

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
Copy link

@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
Copy link
Member

@richwagner good job!

@danielgindi
Copy link
Collaborator

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
Copy link
Collaborator

PhilJay commented Jan 27, 2016

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
Copy link
Collaborator

Any propositions?

@PhilJay
Copy link
Collaborator

PhilJay commented Jan 27, 2016

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
Copy link

@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
Copy link
Collaborator

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

@richwagner
Copy link

@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
Copy link

vojto commented Oct 31, 2016

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
Copy link

jtweeks commented Oct 8, 2017

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

@jtweeks
Copy link

jtweeks commented Oct 8, 2017

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
Copy link

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

@Gaebuidhe
Copy link

Gaebuidhe commented Feb 27, 2018

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

@acegreen
Copy link

acegreen commented Mar 10, 2020

@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
Copy link

dushansri commented Nov 21, 2020

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
Copy link

lx-xi commented Apr 27, 2022

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
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests