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

How can I draw inconsecutive line chart? #533

Closed
AntiMoron opened this issue Nov 4, 2015 · 19 comments
Closed

How can I draw inconsecutive line chart? #533

AntiMoron opened this issue Nov 4, 2015 · 19 comments

Comments

@AntiMoron
Copy link
Contributor

How can I draw line chart with some data not set.

For example:

[x,y] : {
[0,1]
[1,1]
[2,1]
[4,1]
[5,1]
}

Point at x = 3 is missing. The line chart should break at x = 3

@liuxuan30
Copy link
Member

Well there is #280 asking for it but not implemented yet. It's not as easy as it looks like, because we have to think about the filling for negative + positive values and the zero y axis cutting. I had my own implementation, excluding the filling part, and still buggy.

#335 is a PR but closed early, I guess it has the same problem as mine.

@danielgindi
Copy link
Collaborator

This does not sound like a normal line chart, but rather like multiple
datasets

‏בתאריך יום חמישי, 5 בנובמבר 2015, Xuan notifications@github.com כתב:

Closed #533 #533.


Reply to this email directly or view it on GitHub
#533 (comment).

@liuxuan30
Copy link
Member

it's just a xIndex which has no value. But, multiple datasets are a great trick! Though we have to break the data sets :)

@danielgindi
Copy link
Collaborator

An xIndex with no value should not mean "break the line", but "I have no
value for this point, draw the line to the next value"...

On Thu, Nov 5, 2015 at 8:05 AM, Xuan notifications@github.com wrote:

it's just a xIndex which has no value. But, multiple datasets are a great
trick! Though we have to break the data sets :)


Reply to this email directly or view it on GitHub
#533 (comment)
.

@AntiMoron
Copy link
Contributor Author

@danielgindi

Breaking the line is neccessary.

Try multiple lines in one chart:

Line1 : {[0,5],[1,5],[3,5],[4,5]}   -- point at x = 2 is missing
Line2 : {[0,5],[2,5],[3,5],[4,5]}   -- point at x = 1 is missing

How can this be implemented ?
The two lines share the same xAxis. If lines are not broken, which x values should be kept?

@liuxuan30
Copy link
Member

@danielgindi
"I have no value for this point, draw the line to the next value"
I guess there are lots of people will argue why not break here. line break has its meaning also.

@AntiMoron I think what @danielgindi said is you need 4 dataSets for this two lines? the first 2 dataSets are Line1, and the last 2 are Line 2? So it can render like line break, but requires addtional logic to break the data sets. Will this workaround for you?

I will vote line break feature for sure :)

@AntiMoron
Copy link
Contributor Author

@liuxuan30 I was just thinking how the semantic about an xIndex with no value that Daniel described fit the situation I assumed.

@AntiMoron
Copy link
Contributor Author

For now,this workaround is fine.Thx
I think it's not that good to have 4 datasets to show just 2 lines... It's not correct exactly.

@danielgindi
Copy link
Collaborator

Well, it's not 2 lines, it's 4 lines!

On Thu, Nov 5, 2015 at 2:40 PM, Anti_Moron notifications@github.com wrote:

For now,this workaround is fine.
I think it's not that good to have 4 datasets to show just 2 lines... It's
not correct exactly.


Reply to this email directly or view it on GitHub
#533 (comment)
.

@AntiMoron
Copy link
Contributor Author

😂 😂 😂 😂 😂 😂 😂 alright then

@danielgindi
Copy link
Collaborator

We may support this in the future though

On Thu, Nov 5, 2015 at 2:42 PM, Anti_Moron notifications@github.com wrote:

[image: 😂] [image: 😂] [image: 😂] [image: 😂] [image: 😂] [image:
😂] alright then


Reply to this email directly or view it on GitHub
#533 (comment)
.

@AntiMoron
Copy link
Contributor Author

Looking forward to!!

@automactic
Copy link

automactic commented Apr 27, 2016

May I offer a solution for 1 color per line situation? (since I only use one color per line)
I subclassed LineChartRenderer, then set my own renderer to lineChartView.renderer
I don't know if I should post my code here, or make a pull request.

