
-
Swift Chartsの3次元変換
Chart3Dを使って2D Swift Chartsを3次元に変換し、まったく新しい視点からデータセットを可視化する方法を説明します。3Dでデータをプロットし、数学的サーフェスを可視化し、カメラからマテリアルまですべてをカスタマイズして、より直感的で楽しい3Dチャートを作成しましょう。
このセッションの内容を十分理解できるよう、2D Swift Chartsの作成方法を確認しておくことをおすすめします。関連する章
- 0:00 - イントロダクション
- 1:54 - 3Dでのプロット
- 5:05 - サーフェスのプロット
- 7:03 - カスタマイズ
リソース
関連ビデオ
WWDC24
WWDC22
-
このビデオを検索
こんにちはMikeです System Experienceチームでエンジニアをしています 本日はSwift Chartsの素晴らしい新機能 について説明したいと思います Swift Chartsはわかりやすく魅力的な グラフを作成するためのフレームワークです
グラフはAppleプラットフォーム全体で使われています 「天気」での気温の確認
「設定」でのバッテリー使用状況の表示 「計算メモ」での数学関数のグラフ化などに 利用されています
Swift Chartsに用意されている 構成要素を利用すると 軸の目盛り ラベル 折れ線グラフなどの コンポーネントを使って 2Dグラフを作成できます
もっと魅力的なグラフを作ることもできます iOS macOS visionOS 26の新機能により Swift Chartsは3Dグラフに対応できます そうです ユーザーはまったく新しい 視点からデータセットを調べ 理解できるようになるのです
このセッションでは 2Dグラフのセットを3Dでプロットして 3次元化する方法を紹介します またサーフェスプロットを使って 3次元の数学関数をグラフ化する 方法を説明します 最後にグラフをより直感的で 魅力的なものにするための カスタマイズ方法を説明します その前に重要なお知らせがあります
私はペンギンが大好きです
実はお気に入りのデータセットの1つには 南極のパーマー諸島に生息する 何百羽ものペンギンの観察データが 含まれているのです
このデータには各ペンギンの くちばしや翼の長さと体重が含まれており
ヒゲペンギン ジェンツーペンギン アデリーペンギンという種類ごとに
分類されています
Swift Chartsを使ってこのデータから 詳細情報を引き出し 3Dでプロットすることで ペンギンの種類の違いを視覚化する方法を 紹介します
ペンギンの翼の長さと 体重の関係を示す 2Dグラフを作成しました PointMarkを使って各ペンギンの 翼の長さと体重をプロットし foregroundStyleによって 種類ごとにポイントの色分けをして 隅に凡例を作成します
これは素晴らしいグラフです Swift Chartsを 使えばグラフを簡単に作成できます このグラフでは ヒゲペンギンとアデリーペンギンの 翼の長さと体重は似ているが ジェンツーペンギンは翼が長く 体重も重いことを示しています
ペンギンデータセットには くちばしの長さも含まれており くちばしの長さと体重をプロットした グラフも作成しました
このグラフはヒゲペンギンとジェンツー ペンギンのくちばしの長さは似ているが アデリーペンギンのくちばしは短いことを 示しています
最後にくちばしと翼の長さの グラフを作成しました くちばしが長いペンギンは 翼も長い傾向があります
これらはどちらも優れた 2Dグラフで 一度に2つの特徴間の関係について 有用な詳細情報を提供します
Swift Chartsではこれらのグラフを 次のレベルに引き上げることができます 1つのグラフにすべてのデータを 含めることができるのです
それはChart3Dと呼ばれています Chart3Dは 散布図などの2Dグラフで よく知られている概念を採用しており それをフル3Dのグラフに適用します
3Dグラフを使うために ChartをChart3Dに変更します
PointMarkはChart3Dで有用であり Z値も指定できるようになりました ここでは「Beak Length」を使用します
Z軸のラベルも 「Beak Length」に設定して以上で完了です
数行のコードとChart3Dにより ペンギンの種類ごとの違いを 楽しくてインタラクティブな方法で はっきり表現できるようになりました
単純なジェスチャーで チャートを正確な角度まで回転させて データポイントの 3つのクラスタを確認できます
チャートは側面からも表示でき 一度に2つの特徴を比較できます グラフを2D表示する場合と似ています
3Dグラフが特に有効なのは 正確な数値よりも データの形状が重要な場合です これは当然ながら データ自体が3次元の場合であり 特に データが3D空間の物理的な位置を 表す場合に当てはまります
またインタラクティブ性は 3次元データセットを理解する際に重要です アプリの体験を向上させるために インタラクションが必要な場合にのみ 3Dチャートを検討してください データセットの最適な表現について 2Dと3Dのどちらを選択したら良いかは 一概には決められません
PointMarkやRuleMark RectangleMarkはすべて3Dグラフ用に 更新されています そしてChart3Dに固有の機能として SurfacePlotがあるのです
次にSurfacePlotの仕組みを 詳しく見ていきましょう
SurfacePlotはLinePlotの3次元拡張機能です 最大2つの変数を含む数学的なサーフェスを 3次元でプロットします
新しいSurfacePlot APIは LinePlot APIに似ています
2つのdoubleを受け取り 1つのdoubleを返す クロージャを受け入れます
クロージャに数式を入力すると SurfacePlotはさまざまな XとZの値で式を評価し 計算されたYの値から 連続したサーフェスを作成します これらのサーフェスは必要に応じて 複雑にしたり シンプルにしたりできます
LinePlot APIを介した関数プロットの 詳細については WWDC24の 「Swift Charts:ベクトルプロットと関数プロット」 をご覧ください
お気づきでしょうか ペンギンのデータセットを改めて見ると ペンギンのくちばしと翼の長さ および体重の間には線形関係があるようです これは妥当な推測のように思えますが 一時的な対処で済ませずに SurfacePlotを使って データの線形回帰を示します
独立したx変数とz変数に 基づいてy値を推定する LinearRegressionクラスを定義しました 具体的には 翼の長さとくちばしの長さに基づいて ペンギンの体重を推定する 線形回帰を設定します
この線形回帰はSurfacePlotで使われ 推定された体重を連続したサーフェスとして プロットします
素晴らしいですね このデータには線形関係があるようです SurfacePlotは翼の長さと体重の間に 正の相関関係があることを示しています また くちばしの長さと体重の間にも わずかに正の相関関係が見られます
では Chart3Dのスタイルや動作を カスタマイズするための 優れた方法を説明します
先ほどペンギンのデータセットを 操作していましたが グラフの角度が変わるとデータの外観も 変更されることに気づきました
この角度はデータポイントの クラスタを表示するのに最適です この角度は線形関係を示すのに 適しています
これらの角度はグラフのポーズと呼ばれます
最初のポーズを選択することが重要です これによりデータを適切に表現できます
事前に値がわからない動的データの場合 その種類の代表的なデータセットを 適切に表す初期ポーズを選んでください
グラフのポーズはChart3DPose修飾子を 使って調整され この修飾子はChart3DPoseの値を 受け取ります
ポーズをfrontなどの特定の値に 設定できます
カスタムポーズを定義することもできます このイニシャライザは2つのパラメータを 取ります グラフを左右に回転させるazimuthと
上下に傾けるinclinationです
次に グラフの奥にあるポイントと 手前にあるポイントが 同じサイズであることに注目してください
これにより 奥行きに関係なく グラフ全体でサイズや距離を 比較しやすくなります
またグラフを横から見るのにも適しており 3Dグラフを実質的に2Dグラフに変換できます
これは正投影カメラ投影と呼ばれます
Chart3Dには2つのカメラ投影があります デフォルトの動作である正投影と 透視投影です 透視投影では 遠くにあるデータポイントは小さく表示され 平行線は収束します これによりイマーシブな体験が可能になり 奥行きの認識に役立ちます
カメラ投影はchart3DCameraProjection 修飾子を使って設定されます
SurfacePlotsにはサーフェスのスタイルを カスタマイズするオプションがあります
ForegroundStyleはLinearGradientや EllipticalGradientなどのグラデーションを 受け取り 次の2つの新しい値をサポートします 各ポイントのサーフェスの高さに基づいて サーフェス上のポイントを着色する heightBasedと
各ポイントのサーフェスの角度に基づいて サーフェス上のポイントを着色する normalBasedです
Chart3Dでは他にも多くの修飾子を利用でき その中には2Dグラフで よく使われているものもあります これらを使って サーフェスのスタイル PointMarkのシンボル グラフのドメインと軸の目盛り 選択時の動作をカスタマイズします
これらのビュー修飾子をPointMark RuleMark RectangleMark SurfacePlotと 組み合わせることで さまざまな種類の 魅力的なグラフを作成できます これはほんの一例にすぎません
また3DグラフはVision Proで動作し 見た目も優れており 3次元データセットに自然に適合します
これらはSwift Chartsに導入される 新しい3D機能の一部です
3Dがデータの適切な表現方法であると 判断したら Chart3Dでプロットして グラフに新たなレベルの奥行きを加え Swift ChartsのカスタマイズAPIを使って わかりやすく魅力的なグラフを 独自にデザインしてみてください
Swift Chartsをアプリに組み込む場合の ベストプラクティスについては WWDC22の「グラフを使った App体験のデザイン」をご覧ください
ご視聴ありがとうございました 皆さんが3次元で作るグラフを 楽しみにしています
-
-
2:03 - A scatterplot of a penguin's flipper length and weight
// A scatterplot of a penguin's flipper length and weight import SwiftUI import Charts struct PenguinChart: View { var body: some View { Chart(penguins) { penguin in PointMark( x: .value("Flipper Length", penguin.flipperLength), y: .value("Weight", penguin.weight) ) .foregroundStyle(by: .value("Species", penguin.species)) } .chartXAxisLabel("Flipper Length (mm)") .chartYAxisLabel("Weight (kg)") .chartXScale(domain: 160...240) .chartYScale(domain: 2...7) .chartXAxis { AxisMarks(values: [160, 180, 200, 220, 240]) { AxisTick() AxisGridLine() AxisValueLabel() } } .chartYAxis { AxisMarks(values: [2, 3, 4, 5, 6, 7]) { AxisTick() AxisGridLine() AxisValueLabel() } } } }
-
2:39 - A scatterplot of a penguin's beak length and weight
// A scatterplot of a penguin's beak length and weight import SwiftUI import Charts struct PenguinChart: View { var body: some View { Chart(penguins) { penguin in PointMark( x: .value("Beak Length", penguin.beakLength), y: .value("Weight", penguin.weight) ) .foregroundStyle(by: .value("Species", penguin.species)) } .chartXAxisLabel("Beak Length (mm)") .chartYAxisLabel("Weight (kg)") .chartXScale(domain: 30...60) .chartYScale(domain: 2...7) .chartXAxis { AxisMarks(values: [30, 40, 50, 60]) { AxisTick() AxisGridLine() AxisValueLabel() } } .chartYAxis { AxisMarks(values: [2, 3, 4, 5, 6, 7]) { AxisTick() AxisGridLine() AxisValueLabel() } } } }
-
2:51 - A scatterplot of a penguin's beak length and flipper length
// A scatterplot of a penguin's beak length and flipper length import SwiftUI import Charts struct PenguinChart: View { var body: some View { Chart(penguins) { penguin in PointMark( x: .value("Beak Length", penguin.beakLength), y: .value("Flipper Length", penguin.flipperLength) ) .foregroundStyle(by: .value("Species", penguin.species)) } .chartXAxisLabel("Beak Length (mm)") .chartYAxisLabel("Flipper Length (mm)") .chartXScale(domain: 30...60) .chartYScale(domain: 160...240) .chartXAxis { AxisMarks(values: [30, 40, 50, 60]) { AxisTick() AxisGridLine() AxisValueLabel() } } .chartYAxis { AxisMarks(values: [160, 180, 200, 220, 240]) { AxisTick() AxisGridLine() AxisValueLabel() } } } }
-
3:28 - A scatterplot of a penguin's flipper length, beak length, and weight
// A scatterplot of a penguin's flipper length, beak length, and weight import SwiftUI import Charts struct PenguinChart: View { var body: some View { Chart3D(penguins) { penguin in PointMark( x: .value("Flipper Length", penguin.flipperLength), y: .value("Weight", penguin.weight), z: .value("Beak Length", penguin.beakLength) ) .foregroundStyle(by: .value("Species", penguin.species)) } .chartXAxisLabel("Flipper Length (mm)") .chartYAxisLabel("Weight (kg)") .chartZAxisLabel("Beak Length (mm)") .chartXScale(domain: 160...240, range: -0.5...0.5) .chartYScale(domain: 2...7, range: -0.5...0.5) .chartZScale(domain: 30...60, range: -0.5...0.5) .chartXAxis { AxisMarks(values: [160, 180, 200, 220, 240]) { AxisTick() AxisGridLine() AxisValueLabel() } } .chartYAxis { AxisMarks(values: [2, 3, 4, 5, 6, 7]) { AxisTick() AxisGridLine() AxisValueLabel() } } .chartZAxis { AxisMarks(values: [30, 40, 50, 60]) { AxisTick() AxisGridLine() AxisValueLabel() } } } }
-
5:19 - A surface plot showing mathematical functions (x * z)
// A surface plot showing mathematical functions import SwiftUI import Charts var SurfacePlotChart: View { var body: some View { Chart3D { SurfacePlot(x: "X", y: "Y", z: "Z") { x, z in // (Double, Double) -> Double x * z } } } }
-
5:43 - A surface plot showing mathematical functions
// A surface plot showing mathematical functions import SwiftUI import Charts var SurfacePlotChart: View { var body: some View { Chart3D { SurfacePlot(x: "X", y: "Y", z: "Z") { x, z in // (Double, Double) -> Double (sin(5 * x) + sin(5 * z)) / 2 } } } }
-
5:46 - A surface plot showing mathematical functions (-z)
// A surface plot showing mathematical functions import SwiftUI import Charts var SurfacePlotChart: View { var body: some View { Chart3D { SurfacePlot(x: "X", y: "Y", z: "Z") { x, z in // (Double, Double) -> Double -z } } } }
-
6:19 - Present a linear regression of the penguin data
// Present a linear regression of the penguin data import SwiftUI import Charts import CreateML import TabularData final class LinearRegression: Sendable { let regressor: MLLinearRegressor init<Data: RandomAccessCollection>( _ data: Data, x xPath: KeyPath<Data.Element, Double>, y yPath: KeyPath<Data.Element, Double>, z zPath: KeyPath<Data.Element, Double> ) { let x = Column(name: "X", contents: data.map { $0[keyPath: xPath] }) let y = Column(name: "Y", contents: data.map { $0[keyPath: yPath] }) let z = Column(name: "Z", contents: data.map { $0[keyPath: zPath] }) let data = DataFrame(columns: [x, y, z].map { $0.eraseToAnyColumn() }) regressor = try! MLLinearRegressor(trainingData: data, targetColumn: "Y") } func callAsFunction(_ x: Double, _ z: Double) -> Double { let x = Column(name: "X", contents: [x]) let z = Column(name: "Z", contents: [z]) let data = DataFrame(columns: [x, z].map { $0.eraseToAnyColumn() }) return (try? regressor.predictions(from: data))?.first as? Double ?? .nan } } let linearRegression = LinearRegression( penguins, x: \.flipperLength, y: \.weight, z: \.beakLength ) struct PenguinChart: some View { var body: some View { Chart3D { ForEach(penguins) { penguin in PointMark( x: .value("Flipper Length", penguin.flipperLength), y: .value("Weight", penguin.weight), z: .value("Beak Length", penguin.beakLength), ) .foregroundStyle(by: .value("Species", penguin.species)) } SurfacePlot(x: "Flipper Length", y: "Weight", z: "Beak Length") { flipperLength, beakLength in linearRegression(flipperLength, beakLength) } .foregroundStyle(.gray) } .chartXAxisLabel("Flipper Length (mm)") .chartYAxisLabel("Weight (kg)") .chartZAxisLabel("Beak Length (mm)") .chartXScale(domain: 160...240, range: -0.5...0.5) .chartYScale(domain: 2...7, range: -0.5...0.5) .chartZScale(domain: 30...60, range: -0.5...0.5) .chartXAxis { AxisMarks(values: [160, 180, 200, 220, 240]) { AxisTick() AxisGridLine() AxisValueLabel() } } .chartYAxis { AxisMarks(values: [2, 3, 4, 5, 6, 7]) { AxisTick() AxisGridLine() AxisValueLabel() } } .chartZAxis { AxisMarks(values: [30, 40, 50, 60]) { AxisTick() AxisGridLine() AxisValueLabel() } } } }
-
7:50 - Adjust the initial chart pose (Default)
// Adjust the initial chart pose import SwiftUI import Charts struct PenguinChart: View { @State var pose: Chart3DPose = .default var body: some View { Chart3D(penguins) { penguin in PointMark( x: .value("Flipper Length", penguin.flipperLength), y: .value("Weight", penguin.weight), z: .value("Beak Length", penguin.beakLength) ) .foregroundStyle(by: .value("Species", penguin.species)) } .chart3DPose($pose) .chartXAxisLabel("Flipper Length (mm)") .chartYAxisLabel("Weight (kg)") .chartZAxisLabel("Beak Length (mm)") .chartXScale(domain: 160...240, range: -0.5...0.5) .chartYScale(domain: 2...7, range: -0.5...0.5) .chartZScale(domain: 30...60, range: -0.5...0.5) .chartXAxis { AxisMarks(values: [160, 180, 200, 220, 240]) { AxisTick() AxisGridLine() AxisValueLabel() } } .chartYAxis { AxisMarks(values: [2, 3, 4, 5, 6, 7]) { AxisTick() AxisGridLine() AxisValueLabel() } } .chartZAxis { AxisMarks(values: [30, 40, 50, 60]) { AxisTick() AxisGridLine() AxisValueLabel() } } } }
-
8:02 - Adjust the initial chart pose (Front)
// Adjust the initial chart pose import SwiftUI import Charts struct PenguinChart: View { @State var pose: Chart3DPose = .front var body: some View { Chart3D(penguins) { penguin in PointMark( x: .value("Flipper Length", penguin.flipperLength), y: .value("Weight", penguin.weight), z: .value("Beak Length", penguin.beakLength) ) .foregroundStyle(by: .value("Species", penguin.species)) } .chart3DPose($pose) .chartXAxisLabel("Flipper Length (mm)") .chartYAxisLabel("Weight (kg)") .chartZAxisLabel("Beak Length (mm)") .chartXScale(domain: 160...240, range: -0.5...0.5) .chartYScale(domain: 2...7, range: -0.5...0.5) .chartZScale(domain: 30...60, range: -0.5...0.5) .chartXAxis { AxisMarks(values: [160, 180, 200, 220, 240]) { AxisTick() AxisGridLine() AxisValueLabel() } } .chartYAxis { AxisMarks(values: [2, 3, 4, 5, 6, 7]) { AxisTick() AxisGridLine() AxisValueLabel() } } .chartZAxis { AxisMarks(values: [30, 40, 50, 60]) { AxisTick() AxisGridLine() AxisValueLabel() } } } }
-
8:09 - Adjust the initial chart pose (Custom)
// Adjust the initial chart pose import SwiftUI import Charts struct PenguinChart: View { @State var pose = Chart3DPose( azimuth: .degrees(20), inclination: .degrees(7) ) var body: some View { Chart3D(penguins) { penguin in PointMark( x: .value("Flipper Length", penguin.flipperLength), y: .value("Weight", penguin.weight), z: .value("Beak Length", penguin.beakLength) ) .foregroundStyle(by: .value("Species", penguin.species)) } .chart3DPose($pose) .chartXAxisLabel("Flipper Length (mm)") .chartYAxisLabel("Weight (kg)") .chartZAxisLabel("Beak Length (mm)") .chartXScale(domain: 160...240, range: -0.5...0.5) .chartYScale(domain: 2...7, range: -0.5...0.5) .chartZScale(domain: 30...60, range: -0.5...0.5) .chartXAxis { AxisMarks(values: [160, 180, 200, 220, 240]) { AxisTick() AxisGridLine() AxisValueLabel() } } .chartYAxis { AxisMarks(values: [2, 3, 4, 5, 6, 7]) { AxisTick() AxisGridLine() AxisValueLabel() } } .chartZAxis { AxisMarks(values: [30, 40, 50, 60]) { AxisTick() AxisGridLine() AxisValueLabel() } } } }
-
9:15 - Adjust the initial chart pose and camera projection
// Adjust the initial chart pose and camera projection import SwiftUI import Charts struct PenguinChart: View { @State var pose = Chart3DPose( azimuth: .degrees(20), inclination: .degrees(7) ) var body: some View { Chart3D(penguins) { penguin in PointMark( x: .value("Flipper Length", penguin.flipperLength), y: .value("Weight", penguin.weight), z: .value("Beak Length", penguin.beakLength) ) .foregroundStyle(by: .value("Species", penguin.species)) } .chart3DPose($pose) .chart3DCameraProjection(.perspective) .chartXAxisLabel("Flipper Length (mm)") .chartYAxisLabel("Weight (kg)") .chartZAxisLabel("Beak Length (mm)") .chartXScale(domain: 160...240, range: -0.5...0.5) .chartYScale(domain: 2...7, range: -0.5...0.5) .chartZScale(domain: 30...60, range: -0.5...0.5) .chartXAxis { AxisMarks(values: [160, 180, 200, 220, 240]) { AxisTick() AxisGridLine() AxisValueLabel() } } .chartYAxis { AxisMarks(values: [2, 3, 4, 5, 6, 7]) { AxisTick() AxisGridLine() AxisValueLabel() } } .chartZAxis { AxisMarks(values: [30, 40, 50, 60]) { AxisTick() AxisGridLine() AxisValueLabel() } } } }
-
9:24 - Customize the surface styles for a sinc function
// Customize the surface styles for a sinc function import SwiftUI import Charts struct SurfacePlotChart: View { var body: some View { Chart3D { SurfacePlot(x: "X", y: "Y", z: "Z") { x, z in let h = hypot(x, z) return sin(h) / h } } .chartXScale(domain: -10...10, range: -0.5...0.5) .chartZScale(domain: -10...10, range: -0.5...0.5) .chartYScale(domain: -0.23...1, range: -0.5...0.5) } }
-
9:29 - Customize the surface styles for a sinc function (EllipticalGradient)
// Customize the surface styles for a sinc function import SwiftUI import Charts struct SurfacePlotChart: View { var body: some View { Chart3D { SurfacePlot(x: "X", y: "Y", z: "Z") { x, z in let h = hypot(x, z) return sin(h) / h } .foregroundStyle(EllipticalGradient(colors: [.red, .orange, .yellow, .green, .blue, .indigo, .purple])) } .chartXScale(domain: -10...10, range: -0.5...0.5) .chartZScale(domain: -10...10, range: -0.5...0.5) .chartYScale(domain: -0.23...1, range: -0.5...0.5) } }
-
9:38 - Customize the surface styles for a sinc function (heightBased)
// Customize the surface styles for a sinc function import SwiftUI import Charts struct SurfacePlotChart: View { var body: some View { Chart3D { SurfacePlot(x: "X", y: "Y", z: "Z") { x, z in let h = hypot(x, z) return sin(h) / h } .foregroundStyle(.heightBased) } .chartXScale(domain: -10...10, range: -0.5...0.5) .chartZScale(domain: -10...10, range: -0.5...0.5) .chartYScale(domain: -0.23...1, range: -0.5...0.5) } }
-
9:47 - Customize the surface styles for a sinc function (normalBased)
// Customize the surface styles for a sinc function import SwiftUI import Charts struct SurfacePlotChart: View { var body: some View { Chart3D { SurfacePlot(x: "X", y: "Y", z: "Z") { x, z in let h = hypot(x, z) return sin(h) / h } .foregroundStyle(.normalBased) } .chartXScale(domain: -10...10, range: -0.5...0.5) .chartZScale(domain: -10...10, range: -0.5...0.5) .chartYScale(domain: -0.23...1, range: -0.5...0.5) } }
-