“This is the 22nd day of my participation in the Gwen Challenge in November. Check out the details: The Last Gwen Challenge in 2021.”

preface

As a front-end coder, I saw this picture six months ago and cried out how cool it could be!

If only I could develop data visualizations like this, so I set up a flag.

Finally today I want to honor my flag!

Let’s start with a wave of concepts

Before we start coding, let’s familiarize ourselves with some of SwiftUI’s apis for drawing graphics and special effects.

  1. Draw a rectangle with rounded corners
RoundedRectangle(cornerRadius: 4)
Copy the code
  1. Fill the shape with a color or gradient.
public func fill<S> (_ content: S.style: FillStyle = FillStyle()) -> some View where S : ShapeStyle
Copy the code
  1. Scale the view according to the given size and anchor point
public func scaleEffect(_ scale: CGSize.anchor: UnitPoint = .center) -> some View
Copy the code
  1. animation
public func animation(_ animation: Animation?). -> some View
Copy the code
  1. shadow
public func shadow(color: Color = Color(.sRGBLinear, white: 0, opacity: 0.33), radius: CGFloat.x: CGFloat = 0.y: CGFloat = 0) -> some View
Copy the code
  1. Create a path
var path = Path(a)Copy the code
  1. Starts a new subpath at the specified point
public mutating func move(to p: CGPoint)
Copy the code
  1. Adds a quadratic Bezier curve to a path with specified endpoints and control points
public mutating func addQuadCurve(to p: CGPoint.control cp: CGPoint)
Copy the code
  1. Adds an arc to the path, specifying the radius and Angle
public mutating func addArc(center: CGPoint.radius: CGFloat.startAngle: Angle.endAngle: Angle.clockwise: Bool.transform: CGAffineTransform = .identity)
Copy the code
  1. Appends a line segment from the current point to the specified point
public mutating func addLine(to p: CGPoint)
Copy the code
  1. Close and complete the current subpath
public mutating func closeSubpath(a)
Copy the code
  1. Outline the shape using a color or gradient
public func stroke<S> (_ content: S.style: StrokeStyle) -> some View where S : ShapeStyle
Copy the code
  1. Rotate the render output of this view around the specified point
public func rotationEffect(_ angle: Angle.anchor: UnitPoint = .center) -> some View
Copy the code
  1. Rotate this view in three dimensions around the given rotation axis
public func rotation3DEffect(_ angle: Angle.axis: (x: CGFloat, y: CGFloat, z: CGFloat), anchor: UnitPoint = .center, anchorZ: CGFloat = 0.perspective: CGFloat = 1) -> some View
Copy the code
  1. Add gestures to the view
public func gesture<T> (_ gesture: T.including mask: GestureMask = .all) -> some View where T : Gesture
Copy the code

Code practice

Next, let’s start to use these apis to implement beautiful Chart!

A histogram

First, let’s start with a simple bar chart. The results are as follows:

Draw a rectangle with rounded corners. Here I need to use RoundedRectangle.

@frozen public struct RoundedRectangle : Shape {

    public var cornerSize: CGSize

    public var style: RoundedCornerStyle

    @inlinable public init(cornerSize: CGSize.style: RoundedCornerStyle = .circular)

    @inlinable public init(cornerRadius: CGFloat.style: RoundedCornerStyle = .circular)

    /// Describes this shape as a path within a rectangular frame of reference.
    ///
    /// - Parameter rect: The frame of reference for describing this shape.
    ///
    /// - Returns: A path that describes this shape.
    public func path(in rect: CGRect) -> Path

    /// The data to animate.
    public var animatableData: CGSize.AnimatableData

    /// The type defining the data to animate.
    public typealias AnimatableData = CGSize.AnimatableData

    /// The type of view representing the body of this view.
    ///
    /// When you create a custom view, Swift infers this type from your
    /// implementation of the required ``View/body-swift.property`` property.
    public typealias Body
}
Copy the code

RoundedRectangle generates a RoundedRectangle. RoundedRectangle inherits Shape, so you can use Shape to add effects to the RoundedRectangle.

RoundedRectangle(cornerRadius: 4)
Copy the code

