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

Filled Line Chart #2863

Closed
sunynator opened this issue Oct 10, 2017 · 22 comments
Closed

Filled Line Chart #2863

sunynator opened this issue Oct 10, 2017 · 22 comments

Comments

@sunynator
Copy link

I'm trying to draw a filled line chart, you guys can see the picture for more clarity.
screen shot 2017-10-09 at 23 02 38
So, I saw the example and they are using IFillFormatter protocol to get y-axis position where the filled line of dataset end. But, it was a fixed position, not dynamic at all. How can I can this position depend on the each element of dataset. For instance, if index == 0 then we have a the end of filled position A, if index == 1 then we have a different end value of a filled position.
Or, any other suggestion to draw a chart like the picture above.

@liuxuan30
Copy link
Member

take a look at drawLinearFill where it draw the fill rect. It calculates the fill rect within generateFilledPath and getFillLinePosition. ideally you should override those methods to get your rect between lines, you are good to go

@rob-k
Copy link

rob-k commented Nov 15, 2017

I managed to achieve this by following @liuxuan30 's suggestions by subclassing LineChartRenderer and adding a variable to the class implementing IFillFormatter, which let's me pass a second data set as indicator for the fill line and draw the path accordingly (only for linear, but I assume you could do the same thing for Bezier).

class AreaFillFormatter: IFillFormatter {

    var fillLineDataSet: LineChartDataSet?
    
    init(fillLineDataSet: LineChartDataSet) {
        self.fillLineDataSet = fillLineDataSet
    }
    
    public func getFillLinePosition(dataSet: ILineChartDataSet, dataProvider: LineChartDataProvider) -> CGFloat {
        return 0.0
    }
    
    public func getFillLineDataSet() -> LineChartDataSet {
        return fillLineDataSet ?? LineChartDataSet()
    }
    
}
class CustomLineChartRenderer: LineChartRenderer {
    
    override open func drawLinearFill(context: CGContext, dataSet: ILineChartDataSet, trans: Transformer, bounds: XBounds) {
        guard let dataProvider = dataProvider else { return }
        
        let areaFillFormatter = dataSet.fillFormatter as? AreaFillFormatter
        
        let filled = generateFilledPath(
            dataSet: dataSet,
            fillMin: dataSet.fillFormatter?.getFillLinePosition(dataSet: dataSet, dataProvider: dataProvider) ?? 0.0,
            fillLineDataSet: areaFillFormatter?.getFillLineDataSet(),
            bounds: bounds,
            matrix: trans.valueToPixelMatrix)
        
        if dataSet.fill != nil
        {
            drawFilledPath(context: context, path: filled, fill: dataSet.fill!, fillAlpha: dataSet.fillAlpha)
        }
        else
        {
            drawFilledPath(context: context, path: filled, fillColor: dataSet.fillColor, fillAlpha: dataSet.fillAlpha)
        }
    }
    
    fileprivate func generateFilledPath(dataSet: ILineChartDataSet, fillMin: CGFloat, fillLineDataSet: ILineChartDataSet?, bounds: XBounds, matrix: CGAffineTransform) -> CGPath
    {
        let phaseY = animator?.phaseY ?? 1.0
        let isDrawSteppedEnabled = dataSet.mode == .stepped
        let matrix = matrix
        
        var e: ChartDataEntry!
        var fillLineE: ChartDataEntry?
        
        let filled = CGMutablePath()
        
        e = dataSet.entryForIndex(bounds.min)
        fillLineE = fillLineDataSet?.entryForIndex(bounds.min)
        
        if e != nil
        {
            if let fillLineE = fillLineE
            {
                filled.move(to: CGPoint(x: CGFloat(e.x), y: CGFloat(fillLineE.y * phaseY)), transform: matrix)
            }
            else
            {
                filled.move(to: CGPoint(x: CGFloat(e.x), y: fillMin), transform: matrix)
            }
            
            filled.addLine(to: CGPoint(x: CGFloat(e.x), y: CGFloat(e.y * phaseY)), transform: matrix)
        }
        
        // Create the path for the data set entries
        for x in stride(from: (bounds.min + 1), through: bounds.range + bounds.min, by: 1)
        {
            guard let e = dataSet.entryForIndex(x) else { continue }
            
            if isDrawSteppedEnabled
            {
                guard let ePrev = dataSet.entryForIndex(x-1) else { continue }
                filled.addLine(to: CGPoint(x: CGFloat(e.x), y: CGFloat(ePrev.y * phaseY)), transform: matrix)
            }
            
            filled.addLine(to: CGPoint(x: CGFloat(e.x), y: CGFloat(e.y * phaseY)), transform: matrix)
        }
        
        // Draw a path to the start of the fill line
        e = dataSet.entryForIndex(bounds.range + bounds.min)
        fillLineE = fillLineDataSet?.entryForIndex(bounds.range + bounds.min)
        if e != nil
        {
            if let fillLineE = fillLineE
            {
                filled.addLine(to: CGPoint(x: CGFloat(e.x), y: CGFloat(fillLineE.y * phaseY)), transform: matrix)
            }
            else
            {
                filled.addLine(to: CGPoint(x: CGFloat(e.x), y: fillMin), transform: matrix)
            }
        }
        
        // Draw the path for the fill line (backwards)
        if let fillLineDataSet = fillLineDataSet {
            for x in stride(from: (bounds.min + 1), through: bounds.range + bounds.min, by: 1).reversed()
            {
                guard let e = fillLineDataSet.entryForIndex(x) else { continue }
                
                if isDrawSteppedEnabled
                {
                    guard let ePrev = fillLineDataSet.entryForIndex(x-1) else { continue }
                    filled.addLine(to: CGPoint(x: CGFloat(e.x), y: CGFloat(ePrev.y * phaseY)), transform: matrix)
                }

                filled.addLine(to: CGPoint(x: CGFloat(e.x), y: CGFloat(e.y * phaseY)), transform: matrix)
            }
        }
        
        filled.closeSubpath()
        
        return filled
    }
}

