How do I create a quadrant axis chart in iOS with Swift?

Hello,

I'm trying to have a quadrant chart in my application to present some data. I have searched on the internet and all the graphs library I found are designed for a line chart or bar. I didn't find anything for quadrant chart except for Anychart and it is not fully implemented in IOS.

The graph is relatively simple but unfortunately, couldn't find one thing related to this graph 😟

The quadrant chart should look like this:
https://i.udemycdn.com/redactor/raw/2019-10-24_17-48-03-08fafa5567d2cb66fe788a19eb9d848d.png

Accepted Reply

What is it you do not succeed in doing ?


That's pretty easy to code it by yourself.


You subclass UIView (ChartView) and draw the graph:

- The colored quadrants

- the axes and grid lines

- then draw the points : dataPoints is set from the ViewController ; values must be scaled to have their coordinates between -1 and +1


You set the dataPoints from the calling viewController: either at viewWillLayout or in the action of a 'draw it!" button.

Data values must be scalld to be in[-1.0 ... +1.0] range

if minXValue and maxXValue are the min and max x-values of your points, scale each rawXValue with

let scaledXValue = -1.0 + 2.0 * (rawXValue - minXValue) / (maxXValue - minXValue)

- idem for Y axis


The class structure would be similar to this (just to give the idea):


import UIKit

struct DataPoint {
    var pos: CGPoint     // Must be in [-1.0...1.0] range
    var label: String
}

class ChartView: UIView {

    var dataPoints :[DataPoint]?     // To be loaded in ViewController