Now that we know how to draw a rectangle, it’s no longer a problem to draw 10 or 20 rectangles, we just use a for loop to draw a certain number of rounded rectangles depending on the size of the incoming data.

GeometryReader { geometry in
            HStack(alignment: .bottom, spacing: getSpaceWidth(width: geometry.frame(in: .local).width - 20)) {
                ForEach(0..<self.data.count, id: \.self){ i in
                    .
                }
            }.padding([.top, .leading, .trailing], 20)}Copy the code

It’s not enough just to draw rectangles, but how do you make those rectangles look different depending on the incoming data? At this point, we need to use the scaleEffect scaling function, which is defined as follows:

@inlinable public func scaleEffect(_ scale: CGSize.anchor: UnitPoint = .center) -> some View
Copy the code

According to the definition of parameters, we only need to pass in the scale and anchor point, we can scale our rectangle, this API is used in 2D graphics, very often, very useful.

RoundedRectangle(cornerRadius: 4)
    .frame(width: CGFloat(self.cellWidth))
    .scaleEffect(CGSize(width: 1, height: self.scaleValue), anchor: .bottom)
    .onAppear {
        self.scaleValue = self.value
    }
    .animation(.spring().delay(self.touchLocation < 0 ? Double(self.index) * 0.04 : 0))
Copy the code

Next, it’s time to fill our rectangle with color! Here we use the fill function, which can be filled with either Color or Gradient. To look good we choose Gradient, of course. The LinearGradient object happens to draw the Gradient Color for us.

RoundedRectangle(cornerRadius: 4)
                    .fill(LinearGradient(gradient: gradient?.getGradient() ?? GradientColor(start: accentColor, end: accentColor).getGradient(), startPoint: .bottom, endPoint: .top))
                    .frame(width: CGFloat(self.cellWidth))
                    .scaleEffect(CGSize(width: 1, height: self.scaleValue), anchor: .bottom)
                    .onAppear {
                        self.scaleValue = self.value
                    }
                    .animation(.spring().delay(self.touchLocation < 0 ? Double(self.index) * 0.04 : 0))
Copy the code

Finally, to make our bar graph user experience better, we need to add gesture responses to it. Slide according to the gesture to get the coordinates on the screen, and then calculate the index in the data array according to the current coordinates, so as to get the value of the array, part of the code is as follows:

.gesture(DragGesture().onChanged({ value in
                self.touchLocation = value.location.x / self.formSize.width
                self.showValue = true
                self.currentValue = self.getCurrentValue()?.1 ?? 0
                if self.data.valuesGiven && self.formSize = = ChartForm.medium {
                    self.showLabelValue = true
                }
            }).onEnded({ value in
                self.showValue = false
                self.showLabelValue = false
                self.touchLocation = -1
            })
            )
            .gesture(TapGesture())
Copy the code

Due to space reasons, I will not put on the entire code, to see the source code, please go to: github.com/ShenJieSuzh…

The pie chart

Next, let’s move on to implementing the PieChart PieChart. Before we draw the pie chart, let’s draw a circle. SwiftUI provides the Path structure so that we can draw 2D graphics, so the code for drawing a circle is as follows:

var path: Path {
    var path = Path()
    path.addArc(center: CGPoint(x: 200, y: 200), radius: 100, startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 360), clockwise: false)
    return path
}
Copy the code

A circle should appear on your page, as shown below:

Did you suddenly realize something when you saw that? A pie chart is actually a circle, but it is composed of several large fan-shaped parts with the same center and radius. Therefore, through this feature, we can successively draw fan-shaped parts with different areas, and finally assemble these fan-shaped parts into a pie chart.

Here we need to use the apis provided by SwiftUI: Path, addArc, addLine and closeSubpath.

Next time to introduce it!

  1. Path is a structure provided by SwiftUI for drawing 2D graphics, which I call paths.

  2. The addArc function is defined as:

public mutating func addArc(center: CGPoint.radius: CGFloat.startAngle: Angle.endAngle: Angle.clockwise: Bool.transform: CGAffineTransform = .identity)
Copy the code

This function draws an arc based on a given center, radius, and Angle.

  1. The addLine function is defined as:
public mutating func addLine(to p: CGPoint)
Copy the code