`class LineChartLineBreakRenderer: LineChartRenderer {
private var _lineSegments = [CGPoint](count: 2, repeatedValue: CGPoint%28%29)
override func drawLinear(context context: CGContext, dataSet: ILineChartDataSet) {
guard let
trans = dataProvider?.getTransformer(dataSet.axisDependency),
animator = animator
else { return }

    let valueToPixelMatrix = trans.valueToPixelMatrix

    let entryCount = dataSet.entryCount
    let isDrawSteppedEnabled = dataSet.isDrawSteppedEnabled
    let pointsPerEntryPair = isDrawSteppedEnabled ? 4 : 2

    let phaseX = animator.phaseX
    let phaseY = animator.phaseY

    guard let
        entryFrom = dataSet.entryForXIndex(self.minX < 0 ? self.minX : 0, rounding: .Down),
        entryTo = dataSet.entryForXIndex(self.maxX, rounding: .Up)
        else { return }

    let diff = (entryFrom == entryTo) ? 1 : 0
    let minx = max(dataSet.entryIndex(entry: entryFrom) - diff, 0)
    let maxx = min(max(minx + 2, dataSet.entryIndex(entry: entryTo) + 1), entryCount)

    CGContextSaveGState(context)

    CGContextSetLineCap(context, dataSet.lineCapType)

    // more than 1 color
    if (dataSet.colors.count > 1)
    {
        if (_lineSegments.count != pointsPerEntryPair)
        {
            _lineSegments = [CGPoint](count: pointsPerEntryPair, repeatedValue: CGPoint())
        }

        let count = Int(ceil(CGFloat(maxx - minx) * phaseX + CGFloat(minx)))
        for j in minx ..< count
        {
            if (count > 1 && j == count - 1)
            { // Last point, we have already drawn a line to this point
                break
            }

            var e: ChartDataEntry! = dataSet.entryForIndex(j)

            if e == nil { continue }

            _lineSegments[0].x = CGFloat(e.xIndex)
            _lineSegments[0].y = CGFloat(e.value) * phaseY

            if (j + 1 < count)
            {
                e = dataSet.entryForIndex(j + 1)

                if e == nil { break }

                if isDrawSteppedEnabled
                {
                    _lineSegments[1] = CGPoint(x: CGFloat(e.xIndex), y: _lineSegments[0].y)
                    _lineSegments[2] = _lineSegments[1]
                    _lineSegments[3] = CGPoint(x: CGFloat(e.xIndex), y: CGFloat(e.value) * phaseY)
                }
                else
                {
                    _lineSegments[1] = CGPoint(x: CGFloat(e.xIndex), y: CGFloat(e.value) * phaseY)
                }
            }
            else
            {
                _lineSegments[1] = _lineSegments[0]
            }

            for i in 0..<_lineSegments.count
            {
                _lineSegments[i] = CGPointApplyAffineTransform(_lineSegments[i], valueToPixelMatrix)
            }

            if (!viewPortHandler.isInBoundsRight(_lineSegments[0].x))
            {
                break
            }

            // make sure the lines don't do shitty things outside bounds
            if (!viewPortHandler.isInBoundsLeft(_lineSegments[1].x)
                || (!viewPortHandler.isInBoundsTop(_lineSegments[0].y) && !viewPortHandler.isInBoundsBottom(_lineSegments[1].y))
                || (!viewPortHandler.isInBoundsTop(_lineSegments[0].y) && !viewPortHandler.isInBoundsBottom(_lineSegments[1].y)))
            {
                continue
            }

            // get the color that is set for this line-segment
            CGContextSetStrokeColorWithColor(context, dataSet.colorAt(j).CGColor)
            CGContextStrokeLineSegments(context, _lineSegments, pointsPerEntryPair)
        }
    }
    else
    { // only one color per dataset

        var e1: ChartDataEntry!
        var e2: ChartDataEntry!

        var slices = [(start: Int, end: Int)]()

        if (_lineSegments.count != max((entryCount - 1) * pointsPerEntryPair, pointsPerEntryPair))
        {
            _lineSegments = [CGPoint](count: max((entryCount - 1) * pointsPerEntryPair, pointsPerEntryPair), repeatedValue: CGPoint())
        }

        e1 = dataSet.entryForIndex(minx)

        if e1 != nil
        {
            let count = Int(ceil(CGFloat(maxx - minx) * phaseX + CGFloat(minx)))

            var j = 0
            var sliceStartXIndex = 0

            for x in (count > 1 ? minx + 1 : minx) ..< count
            {
                e1 = dataSet.entryForIndex(x == 0 ? 0 : (x - 1))
                e2 = dataSet.entryForIndex(x)
                //                    print("e1x=\(e1.xIndex), e2x=\(e2.xIndex), \(x), \(j)")

                // e1 and e2 are not consecutive
                if e1.xIndex + 1 != e2.xIndex {
                    slices.append((sliceStartXIndex, j - 1))
                    sliceStartXIndex = j + pointsPerEntryPair
                }

                if e1 == nil || e2 == nil { continue }

                _lineSegments[j] = CGPointApplyAffineTransform(
                    CGPoint(
                        x: CGFloat(e1.xIndex),
                        y: CGFloat(e1.value) * phaseY
                    ), valueToPixelMatrix)
                j += 1

                if isDrawSteppedEnabled
                {
                    _lineSegments[j] = CGPointApplyAffineTransform(
                        CGPoint(
                            x: CGFloat(e2.xIndex),
                            y: CGFloat(e1.value) * phaseY
                        ), valueToPixelMatrix)
                    j += 1

                    _lineSegments[j] = CGPointApplyAffineTransform(
                        CGPoint(
                            x: CGFloat(e2.xIndex),
                            y: CGFloat(e1.value) * phaseY
                        ), valueToPixelMatrix)
                    j += 1
                }

                _lineSegments[j] = CGPointApplyAffineTransform(
                    CGPoint(
                        x: CGFloat(e2.xIndex),
                        y: CGFloat(e2.value) * phaseY
                    ), valueToPixelMatrix)
                j += 1
            }

            // add the last slice
            slices.append((sliceStartXIndex, _lineSegments.count - 1))

            // draw lines
            for slice in slices {
                let sliceLineSegments = Array(_lineSegments[slice.start...slice.end])
                CGContextSetStrokeColorWithColor(context, dataSet.colorAt(0).CGColor)
                CGContextStrokeLineSegments(context, sliceLineSegments, sliceLineSegments.count)
            }
        }
    }

    CGContextRestoreGState(context)

    // if drawing filled is enabled
    if (dataSet.isDrawFilledEnabled && entryCount > 0)
    {
        drawLinearFill(context: context, dataSet: dataSet, minx: minx, maxx: maxx, trans: trans)
    }
}

}`