    override func draw(_ rect: CGRect) {
        // Just for showing some data: initialize some data here ; this must be done in the ViewController
        // y axis starts from top
        let dataOne = DataPoint(pos: CGPoint(x: 0.4, y: 0.5), label: "QAT")
        let dataTwo = DataPoint(pos: CGPoint(x: 0.9, y: -0.2), label: "SWE")
        let dataThree = DataPoint(pos: CGPoint(x: 0.05, y: 0), label: "BEU")
        dataPoints = [dataOne, dataTwo, dataThree]

        // Drawing code

        // Set general values
        let margin : CGFloat = 5.0
        let drawingArea = self.bounds.insetBy(dx: margin, dy: margin) // Keep some marging around quadrants
    
        let fullWidth = drawingArea.width
        let fullHeight = drawingArea.height
        let innerMidWidth = fullWidth / 2.0
        let innerMidHeight = fullHeight / 2.0
        let midDrawX = margin + innerMidWidth
        let midDrawY = margin + innerMidHeight
    
        let drawLeftMost = margin
        let drawRightMost = fullWidth + margin
        let drawTopMost = margin
        let drawBottomMost = fullHeight + margin

        // Draw the quadrants background
        // Take care to the orientation of y axis
        let quadBottomLeft = CGRect(x: drawLeftMost, y: midDrawY, width: innerMidWidth, height: innerMidHeight)
        let quadTopLeft = CGRect(x: drawLeftMost, y: drawTopMost, width: innerMidWidth, height: innerMidHeight)
        let quadBottomRight = CGRect(x: midDrawX, y: midDrawY, width: innerMidWidth, height: innerMidHeight)
        let quadTopRight = CGRect(x: midDrawX, y: drawTopMost, width: innerMidWidth, height: innerMidHeight)

        var quadPath = UIBezierPath(rect: quadTopLeft)
        UIColor.yellow.setFill()
        quadPath.fill()
        quadPath = UIBezierPath(rect: quadBottomLeft)
        UIColor.lightGray.setFill()
        quadPath.fill()
        quadPath = UIBezierPath(rect: quadTopRight)
        UIColor.lightGray.setFill()
        quadPath.fill()
        quadPath = UIBezierPath(rect: quadBottomRight)
        UIColor.lightGray.setFill()
        quadPath.fill()

        // Draw horizontal grid lines:
        for i in 1...4 {    // Border frame draws later
            let y = drawTopMost + CGFloat(i) * fullHeight / 5.0
            let startPoint = CGPoint(x: drawLeftMost, y: y)
            let destPoint = CGPoint(x: drawRightMost, y: y)
         
            let aPath = UIBezierPath()
            aPath.move(to: startPoint)
            aPath.addLine(to: destPoint)
            UIColor.red.setStroke()
            aPath.stroke()
        }
        // Draw vertical grid lines
        for j in 1...9 {    // Border frame draws later
            let x = drawLeftMost + CGFloat(j) * fullWidth / 10.0
            let startPoint = CGPoint(x: x, y: drawTopMost)
            let destPoint = CGPoint(x: x, y: drawBottomMost)
         
            let aPath = UIBezierPath()
            aPath.move(to: startPoint)
            aPath.lineWidth = 0.5
            aPath.addLine(to: destPoint)
            UIColor.red.setStroke()
            aPath.stroke()
        }

        // Draw frame and axis

        let framePath = UIBezierPath(rect: drawingArea)
        framePath.lineWidth = 2.0
        UIColor.black.setStroke()
        framePath.stroke()
      
        let axisPath = UIBezierPath()
        // x axis first, with the labels
        axisPath.move(to: CGPoint(x: drawLeftMost, y: midDrawY))
        axisPath.lineWidth = 2.0
        axisPath.addLine(to: CGPoint(x: drawRightMost, y: midDrawY))
        UIColor.black.setStroke()
        axisPath.stroke()
        for j in 0...10 {
            var scalePoint = CGPoint.zero
            scalePoint.x = -10 + CGFloat(j) * fullWidth / 10.0
            scalePoint.y = midDrawY - fullHeight / 15.0
            let value = (j - 5) * 20
            let textToDraw = "\(value)%"
            textToDraw.draw(at: scalePoint)
        }
      
        // y axis now
        axisPath.move(to: CGPoint(x: midDrawX, y: drawTopMost))
        axisPath.addLine(to: CGPoint(x: midDrawX, y: drawBottomMost))
        axisPath.stroke()
        for i in 0...5 {
            var scalePoint = CGPoint.zero
            scalePoint.x = midDrawX - 30
            scalePoint.y = -4 + CGFloat(i) * fullHeight / 5.0
            let value = (2*i - 5) * 20
            let textToDraw = "\(value)%"
            textToDraw.draw(at: scalePoint)
        }

        // Draw data points
        if let dataPoints = dataPoints {
            for dataPoint in dataPoints {
                var drawPoint = CGPoint.zero
                drawPoint.x = midDrawX + midDrawX * dataPoint.pos.x // pos.x is in [-1...+1] range
                drawPoint.y = midDrawY + midDrawY * dataPoint.pos.y // pos.y is in [-1...+1] range ; y axis: -1.0 is top, 1.0 bottom
                let dotRect = CGRect(x: drawPoint.x - 3, y: drawPoint.y - 3, width: 6.0, height: 6.0)
                let dotPath = UIBezierPath(ovalIn: dotRect)
                UIColor.red.setFill()
                dotPath.fill()
                let textToDraw = dataPoint.label
                textToDraw.draw(at: drawPoint)
            }
        }

}

Replies

What is it you do not succeed in doing ?


That's pretty easy to code it by yourself.


You subclass UIView (ChartView) and draw the graph:

- The colored quadrants

- the axes and grid lines

- then draw the points : dataPoints is set from the ViewController ; values must be scaled to have their coordinates between -1 and +1


You set the dataPoints from the calling viewController: either at viewWillLayout or in the action of a 'draw it!" button.

Data values must be scalld to be in[-1.0 ... +1.0] range

if minXValue and maxXValue are the min and max x-values of your points, scale each rawXValue with

let scaledXValue = -1.0 + 2.0 * (rawXValue - minXValue) / (maxXValue - minXValue)

- idem for Y axis


The class structure would be similar to this (just to give the idea):


import UIKit

struct DataPoint {
    var pos: CGPoint     // Must be in [-1.0...1.0] range
    var label: String
}

class ChartView: UIView {

    var dataPoints :[DataPoint]?     // To be loaded in ViewController