This function draws a line from the current point to a given point.

  1. CloseSubpath is defined as:
public mutating func closeSubpath(a)
Copy the code

This function closes the Path we defined earlier, just as we cover the pen after writing.

Now that you are familiar with the concepts of these functions, let’s begin to draw pie charts. Since the pie chart shows the user the comparison of several large pieces of data, the data passed to it must be an array, so we can use the for loop to draw the pie chart in turn, the code is as follows:

var body: some View {
        GeometryReader { geometry in
            ZStack {
                ForEach(0..<self.slices.count) { i in
                    PieChartCell(rect: geometry.frame(in: .local), startDeg: self.slices[i].startDeg, endDeg: self.slices[i].endDeg, index: i, backgroundColor: self.backgroundColor, accentColor: self.accentColor)
                }
            }
        }
    }
Copy the code

In addition, we also need to calculate the starting Angle and ending Angle of each sector. Since it is a pie chart, we need to use a 360 degree Angle as the benchmark to calculate the data of each sector in turn. The code is as follows:

var slices: [PieSlice] {
        var tempSlices:[PieSlice] = []
        var lastEndDeg: Double = 0
        let maxValue = data.reduce(0.+)
        for slice in data {
            let normalized: Double = Double(slice) / Double(maxValue)
            let startDeg = lastEndDeg
            let endDeg = lastEndDeg + (normalized * 360)
            lastEndDeg = endDeg
            tempSlices.append(PieSlice(startDeg: startDeg, endDeg: endDeg, value: slice, normalizedValue: normalized))
        }
        return tempSlices
    }
Copy the code

After the Angle calculation is complete, the fan can be drawn. Here I will paste the code directly, which is relatively easy. In addition, some of its special effects functions are also mentioned in the drawing of the bar chart above.

struct PieChartCell: View {
    @State private var show: Bool = false
    var rect: CGRect
    var radius: CGFloat {
        return min(rect.width, rect.height) / 2
    }
    var startDeg: Double
    var endDeg: Double
    var path: Path {
        var path = Path()
        path.addArc(center: rect.mid, radius: self.radius, startAngle: Angle(degrees: self.startDeg), endAngle: Angle(degrees: self.endDeg), clockwise: false)
        path.addLine(to: rect.mid)
        path.closeSubpath()
        return path
    }
    
    var index: Int
    var backgroundColor: Color
    var accentColor: Color
    
    var body: some View {
        path.fill()
            .foregroundColor(self.accentColor)
            .overlay(path.stroke(self.backgroundColor, lineWidth: 2))
            .scaleEffect(self.show ? 1 : 0)
            .animation(Animation.spring().delay(Double(self.index) * 0.04))
            .onAppear {
                self.show = true}}}Copy the code

The operation effect is as follows:

The line chart

With bar charts and pie charts out of the way, it’s time to look at line charts.

See the effect above first!

Is it a little cool, then we will go step by step to achieve it!

First of all, SwiftUI’s Path structure is the best way to draw line charts. Since the distribution of a line chart is a series of points, we first calculate the points of the line chart based on the given data array.

Since we already know the data array, but it is only an array of type Double, we need to match each of its values to a CGPoint. How to do that? And then we look down.

We first calculate the proportion of x and y between each point. Since it is a broken line graph, the proportion between points on the X-axis should be equal. What needs to reflect the data difference is that points are different on the Y-axis.

var stepWidth: CGFloat {
        if data.points.count < 2 {
            return 0
        }
        return frame.size.width / CGFloat(data.points.count - 1)}Copy the code

The code for calculating the ratio on the Y-axis is as follows:

var stepHeight: CGFloat {
        var min: Double?
        var max: Double?
        let points = self.data.onlyPoints()
        if minDataValue ! = nil && maxDataValue ! = nil {
            min = minDataValue
            max = maxDataValue
        }else if let minPoint = points.min(), let maxPoint = points.max(), minPoint ! = maxPoint {
            min = minPoint
            max = maxPoint
        }else {
            return 0
        }
        
        if let min = min, let max = max, min ! = max{
            if min < = 0 {
                return (frame.size.height - padding) / CGFloat(max - min)
            }else {
                return (frame.size.height - padding) / CGFloat(max - min)
            }
        }
        
        return 0
    }