@kscheff
Copy link

kscheff commented Dec 11, 2018

I understand the concept of breaking the line graph into multiple sets in order to achieve different sections with no line in between. However I don't like this concept, especially when multiple different types of lines are involved (e.g. with right and left axis). All assorted colors and labels would need to be taken into consideration as well. In my use case historic battery current and voltage is displayed in a single line chart, and the sample data sometimes has gaps. The nominal interval is 600 seconds. See attached how the graph currently looks like, the gaps are marked with the red arrows. Any ideas how to better handle this situation? Thanks.

img_2162

OK, I made an array of arrays with the data, so I can group it [[LineChartData] the remaining issue is that the label for each segment is then present. Assigning only to the first segment a label and the subsequent label needs to be switched of by:

ds.label = nil 
ds.form = .none  // remove form element 

img_2165

@sashkopotapov
Copy link

yo, guys. I was not able to find solution, but wrote my own for horizontal bezier.
Screenshot 2020-08-05 at 18 48 58

@sashkopotapov
Copy link

sashkopotapov commented Aug 5, 2020

import UIKit
import CoreGraphics

class CustomLineChartRenderer: LineChartRenderer {
    override func drawHorizontalBezier(context: CGContext, dataSet: ILineChartDataSet)
    {
        guard let dataProvider = dataProvider else { return }
        
        let trans = dataProvider.getTransformer(forAxis: dataSet.axisDependency)
        
        let phaseY = animator.phaseY
        
        _xBounds.set(chart: dataProvider, dataSet: dataSet, animator: animator)
        
        let set = (dataSet as! LineChartDataSet)
        
        // get the color that is specified for this position from the DataSet
        let drawingColor = dataSet.colors.first!
        
        // the path for the cubic-spline
        let cubicPath = CGMutablePath()
        let fillPath = CGMutablePath()
        
        let valueToPixelMatrix = trans.valueToPixelMatrix
        
        if _xBounds.range >= 1
        {
            var prev: ChartDataEntry! = dataSet.entryForIndex(_xBounds.min)
            var cur: ChartDataEntry! = prev
            
            // let the spline start
            cubicPath.move(to: CGPoint(x: CGFloat(cur.x), y: CGFloat(cur.y * phaseY)), transform: valueToPixelMatrix)
            fillPath.move(to: CGPoint(x: CGFloat(cur.x), y: CGFloat(cur.y * phaseY)), transform: valueToPixelMatrix)
            
            var xValues: [Int] = []
            set.entries.forEach { xValues.append(Int($0.x)) }
            
            for j in 1..<set.entries.count
            {
                prev = cur
                cur = dataSet.entryForIndex(j)
                
                if cur.x - prev.x > 1 {
                    fillPath.addLine(to: CGPoint(x: CGFloat(prev.x), y: CGFloat(0)), transform: valueToPixelMatrix)
                    fillPath.addLine(to: CGPoint(x: CGFloat(cur.x), y: CGFloat(0)), transform: valueToPixelMatrix)
                    fillPath.addLine(to: CGPoint(x: CGFloat(cur.x), y: CGFloat(cur.y * phaseY)), transform: valueToPixelMatrix)
                    
                    cubicPath.move(to: CGPoint(x: CGFloat(cur.x),
                                               y: CGFloat(cur.y * phaseY)),
                                   transform: valueToPixelMatrix)
                } else {
                    let cpx = CGFloat(prev.x + (cur.x - prev.x) / 2.0)
                    
                    cubicPath.addCurve(
                        to: CGPoint(
                            x: CGFloat(cur.x),
                            y: CGFloat(cur.y * phaseY)),
                        control1: CGPoint(
                            x: cpx,
                            y: CGFloat(prev.y * phaseY)),
                        control2: CGPoint(
                            x: cpx,
                            y: CGFloat(cur.y * phaseY)),
                        transform: valueToPixelMatrix)
                    
                    fillPath.addCurve(
                        to: CGPoint(
                            x: CGFloat(cur.x),
                            y: CGFloat(cur.y * phaseY)),
                        control1: CGPoint(
                            x: cpx,
                            y: CGFloat(prev.y * phaseY)),
                        control2: CGPoint(
                            x: cpx,
                            y: CGFloat(cur.y * phaseY)),
                        transform: valueToPixelMatrix)
                }
            }
        }
        
        context.saveGState()
        
        if dataSet.isDrawFilledEnabled
        {
            // Copy this path because we make changes to it
//            let fillPath = cubicPath.mutableCopy()
            
            drawCubicFill(context: context,
                          dataSet: dataSet,
                          spline: fillPath,
                          matrix: valueToPixelMatrix,
                          bounds: _xBounds)
        }
        
        context.beginPath()
        context.addPath(cubicPath)
        context.setStrokeColor(drawingColor.cgColor)
        context.strokePath()
        
        context.restoreGState()
    }
    
    override func drawCubicFill(
        context: CGContext,
        dataSet: ILineChartDataSet,
        spline: CGMutablePath,
        matrix: CGAffineTransform,
        bounds: XBounds)
    {
        guard
            let dataProvider = dataProvider
            else { return }
        
        if bounds.range <= 0
        {
            return
        }
        
        let fillMin = dataSet.fillFormatter?.getFillLinePosition(dataSet: dataSet, dataProvider: dataProvider) ?? 0.0
        
        var pt1 = CGPoint(x: CGFloat(dataSet.entryForIndex(bounds.min + bounds.range)?.x ?? 0.0), y: fillMin)
        var pt2 = CGPoint(x: CGFloat(dataSet.entryForIndex(bounds.min)?.x ?? 0.0), y: fillMin)
        pt1 = pt1.applying(matrix)
        pt2 = pt2.applying(matrix)
        
        spline.addLine(to: pt1)
        spline.addLine(to: pt2)
        spline.closeSubpath()
        
        drawFilledPath(context: context, path: spline, fillColor: dataSet.fillColor, fillAlpha: dataSet.fillAlpha)
    }
    
    override func drawFilledPath(context: CGContext,
                                 path: CGPath,
                                 fillColor: NSUIColor,
                                 fillAlpha: CGFloat)
    {
        context.saveGState()
        context.beginPath()
        context.addPath(path)
        
        // filled is usually drawn with less alpha
        context.setAlpha(fillAlpha)
        
        context.setFillColor(fillColor.cgColor)
        context.fillPath()
        
        context.restoreGState()
    }}

@sashkopotapov
Copy link

sashkopotapov commented Aug 5, 2020

You just need to skip x values

var values: [ChartDataEntry] = []
let entry1 = ChartDataEntry(x: 0, y: 1)
let entry2 = ChartDataEntry(x: 1, y: 2)
let entry3 = ChartDataEntry(x: 4, y: 3)
let entry4 = ChartDataEntry(x: 5, y: 4)

values.append(contentsOf: [entry1, entry2, entry3, entry4])

@StrauneanuDimitri
Copy link

@sashkopotapov - Hi. Your subclass isn't working anymore. Do you have a working example? Thank you.

@JLPenaLopez
Copy link

JLPenaLopez commented Jan 28, 2022

Hi, Someone knows if currently there is already an official solution for this requirement within the library?

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

8 participants