    override func draw(_ rect: CGRect) {
        // Just for showing some data: initialize some data here ; this must be done in the ViewController
        // y axis starts from top
        let dataOne = DataPoint(pos: CGPoint(x: 0.4, y: 0.5), label: "QAT")
        let dataTwo = DataPoint(pos: CGPoint(x: 0.9, y: -0.2), label: "SWE")
        let dataThree = DataPoint(pos: CGPoint(x: 0.05, y: 0), label: "BEU")
        dataPoints = [dataOne, dataTwo, dataThree]

        // Drawing code

        // Set general values
        let margin : CGFloat = 5.0
        let drawingArea = self.bounds.insetBy(dx: margin, dy: margin) // Keep some marging around quadrants
    
        let fullWidth = drawingArea.width
        let fullHeight = drawingArea.height
        let innerMidWidth = fullWidth / 2.0
        let innerMidHeight = fullHeight / 2.0
        let midDrawX = margin + innerMidWidth
        let midDrawY = margin + innerMidHeight
    
        let drawLeftMost = margin
        let drawRightMost = fullWidth + margin
        let drawTopMost = margin
        let drawBottomMost = fullHeight + margin

        // Draw the quadrants background
        // Take care to the orientation of y axis
        let quadBottomLeft = CGRect(x: drawLeftMost, y: midDrawY, width: innerMidWidth, height: innerMidHeight)
        let quadTopLeft = CGRect(x: drawLeftMost, y: drawTopMost, width: innerMidWidth, height: innerMidHeight)
        let quadBottomRight = CGRect(x: midDrawX, y: midDrawY, width: innerMidWidth, height: innerMidHeight)
        let quadTopRight = CGRect(x: midDrawX, y: drawTopMost, width: innerMidWidth, height: innerMidHeight)

        var quadPath = UIBezierPath(rect: quadTopLeft)
        UIColor.yellow.setFill()
        quadPath.fill()
        quadPath = UIBezierPath(rect: quadBottomLeft)
        UIColor.lightGray.setFill()
        quadPath.fill()
        quadPath = UIBezierPath(rect: quadTopRight)
        UIColor.lightGray.setFill()
        quadPath.fill()
        quadPath = UIBezierPath(rect: quadBottomRight)
        UIColor.lightGray.setFill()
        quadPath.fill()

        // Draw horizontal grid lines:
        for i in 1...4 {    // Border frame draws later
            let y = drawTopMost + CGFloat(i) * fullHeight / 5.0
            let startPoint = CGPoint(x: drawLeftMost, y: y)
            let destPoint = CGPoint(x: drawRightMost, y: y)
         
            let aPath = UIBezierPath()
            aPath.move(to: startPoint)
            aPath.addLine(to: destPoint)
            UIColor.red.setStroke()
            aPath.stroke()
        }
        // Draw vertical grid lines
        for j in 1...9 {    // Border frame draws later
            let x = drawLeftMost + CGFloat(j) * fullWidth / 10.0
            let startPoint = CGPoint(x: x, y: drawTopMost)
            let destPoint = CGPoint(x: x, y: drawBottomMost)
         
            let aPath = UIBezierPath()
            aPath.move(to: startPoint)
            aPath.lineWidth = 0.5
            aPath.addLine(to: destPoint)
            UIColor.red.setStroke()
            aPath.stroke()
        }

        // Draw frame and axis

        let framePath = UIBezierPath(rect: drawingArea)
        framePath.lineWidth = 2.0
        UIColor.black.setStroke()
        framePath.stroke()
      
        let axisPath = UIBezierPath()
        // x axis first, with the labels
        axisPath.move(to: CGPoint(x: drawLeftMost, y: midDrawY))
        axisPath.lineWidth = 2.0
        axisPath.addLine(to: CGPoint(x: drawRightMost, y: midDrawY))
        UIColor.black.setStroke()
        axisPath.stroke()
        for j in 0...10 {
            var scalePoint = CGPoint.zero
            scalePoint.x = -10 + CGFloat(j) * fullWidth / 10.0
            scalePoint.y = midDrawY - fullHeight / 15.0
            let value = (j - 5) * 20
            let textToDraw = "\(value)%"
            textToDraw.draw(at: scalePoint)
        }
      
        // y axis now
        axisPath.move(to: CGPoint(x: midDrawX, y: drawTopMost))
        axisPath.addLine(to: CGPoint(x: midDrawX, y: drawBottomMost))
        axisPath.stroke()
        for i in 0...5 {
            var scalePoint = CGPoint.zero
            scalePoint.x = midDrawX - 30
            scalePoint.y = -4 + CGFloat(i) * fullHeight / 5.0
            let value = (2*i - 5) * 20
            let textToDraw = "\(value)%"
            textToDraw.draw(at: scalePoint)
        }

        // Draw data points
        if let dataPoints = dataPoints {
            for dataPoint in dataPoints {
                var drawPoint = CGPoint.zero
                drawPoint.x = midDrawX + midDrawX * dataPoint.pos.x // pos.x is in [-1...+1] range
                drawPoint.y = midDrawY + midDrawY * dataPoint.pos.y // pos.y is in [-1...+1] range ; y axis: -1.0 is top, 1.0 bottom
                let dotRect = CGRect(x: drawPoint.x - 3, y: drawPoint.y - 3, width: 6.0, height: 6.0)
                let dotPath = UIBezierPath(ovalIn: dotRect)
                UIColor.red.setFill()
                dotPath.fill()
                let textToDraw = dataPoint.label
                textToDraw.draw(at: drawPoint)
            }
        }

}

To be completly honest, I was not expecting such a detailed and useful reply like this!


Thank you so much for all your help and explaination. I just tested it out and it worked!


I will tweak the code more and add more features on it.


I wish that you a very nice day like you made my day.

Enjoy and add new features. There are a lot to do to imporove.


Where are you based, if I may ask ?

I'm based in Germany atm.


What about you?

I am based in Toulouse (that should show on my card in the forum).


I've found a few slight errors that skew the drawing a little:


line 26 to 31: need some comments and rename some var to be more readable:

