Browse Source

Fixes #4099: Line renderer did not render lines if their coordinates fell outside of the viewport. (#4100)

* Fixed #4099: Line renderer did not render lines if they coordinates fell outside of the viewport, even though they might intersect the viewport.

* Updated inline documentation.

* Implemented code review feedback and removed unnecessary checks for performance reasons.

* Simplified and clarified the linear function to check for collisions with the left, top and bottom edges of the view port, and commented out the unecessary logic that checks for collision with the right edge of the view port.

* Updated in-line documentation.

* Update ViewPortHandler.swift

add a check for vertical line and a few comments change
Jeroen Wesbeek 5 years ago
parent
commit
9429f5c97e

+ 13 - 10
Source/Charts/Renderers/LineChartRenderer.swift

@@ -354,17 +354,20 @@ open class LineChartRenderer: LineRadarRenderer
                 _lineSegments[i] = _lineSegments[i].applying(valueToPixelMatrix)
                 _lineSegments[i] = _lineSegments[i].applying(valueToPixelMatrix)
             }
             }
             
             
-            if (!viewPortHandler.isInBoundsRight(_lineSegments[0].x))
-            {
-                break
-            }
+            // Determine the start and end coordinates of the line, and make sure they differ.
+            guard
+                let firstCoordinate = _lineSegments.first,
+                let lastCoordinate = _lineSegments.last,
+                firstCoordinate != lastCoordinate else { continue }
             
             
-            // 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))
-            {
-                continue
-            }
+            // If both points lie left of viewport, skip stroking.
+            if !viewPortHandler.isInBoundsLeft(lastCoordinate.x) { continue }
+            
+            // If both points lie right of the viewport, break out early.
+            if !viewPortHandler.isInBoundsRight(firstCoordinate.x) { break }
+            
+            // Only stroke the line if it intersects with the viewport.
+            guard viewPortHandler.isIntersectingLine(from: firstCoordinate, to: lastCoordinate) else { continue }
             
             
             // get the color that is set for this line-segment
             // get the color that is set for this line-segment
             context.setStrokeColor(dataSet.color(atIndex: j).cgColor)
             context.setStrokeColor(dataSet.color(atIndex: j).cgColor)

+ 57 - 0
Source/Charts/Utils/ViewPortHandler.swift

@@ -416,6 +416,17 @@ open class ViewPortHandler: NSObject
         return isInBoundsTop(y) && isInBoundsBottom(y)
         return isInBoundsTop(y) && isInBoundsBottom(y)
     }
     }
     
     
+    /**
+     A method to check whether coordinate lies within the viewport.
+     
+     - Parameters:
+         - point: a coordinate.
+     */
+    @objc open func isInBounds(point: CGPoint) -> Bool
+    {
+        return isInBounds(x: point.x, y: point.y)
+    }
+    
     @objc open func isInBounds(x: CGFloat, y: CGFloat) -> Bool
     @objc open func isInBounds(x: CGFloat, y: CGFloat) -> Bool
     {
     {
         return isInBoundsX(x) && isInBoundsY(y)
         return isInBoundsX(x) && isInBoundsY(y)
@@ -443,6 +454,52 @@ open class ViewPortHandler: NSObject
         return (_contentRect.origin.y + _contentRect.size.height) >= normalizedY
         return (_contentRect.origin.y + _contentRect.size.height) >= normalizedY
     }
     }
     
     
+    /**
+     A method to check whether a line between two coordinates intersects with the view port  by using a linear function.
+     
+        Linear function (calculus): `y = ax + b`
+            
+        Note: this method will not check for collision with the right edge of the view port, as we assume lines run from left
+        to right (e.g. `startPoint < endPoint`).
+     
+     - Parameters:
+        - startPoint: the start coordinate of the line.
+        - endPoint: the end coordinate of the line.
+     */
+    @objc open func isIntersectingLine(from startPoint: CGPoint, to endPoint: CGPoint) -> Bool
+    {
+        // If start- and/or endpoint fall within the viewport, bail out early.
+        if isInBounds(point: startPoint) || isInBounds(point: endPoint) { return true }
+        // check if x in bound when it's a vertical line
+        if startPoint.x == endPoint.x { return isInBoundsX(startPoint.x) }
+        
+        // Calculate the slope (`a`) of the line (e.g. `a = (y2 - y1) / (x2 - x1)`).
+        let a = (endPoint.y - startPoint.y) / (endPoint.x - startPoint.x)
+        // Calculate the y-correction (`b`) of the line (e.g. `b = y1 - (a * x1)`).
+        let b = startPoint.y - (a * startPoint.x)
+        
+        // Check for colission with the left edge of the view port (e.g. `y = (a * minX) + b`).
+        // if a is 0, it's a horizontal line; checking b here is still valid, as b is `point.y` all the time
+        if isInBoundsY((a * contentRect.minX) + b) { return true }
+
+        // Skip unnecessary check for collision with the right edge of the view port
+        // (e.g. `y = (a * maxX) + b`), as such a line will either begin inside the view port,
+        // or intersect the left, top or bottom edges of the view port. Leaving this logic here for clarity's sake:
+        // if isInBoundsY((a * contentRect.maxX) + b) { return true }
+        
+        // While slope `a` can theoretically never be `0`, we should protect against division by zero.
+        guard a != 0 else { return false }
+        
+        // Check for collision with the bottom edge of the view port (e.g. `x = (maxY - b) / a`).
+        if isInBoundsX((contentRect.maxY - b) / a) { return true }
+        
+        // Check for collision with the top edge of the view port (e.g. `x = (minY - b) / a`).
+        if isInBoundsX((contentRect.minY - b) / a) { return true }
+
+        // This line does not intersect the view port.
+        return false
+    }
+    
     /// The current x-scale factor
     /// The current x-scale factor
     @objc open var scaleX: CGFloat
     @objc open var scaleX: CGFloat
     {
     {