在 SwiftUI 構建簡單的數據視覺化工具:圓餅圖 (Pie Chart)


本篇原文(標題:Data Visualization With SwiftUI: Pie Charts)刊登於作者 Medium,由 Jimmy M Andersson 所著,並授權翻譯及轉載。

Apple 在 2019 年推出了 SwiftUI,為我們提供了一個輕巧易用的工具,來創建使用者界面。這系列的教學文章,會讓大家看看如何利用 SwiftUI 框架,構建簡單而漂亮的數據視覺化工具 (data visualization tool)。在第三篇文章中,我們會介紹如何構建圓餅圖 (pie chart)。

什麼是圓餅圖?

圓餅圖(亦稱圓形圖)是一種數據可視化工具,它將每個數值按比例繪製一個圓形。就像長條圖一樣,圓餅圖可以處理分類數據,也可以展示不同類別之間的比率。

swiftui-pie-chart

這種視覺化工具適用於以下情況:

  • 看看你公司年度預算中,分配給各個部門的比例。
  • 了解自己如何分配月薪,像是支付房租、購買食物、和購買編程的書籍等方面。
  • 統計地區中不同種類房屋的出租比例。

我們需要什麼?

要製作一個圓餅圖,我們需要使用 SwiftUI 的 PathShape、和 ZStack 型別。由於我們在前兩篇文章已經介紹過這些概念,因此讓我們直接開始實作吧!

如何用程式碼構建圓餅圖?

讓我們先看一下主要組件:PieCart 結構 (struct)。

import SwiftUI

struct PieChart: View {
  @Binding var data: [Double]
  @Binding var labels: [String]
  
  private let colors: [Color]
  private let borderColor: Color
  private let sliceOffset: Double = -.pi / 2
  
  
  init(data: Binding<[Double]>, labels: Binding<[String]>, colors: [Color], borderColor: Color) {
    self._data = data
    self._labels = labels
    self.colors = colors
    self.borderColor = borderColor
  }
  
  var body: some View {
    GeometryReader { geo in
      ZStack(alignment: .center) {
        ForEach(0 ..< data.count) { index in
          PieSlice(startAngle: startAngle(for: index), endAngle: endAngle(for: index))
            .fill(colors[index % colors.count])
          
          PieSlice(startAngle: startAngle(for: index), endAngle: endAngle(for: index))
            .stroke(Color.white, lineWidth: 1)
          
          PieSliceText(
            title: "\(labels[index])",
            description: String(format: "$%.2f million", data[index])
          )
          .offset(textOffset(for: index, in: geo.size))
          .zIndex(1)
        }
      }
    }
  }
  
  private func startAngle(for index: Int) -> Double {
    switch index {
      case 0:
        return sliceOffset
      default:
        let ratio: Double = data[..<index].reduce(0.0, +) / data.reduce(0.0, +)
        return sliceOffset + 2 * .pi * ratio
    }
  }
  
  private func endAngle(for index: Int) -> Double {
    switch index {
      case data.count - 1:
        return sliceOffset + 2 * .pi
      default:
        let ratio: Double = data[..<(index + 1)].reduce(0.0, +) / data.reduce(0.0, +)
        return sliceOffset + 2 * .pi * ratio
    }
  }
  
  private func textOffset(for index: Int, in size: CGSize) -> CGSize {
    let radius = min(size.width, size.height) / 3
    let dataRatio = (2 * data[..<index].reduce(0, +) + data[index]) / (2 * data.reduce(0, +))
    let angle = CGFloat(sliceOffset + 2 * .pi * dataRatio)
    return CGSize(width: radius * cos(angle), height: radius * sin(angle))
  }
}

這個結構包含了許多輔助函式 (helper function),其中一些函式會做一些數學運算,乍看之下似乎有些嚇人,但讓我們逐一看看吧!