        let fullDrawingWidth = drawingArea.width   // self.bounds.width - 2 * margin
        let fullDrawingHeight = drawingArea.height // self.bounds.height - 2 * margin
        let innerMidWidth = fullDrawingWidth / 2.0  // half width of drawing area
        let innerMidHeight = fullDrawingHeight / 2.0
        let midDrawX = margin + innerMidWidth   // x-middle of drawing area: self.bounds.width / 2
        let midDrawY = margin + innerMidHeight  // y-middle of drawing area: self.bounds.height / 2


Lines 98 to 105: some small position errors and don't have to draw 0%

        for j in 0...10 {
            var scalePoint = CGPoint.zero
            scalePoint.x = drawLeftMost - 10 + CGFloat(j) * fullDrawingWidth / 10.0 // -10 to center on grid line
            scalePoint.y = midDrawY - 5 - fullDrawingHeight / 15.0 // -5 to draw above x axis
            let value = (j - 5) * 20
            let textToDraw = value == 0 ? "" : "\(value)%"
            textToDraw.draw(at: scalePoint)
        }


Idem for 111 to 118

        for i in 0...5 {
            var scalePoint = CGPoint.zero
            scalePoint.x = midDrawX - 30
            scalePoint.y = drawTopMost - 8 + CGFloat(i) * fullDrawingHeight / 5.0 // -4 to center on grid line
            let value = (2*i - 5) * 20
            let textToDraw = "\(value)%"
            textToDraw.draw(at: scalePoint)
        }


And finally, lines 121 to 133, the dots were shifting to the right a few pixels (QAT was not exactly on grid for instance)

        if let dataPoints = dataPoints {
            for dataPoint in dataPoints {
                var drawPoint = CGPoint.zero
                drawPoint.x = midDrawX + innerMidWidth * dataPoint.pos.x // pos.x is in [-1...+1] range
                drawPoint.y = midDrawY + innerMidHeight * dataPoint.pos.y // pos.y is in [-1...+1] range ; y axis: -1.0 is top, 1.0 bottom
                let dotRect = CGRect(x: drawPoint.x - 3, y: drawPoint.y - 3, width: 6.0, height: 6.0)
                let dotPath = UIBezierPath(ovalIn: dotRect)
                UIColor.red.setFill()
                dotPath.fill()
                let textToDraw = dataPoint.label
                textToDraw.draw(at: drawPoint)
            }
        }

It is cleaner like this.

This is what I was working on yesterday to fix. Thank you!


Also, I had one problem where the values of the axis ( 0 , 20% , 40 ..etc) are placed above the axis. I changed line 111 to:


            scalePoint.y = midDrawY - 5 - fullDrawingHeight / 45.0 // 45 instead of 15.0


Going to implement x&y axis title and add more functions as well.


I really appreciate your help more than you imagine! it was really helpful.

I implemented the x & y title ( just have to rotate the text 90 degrees) but I'll leave it for tomorrow.


One last question, do you have any useful resource to read/learn so I can add a zoom feature to the graph? Just to know where to go from here.

To add a zoom, I would just change the frame size of QuadrantView and call setNewDisplay. As the view content is parametric, view will zoom (but text will remain the same size which is probably good).

And of course, put it in a scrollView so that user can move the zoomed part.

The layout constraints should be on the scrollView, not on the QuadrantView.


Here is a small test I did:


Define the scrollView.

With constraints in IB for the scrollView:

- turn off Content Layout Guide

- Set leading, trailing, top and bottom to Safe area constraints

- Select colored background (just to see)


- Drag the QuadrantView inside the scrollView and position it.

Set its constraints for width and height


in the viewController,

- declare IBOutlets for the scrollView, for the width and height constraints of the QuadrantChartView

- create a zoom In and zoom Out buttons (will be better to have a stepper)

Connect to their IBActions.


Here is the code in controller, nothing to change in QuadrantView (Magic !)



import UIKit

class ChartViewController: UIViewController {

    @IBOutlet weak var quadrantScrollView: UIScrollView!

    @IBOutlet weak var quadrantChartView: QuadrantChartView!

    @IBOutlet weak var quadrantWidthConstraint: NSLayoutConstraint!

    @IBOutlet weak var quadrantHeightConstraint: NSLayoutConstraint!

    override func viewDidLoad() {
        super.viewDidLoad()


        // Do any additional setup after loading the view.
        quadrantScrollView.contentSize = quadrantChartView.frame.size
   
        quadrantScrollView.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth, UIView.AutoresizingMask.flexibleHeight]
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()


        quadrantChartView.setNeedsDisplay()
    }

    @IBAction func zoomInChart(_ sender: UIButton) {
   
        quadrantWidthConstraint.constant *= 1.25
        quadrantHeightConstraint.constant *= 1.25
        let oldContentSize = quadrantChartView.frame.size
        let newContentSize = CGSize(width: 1.25 * oldContentSize.width, height: 1.25 * oldContentSize.height)
        quadrantScrollView.contentSize = newContentSize
        quadrantChartView.setNeedsDisplay()
    }

    @IBAction func zoomOutChart(_ sender: UI1.B5utton) {
        quadrantWidthConstraint.constant *= 0.8
        quadrantHeightConstraint.constant *= 0.8
        let oldContentSize = quadrantChartView.frame.size
        let newContentSize = CGSize(width: 0.8 * oldContentSize.width, height: 0.8 * oldContentSize.height)
        quadrantScrollView.contentSize = newContentSize
        quadrantChartView.setNeedsDisplay()
    }

}

Or some slightly refined version ; works in landscape as well


import UIKit

class ChartViewController: UIViewController {

    @IBOutlet weak var quadrantScrollView: UIScrollView!
    @IBOutlet weak var quadrantChartView: QuadrantChartView!
    @IBOutlet weak var zoomInButton: UIButton!    // Button ; useless with stepper
    @IBOutlet weak var zoomFactorLabel: UILabel!
    @IBOutlet weak var zoomOutButton: UIButton!   // Button ; useless with stepper
    @IBOutlet weak var zoomStepper: UIStepper!  // ranges from -2 to 4 ; step value is 1
    @IBOutlet weak var quadrantWidthConstraint: NSLayoutConstraint!  // quadrantChartView height constraint
    @IBOutlet weak var quadrantHeightConstraint: NSLayoutConstraint! // quadrantChartView width constraint
    @IBOutlet weak var quadrantLeadingConstraint: NSLayoutConstraint!   // leading to its superview 4 in xib ; to center horizontally inside ScrollView