@liuxuan30
Copy link
Member

perfect example to mastering the project :)

@jjy4880
Copy link

jjy4880 commented Dec 5, 2018

how to resolve/ use this solution?

@bhargavsejpalindianic
Copy link

@rob-k can you please provide usage example for this

@rob-k
Copy link

rob-k commented May 27, 2019

Sure, I use the code as follows:

let maxDataSet = LineChartDataSet()
let minDataSet = LineChartDataSet()

// ... fill the data sets

// Set the data
self.lineChart.data = LineChartData(dataSets: [maxDataSet, minDataSet])

// Set the custom line chart renderer
self.lineChart.renderer = CustomLineChartRenderer(dataProvider: self.lineChart, animator: self.lineChart.chartAnimator, viewPortHandler: self.lineChart.viewPortHandler)

maxDataSet.drawFilledEnabled = true
maxDataSet.fillFormatter = AreaFillFormatter(fillLineDataSet: minDataSet)

@bhargavsejpalindianic
Copy link

i installed this manually and followed each step as per example still looks like its not working @rob-k

@bhargavsejpalindianic
Copy link

@rob-k i tried to debug using break points any of function from CustomLineChartRenderer not being called

@rob-k
Copy link

rob-k commented May 28, 2019

I updated my comment to include setting the LineChartRenderer.

@bhargavsejpalindianic
Copy link

still no luck man @rob-k

@rob-k
Copy link

rob-k commented May 28, 2019

If you want the color between ucbDataSet and lcbDataSet you need to pass the second data set to the formatter of the first one. You are passing the dataset to the formatter of itself. Try to do the following:

ucbDataSet.fillFormatter = AreaFillFormatter(fillLineDataSet: lcbDataSet)

(or the other way around)

@bhargavsejpalindianic
Copy link

already tried what i am trying to explain is
i tried to debug using break points any of function from CustomLineChartRenderer class not being called

@rob-k
Copy link

rob-k commented May 28, 2019

I see that you are using Bezier mode in your code:

lineChartDataSet.mode = .horizontalBezier
ucbDataSet.mode = .horizontalBezier
lcbDataSet.mode = .horizontalBezier

The code I provided only works for linear mode. You will have to change the mode to .linear or override drawHorizontalBezier and adjust the code accordingly.

@bhargavsejpalindianic
Copy link

@rob-k got it just worked thanks very much man for finding it out

@michal-th
Copy link

Would it be possible to implement this using CombinedChart ?

@Chakib-Temal
Copy link

Chakib-Temal commented Dec 13, 2019

Would it be possible to implement this using CombinedChart ?

did you find the solution ? the CombinedChart use the CombinedChartRender , not the LinearChartRender , so the call back is not called drawLinearFill , it use the default Render

the method drawLinearFill doesn't exist , it's take many of new renders when you search in the library

@Chakib-Temal
Copy link

Would it be possible to implement this using CombinedChart ?

solution combinedChart : PhilJay/MPAndroidChart#338

@komunamu
Copy link

@rob-k Thank you for sharing the example. It works perfectly for the first drawing. If I change the dateset, it looks like the old chart and the new chart are mixed or duplicated. How to clean the preview chart and draw the new chart using the new dataset? Thank you for your help in advance.

issue

@rob-k
Copy link

rob-k commented May 25, 2021

@komunamu can your share the code that you use to update the chart data? If you simply replace the data set arrays and call lineChart.notifyDataSetChanged() this should be sufficient.

@AlexChzuang
Copy link

@komunamu hello, could you share your code of the amazing charts? Thanks a lot!

@komunamu
Copy link

@AlexChzuang Sorry I don't have the source code. I think the updated demo code has the sample code that filled the line chart. Good luck!

@suryasahu92
Copy link

This has been a great help

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

10 participants