由於我們將數據以扇形 (circle sector) 表示,因此需要計算每個數據起始和結束的角度。我們可以使用 startAngle(for:) 和 endAngle(for:) 方法來計算,它們會回傳弧度數值。

 textOffset(for:in:) 方法看起來更複雜,但實際上,它與角度方法非常相似。它的任務是計算圓形中的一個點,讓我們在這個點上放置標籤,來描述不同扇形所代表的內容。在這些計算中,有兩個需要注意的屬性:

  1. 標籤會被放置在圓心和圓周之間三分之二的位置上。
  2. 標籤會被放置在扇形角度的中心。

現在,讓我們看一下 body 屬性和結構。首先,我們將所有內容包裝在一個 GeometryReader 中。這個組件讓我們可以取得包含矩形 (containing rectangle) 的資訊,讓我們可以正確地放置標籤。

GeometryReader 中是 ZStack,讓我們可以將多個組件一層層疊上。ZStack 包含了一個 ForEach 視圖,為我們所有數據點創建 PieSlice 組件和PieSliceText。請注意,ForEach 每次運行時都會實例化兩個 PieSlice 物件。在撰寫這篇文章時,SwiftUI 還沒有一個方便的方法,讓我們同時為 Shape 型別繪畫外型並填滿顏色,因此我們需要創建兩個物件,一個用來填滿顏色,另一個用來繪畫外型。

最後要注意的是,我們指定了 PieSliceText 的 Z index,以確保它們在其他組件之上呈現。

繪製數據

在計算所有封閉組件的角度和位置方面,PieChart 視圖負責了大部分的工作。因此,實作 PieSlice ShapePieSliceText 標籤就十分輕鬆。讓我們看看 PieSlice 的程式碼:

import SwiftUI

struct PieSlice: Shape {
  let startAngle: Double
  let endAngle: Double
  
  func path(in rect: CGRect) -> Path {
    var path = Path()
    let radius = min(rect.width, rect.height) / 2
    let alpha = CGFloat(startAngle)
    
    let center = CGPoint(
      x: rect.midX,
      y: rect.midY
    )
    
    path.move(to: center)
    
    path.addLine(
      to: CGPoint(
        x: center.x + cos(alpha) * radius,
        y: center.y + sin(alpha) * radius
      )
    )
    
    path.addArc(
      center: center,
      radius: radius,
      startAngle: Angle(radians: startAngle),
      endAngle: Angle(radians: endAngle),
      clockwise: false
    )
    
    path.closeSubpath()
    
    return path
  }
}

PieSlice 以起始和結束角度為實例化的參數,依三角學 (trigonometry) 繪製了一個在兩個角度之間的扇形。之後的步驟很簡單,只是普通直角三角形計算。

最後,讓我們看看 PieSliceText 組件,它唯一的工作就是精準地將兩個文本標籤堆疊。

import SwiftUI

struct PieSliceText: View {
  let title: String
  let description: String
  
  var body: some View {
    VStack {
      Text(title)
        .font(.headline)
      Text(description)
        .font(.body)
    }
  }
}

整合圖表和數據

這篇文章整合好的 PieChart 視圖已經可以使用了,我們要做的就只是將檔案複製到專案中,並實例化一個新物件。快點試試使用這些組件、添加新的組件,並在圖表加入你的個人元素。我也很期待你分享一下你的作品!

在下一篇文章發佈之前,你可以先重溫這系列的第一篇第二篇教學文章。

你也看看這篇文章,看看該如何解決複雜的問題。

本篇原文(標題:Data Visualization With SwiftUI: Pie Charts)刊登於作者 Medium,由 Jimmy M Andersson 所著,並授權翻譯及轉載。

作者簡介:Jimmy M Andersson 是一名軟件開發人員,在活躍於汽車行業的 NIRA Dynamics 中負責數據採集。他開發了監控和日誌記錄 App,來演示及可視化公司的產品組合和其功能。他目前正在進修資訊科技碩士學位,目標是以數據科學專業畢業。他每週都會在 Medium 發表軟件開發文章。你也可以在 Twitter 或 Email 聯絡他。

譯者簡介:Kelly Chan-AppCoda 編輯小姐。


此文章為客座或轉載文章,由作者授權刊登,AppCoda編輯團隊編輯。有關文章詳情,請參考文首或文末的簡介。

blog comments powered by Disqus
Shares
Share This