    private var standardQuadrantWidthConstraint: CGFloat!   // xib value
    private var standardQuadrantHeightConstraint: CGFloat!   // xib value
    private var standardQuadrantChartSize : CGSize!   // xib values

    private var zoom = 0   { // +1 to +4 if zoom in, -1 to -2 zoom out
        didSet {
            if zoom >= 0 {
                zoomFactor = 1.0 + 0.25 * CGFloat(zoom) // 125, 150…
            } else {
                zoomFactor = 1.0 + 0.2 * CGFloat(zoom)  // 80, 60
            }
            zoomInButton.isEnabled = zoom < 4
            zoomOutButton.isEnabled = zoom > -2
            zoomStepper.value = Double(zoom)
        }
    }
    private var zoomFactor : CGFloat = 1.0 {
        didSet {
            zoomFactorLabel.text = String(format: "%.0f", 100 * zoomFactor) + "%"
            quadrantWidthConstraint.constant = standardQuadrantWidthConstraint * zoomFactor
            quadrantHeightConstraint.constant = standardQuadrantHeightConstraint * zoomFactor
            let newContentSize = CGSize(width: zoomFactor * standardQuadrantChartSize.width, height: zoomFactor * standardQuadrantChartSize.height)
            quadrantScrollView.contentSize = newContentSize
            quadrantChartView.setNeedsDisplay()
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
        standardQuadrantChartSize = quadrantChartView.frame.size    // Initial size, from xib
        quadrantScrollView.contentSize = standardQuadrantChartSize    // Initial size, from xib
        standardQuadrantWidthConstraint = quadrantWidthConstraint.constant  // Initial size, from xib
        standardQuadrantHeightConstraint = quadrantHeightConstraint.constant    // Initial size, from xib
    
        quadrantScrollView.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth, UIView.AutoresizingMask.flexibleHeight]
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        let extraWidth = quadrantScrollView.frame.width - quadrantScrollView.contentSize.width

        if extraWidth > 8 {
            quadrantLeadingConstraint.constant = extraWidth / 2
        } else {
            quadrantLeadingConstraint.constant = 4
        }

        quadrantChartView.setNeedsDisplay()
    }

    @IBAction func zoomStepper(_ sender: UIStepper) {   // Redundant with buttons
        zoom = Int(sender.value)
    }

    @IBAction func zoomInChart(_ sender: UIButton) {
    
        if zoom >= 4 { return }
        zoom += 1
    }

    @IBAction func zoomOutChart(_ sender: UIButton) {
        if zoom <= -2 { return }
        zoom -= 1
    }

}

I really don't know how to thank you!


I'm testing it out and I have no words to describe how greateful I'm.

If you want bto make this UIView class easier to reuse and customize, here is a nice thing to do: make it IBDesignable (which means you can set more properties from IB).


Add the following declarations in QuadrantChartView:


@IBDesignable class QuadrantChartView: UIView {
 
    @IBInspectable var quad1Color: UIColor = .yellow     // yellow will be the default color
    @IBInspectable var quad2Color: UIColor = .lightGray
    @IBInspectable var quad3Color: UIColor = .lightGray
    @IBInspectable var quad4Color: UIColor = .lightGray
    @IBInspectable var dotColor: UIColor = .red
    @IBInspectable var subLineColor: UIColor = .red

    var dataPoints :[DataPoint]?    // To be loaded in ViewController


Change some code in draw to use those new properties instead of fixed colors:

        // Draw the quadrants background
        // Take care to the orientation of y axis
        let quadBottomLeft = CGRect(x: drawLeftMost, y: midDrawY, width: innerMidWidth, height: innerMidHeight)
        let quadTopLeft = CGRect(x: drawLeftMost, y: drawTopMost, width: innerMidWidth, height: innerMidHeight)
        let quadBottomRight = CGRect(x: midDrawX, y: midDrawY, width: innerMidWidth, height: innerMidHeight)
        let quadTopRight = CGRect(x: midDrawX, y: drawTopMost, width: innerMidWidth, height: innerMidHeight)
       