Copy the code

Now that we have this scaling relationship, we can figure out the coordinates of the specific point, the starting point;

var p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
Copy the code

The coordinates of the second point are:

let p2 = CGPoint(x: step.x * CGFloat(pointIndex), y: step.y*CGFloat(points[pointIndex]-offset))
Copy the code

Now that we have the coordinates of each point, we can use the method in the Path structure provided by SwiftUI to string the points together and draw a broken line.

But in order to beautiful, to draw on our renderings of the line with the bezier curve, we were drawing a straight line between two points, but after joined the bezier curve, it will be between the two of us to join an anchor point, and then through the anchor can bend our line, to let originally sharp wavy lines present a moderate effect, Similar to the pen tool in Photoshop, the corresponding API is:

ublic mutating func addQuadCurve(to p: CGPoint.control cp: CGPoint)
Copy the code

Use as shown in the code:

static func quadCurvedPathWithPoints(points: [Double].step:CGPoint.globalOffset: Double? = nil) -> Path {
        var path = Path(a)if (points.count < 2) {return path
        }
        
        let offset = globalOffset ?? points.min()!
        var p1 = CGPoint(x: 0, y: CGFloat(points[0]-offset)*step.y)
        path.move(to: p1)
        for pointIndex in 1..<points.count {
            let p2 = CGPoint(x: step.x * CGFloat(pointIndex), y: step.y*CGFloat(points[pointIndex]-offset))
            let midPoint = CGPoint.midPointForPoints(p1: p1, p2: p2)
            path.addQuadCurve(to: midPoint, control: CGPoint.controlPointForPoints(p1: midPoint, p2: p1))
            path.addQuadCurve(to: p2, control: CGPoint.controlPointForPoints(p1: midPoint, p2: p2))
            p1 = p2
        }
        return path
    }
Copy the code

Finally, it is time to add some visual effects to the creases. I feel that the effect of gradient can improve the visual effect very high, so the code is as follows:

self.closedPath
.fill(LinearGradient(gradient: gradient.getGradient(), startPoint: .bottom, endPoint: .top))
.rotationEffect(.degrees(90), anchor: .center)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
.transition(.opacity)
.animation(.easeIn(duration: 1.6))
    
self.path
.trim(from: 0, to: self.showFull ? 1:0)
.stroke(LinearGradient(gradient: gradient.getGradient(), startPoint: .leading, endPoint: .trailing), style: StrokeStyle(lineWidth: 3, lineJoin: .round))
.rotationEffect(.degrees(180), anchor: .center)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
.animation(.easeOut(duration: 1.2).delay(Double(self.index) * 0.4))
.onAppear {
    self.showFull = true
}
.onDisappear {
    self.showFull = false
}                    
                    
Copy the code

The last

Originally also want to write point, saw immediately to one o ‘clock in the morning, the first temporary code so much! My health matters. I have to work tomorrow. Sleeping ~

Source code address: github.com/ShenJieSuzh… Click 🌟!

Previous articles:

  • Binary tree brush summary: binary search tree properties
  • Binary tree summary: binary tree properties
  • Binary tree summary: binary tree modification and construction
  • StoreKit2 smells this good? Yeah, I tried it. It smells good
  • After reading this article, I am no longer afraid of being asked how to construct a binary tree.
  • The game guys are trying to get people to pay again. That’s bad!
  • Take you rolled a netease holding cloud music home | adapter
  • Netease Cloud Music Home Page (3)
  • Netease Cloud Music Home Page (2)
  • Netease Cloud Music Home Page (a)
  • Does the code need comments? Write and you lose
  • I would not study in Codable for a long time. They are for fun
  • IOS handles web data gracefully. Do you really? Why don’t you read this one
  • UICollectionView custom layout! This one is enough

Please drink a cup ☕️ + attention oh ~

  1. After reading, remember to give me a thumbs-up oh, there is 👍 power
  2. Follow the public number – HelloWorld Jie Shao, the first time push new posture

Finally, creation is not easy, if it is helpful to you, I hope you can praise and support, what questions can also be discussed in the comments section 😄 ~ **