        var quadPath = UIBezierPath(rect: quadTopLeft)
        quad1Color.setFill() // UIColor.yellow.setFill()
        quadPath.fill()
       
        quadPath = UIBezierPath(rect: quadBottomLeft)
        quad2Color.setFill() //UIColor.lightGray.setFill()
        quadPath.fill()
       
        quadPath = UIBezierPath(rect: quadTopRight)
        quad3Color.setFill() // UIColor.lightGray.setFill()
        quadPath.fill()
       
        quadPath = UIBezierPath(rect: quadBottomRight)
        quad4Color.setFill()// UIColor.lightGray.setFill()
        quadPath.fill()
       
        // Draw horizontal grid lines:
        for i in 1...4 {    // Border frame draws later
            let y = drawTopMost + CGFloat(i) * fullDrawingHeight / 5.0
            let startPoint = CGPoint(x: drawLeftMost, y: y)
            let destPoint = CGPoint(x: drawRightMost, y: y)
           
            let aPath = UIBezierPath()
            aPath.move(to: startPoint)
            aPath.addLine(to: destPoint)
            subLineColor.setStroke()        // UIColor.red.setStroke()
            aPath.stroke()
        }
        // Draw vertical grid lines
        for j in 1...9 {    // Border frame draws later
            let x = drawLeftMost + CGFloat(j) * fullDrawingWidth / 10.0
            let startPoint = CGPoint(x: x, y: drawTopMost)
            let destPoint = CGPoint(x: x, y: drawBottomMost)

            let aPath = UIBezierPath()
            aPath.move(to: startPoint)
            aPath.lineWidth = 0.5
            aPath.addLine(to: destPoint)
            subLineColor.setStroke()        // UIColor.red.setStroke()
            aPath.stroke()
        }


Do the same for dataPoints:

        // Draw data points
        if let dataPoints = dataPoints {
            for dataPoint in dataPoints {
                let textToDraw = dataPoint.label
                let sizeOfText = (textToDraw as NSString).size(withAttributes: attributes)
               
                var drawPoint = CGPoint.zero
                drawPoint.x = midDrawX + innerMidWidth * dataPoint.pos.x // pos.x is in [-1...+1] range
                drawPoint.y = midDrawY + innerMidHeight * dataPoint.pos.y // pos.y is in [-1...+1] range ; y axis: -1.0 is top, 1.0 bottom
                let dotRect = CGRect(x: drawPoint.x - 3, y: drawPoint.y - 3, width: 6.0, height: 6.0)
                let dotPath = UIBezierPath(ovalIn: dotRect)
                dotColor.setFill()  // UIColor.red.setFill()
                dotPath.fill()
               
                if drawPoint.x + sizeOfText.width > drawRightMost + 4 {
                    drawPoint.x = drawRightMost - sizeOfText.width + 4
                }
                if drawPoint.y + sizeOfText.height > drawBottomMost + 4 {
                    drawPoint.y = drawBottomMost - sizeOfText.height
                }

                textToDraw.draw(at: drawPoint, withAttributes: attributes)
            }
        }

Now, in IB, select the QuadrantView in its viewController. Open the Attributes Inspector.

You should see the new properties appear at top.

Select the colors as you want, instead of default.

And look at the QuadrantView in the ViewController…


That's very handy.

Now you can add other properties, like axisColor or axisThickness…

Thank you!


I actually managed to zoom in the graph by the gesture and using implementing this fucntion:


func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return quadrantChartView
    }

Just to make sure that I tested it out correctly, the width is not getting enlarged. On the other hand, the height is enlarding.


So, lets say we have 2 points next to each others on the X-Axis, by zooming in, they will be still next to each others. But, if two points are next to each others on the Y-Axis, they get away from each others.


Isn't should change the width as well because "newContentSize " is for new width and height as well?


** Update:


It works if the if condition for the extra is changed to:

if extraWidth < 8 {


I noticed that the width is in the nagtive value thou

As I do not get the details of how you implemented the gesture, difficult to say.


So, lets say we have 2 points next to each others on the X-Axis, by zooming in, they will be still next to each others. But, if two points are next to each others on the Y-Axis, they get away from each others.


Do you mean you want to zoom independantly in each direction ?