mcpField
C
charts-3d

charts-3d

github.com/rshankrasデータ・データベース83876 回インストール14 週間前に更新
リポジトリ

charts-3d について

3D可視化と一般的なチャートを作る開発者向けのSwift Chartsスキルです。曲面、カメラ操作、スタイル、チャート構築、選択、アクセシビリティを扱います。

主な機能

  1. 01数式・データ駆動の曲面
  2. 02カメラ姿勢のインタラクティブ制御
  3. 03曲面のグラデーションとシェーディング
  4. 042Dマーク、軸、選択
  5. 05チャートのアクセシビリティパターン

活用シーン

  1. 01数式を3D曲面として描画
  2. 021つのチャートで複数の曲面を比較
  3. 03選択操作に対応する2Dチャートを構築
  4. 04VoiceOverからチャートデータにアクセス可能にする

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/rshankras/claude-code-apple-skills --skill charts-3d

インストール後、Claude Code で/charts-3dを入力して読み込まれたか確認します。

詳細

Swift Chartsによる3Dチャート

Chart3DとSurfacePlotを使用して、3Dデータビジュアライゼーションを作成します。数式に基づくサーフェス、データに基づくサーフェス、インタラクティブなカメラポーズ制御、サーフェスのスタイリング、カメラの投影モードを扱います。

このスキルが有効になる場合

次のような場合に、このスキルを使用します:

  • 3Dチャートまたは3Dデータビジュアライゼーションを作成したい
  • Chart3D、SurfacePlot、または3Dサーフェスプロットについて質問する
  • 数学関数を3Dサーフェスとして可視化したい
  • 3Dチャートをドラッグして回転できるようにしたい
  • 3Dチャートのカメラアングル、ポーズ、または投影について質問する
  • グラデーションや高さに基づく色付けで3Dサーフェスをスタイリングしたい
  • 1つの3Dチャートに複数のサーフェスを描画したい
  • 配列内の点からデータに基づく3Dプロットを作成する方法について質問する
  • 2D Swift Charts APIレイヤー(マーク、軸、系列のスタイリング、選択、SectorMarkによる円グラフ/ドーナツグラフ、スクロール可能なチャート)が必要である

判断ツリー

What 3D chart feature do you need?
|
+-- Visualize a math function f(x, y) -> z
|   +-- Use SurfacePlot(x:y:z:function:)
|
+-- Visualize data points as a surface
|   +-- Use Chart3D(data) { point in SurfacePlot(...) }
|
+-- Interactive drag-to-rotate
|   +-- Bind pose: .chart3DPose($pose) with @State var pose: Chart3DPose
|
+-- Fixed viewing angle (no interaction)
|   +-- Read-only pose: .chart3DPose(Chart3DPose.front) or custom
|
+-- Style the surface color
|   +-- Solid color -> .foregroundStyle(Color.blue)
|   +-- Gradient -> .foregroundStyle(LinearGradient(...))
|   +-- Height-based -> .foregroundStyle(.heightBased(gradient, yRange:))
|   +-- Normal-based -> .foregroundStyle(.normalBased)
|
+-- Camera projection
|   +-- Perspective (depth) -> .chart3DCameraProjection(.perspective)
|   +-- Orthographic (flat) -> .chart3DCameraProjection(.orthographic)
|   +-- System default -> .chart3DCameraProjection(.automatic)
|
+-- Multiple surfaces in one chart
    +-- Place multiple SurfacePlot calls inside a single Chart3D { }

APIの利用可能状況

API最小バージョンインポート備考
Chart3DiOS 26 / macOS 26Charts3Dチャートのメインコンテナ
SurfacePlotiOS 26 / macOS 26Charts3Dサーフェスマーク
Chart3DPoseiOS 26 / macOS 26Charts視角の制御
Chart3DCameraProjectioniOS 26 / macOS 26Charts.automatic、.perspective、.orthographic
Chart3DSurfaceStyleiOS 26 / macOS 26Charts.heightBased、.normalBased

クイックスタート

数式に基づくサーフェス

関数f(x, y) -> zからサーフェスを描画します:

import SwiftUI
import Charts

struct WaveSurfaceView: View {
    var body: some View {
        Chart3D {
            SurfacePlot(
                x: "X",
                y: "Height",
                z: "Z",
                function: { x, z in
                    sin(x) * cos(z)
                }
            )
            .foregroundStyle(.blue)
        }
    }
}
データに基づくサーフェス

データポイントの配列からサーフェスを描画します:

import SwiftUI
import Charts

struct DataPoint: Identifiable {
    let id = UUID()
    let x: Double
    let y: Double
    let z: Double
}

struct DataSurfaceView: View {
    let points: [DataPoint]

    var body: some View {
        Chart3D(points) { point in
            SurfacePlot(
                x: .value("X", point.x),
                y: .value("Height", point.y),
                z: .value("Z", point.z)
            )
        }
    }
}
インタラクティブな回転

ユーザーがドラッグしてチャートを回転できるようにします:

import SwiftUI
import Charts

struct InteractiveChartView: View {
    @State private var pose = Chart3DPose.default

    var body: some View {
        Chart3D {
            SurfacePlot(
                x: "X",
                y: "Height",
                z: "Z",
                function: { x, z in
                    sin(x) * cos(z)
                }
            )
            .foregroundStyle(.blue)
        }
        .chart3DPose($pose)
    }
}

サーフェスのスタイリングパターン

単色
SurfacePlot(x: "X", y: "Y", z: "Z", function: { x, z in x * z })
    .foregroundStyle(.blue)
線形グラデーション
SurfacePlot(x: "X", y: "Y", z: "Z", function: { x, z in x * z })
    .foregroundStyle(
        LinearGradient(
            colors: [.blue, .green, .yellow],
            startPoint: .bottom,
            endPoint: .top
        )
    )
高さに基づくサーフェススタイル

高さの値に基づいてサーフェスに色を付け、y軸の範囲全体にグラデーションを割り当てます:

SurfacePlot(x: "X", y: "Y", z: "Z", function: { x, z in sin(x) * cos(z) })
    .foregroundStyle(
        Chart3DSurfaceStyle.heightBased(
            Gradient(colors: [.blue, .cyan, .green, .yellow, .red]),
            yRange: -1...1
        )
    )
法線に基づくサーフェススタイル

サーフェス法線に基づいて色を付け、照明を反映した見た目にします:

SurfacePlot(x: "X", y: "Y", z: "Z", function: { x, z in sin(x) * cos(z) })
    .foregroundStyle(Chart3DSurfaceStyle.normalBased)
サーフェスの粗さ

サーフェスの光沢を制御します(0 = 反射、1 = つや消し):

SurfacePlot(x: "X", y: "Y", z: "Z", function: { x, z in sin(x) * cos(z) })
    .foregroundStyle(.blue)
    .roughness(0.3)

インタラクティブなポーズ制御

プリセットポーズ

Chart3DPoseには、一般的な視角向けの組み込みプリセットが用意されています:

.chart3DPose(.default)   // Standard 3/4 angle
.chart3DPose(.front)     // Viewing from front
.chart3DPose(.back)      // Viewing from back
.chart3DPose(.top)       // Top-down view
.chart3DPose(.bottom)    // Bottom-up view
.chart3DPose(.right)     // Right side view
.chart3DPose(.left)      // Left side view
カスタムポーズ

方位角(水平回転)と傾斜角(垂直方向の傾き)を正確に指定します:

.chart3DPose(
    Chart3DPose(azimuth: .degrees(45), inclination: .degrees(30))
)
避けるべきパターン
// ❌ Passing a literal where a binding is needed for interactivity
.chart3DPose(.default) // This is read-only; drag gestures will not work

// ✅ Use a @State binding for interactive rotation
@State private var pose = Chart3DPose.default
// ...
.chart3DPose($pose)

カメラ投影

3Dの奥行きの描画方法を制御します:

Chart3D {
    SurfacePlot(x: "X", y: "Y", z: "Z", function: { x, z in sin(x) * cos(z) })
        .foregroundStyle(.blue)
}
.chart3DCameraProjection(.perspective)   // Objects farther away appear smaller
// .chart3DCameraProjection(.orthographic)  // No perspective distortion
// .chart3DCameraProjection(.automatic)     // System decides

複数のサーフェス

比較のため、1つのチャートに複数のサーフェスを描画します:

import SwiftUI
import Charts

struct ComparisonChartView: View {
    @State private var pose = Chart3DPose.default

    var body: some View {
        Chart3D {
            SurfacePlot(
                x: "X",
                y: "Wave A",
                z: "Z",
                function: { x, z in sin(x) * cos(z) }
            )
            .foregroundStyle(.blue.opacity(0.8))

            SurfacePlot(
                x: "X",
                y: "Wave B",
                z: "Z",
                function: { x, z in cos(x) * sin(z) }
            )
            .foregroundStyle(.red.opacity(0.8))
        }
        .chart3DPose($pose)
        .chart3DCameraProjection(.perspective)
    }
}

完全なサンプル

高さに基づく色付け、インタラクティブな回転、透視投影を備えた、機能が充実した3Dチャートです:

import SwiftUI
import Charts

struct TerrainView: View {
    @State private var pose = Chart3DPose(
        azimuth: .degrees(30),
        inclination: .degrees(25)
    )

    var body: some View {
        VStack {
            Text("Terrain Visualization")
                .font(.headline)

            Chart3D {
                SurfacePlot(
                    x: "Longitude",
                    y: "Elevation",
                    z: "Latitude",
                    function: { x, z in
                        let distance = sqrt(x * x + z * z)
                        return sin(distance) / max(distance, 0.1)
                    }
                )
                .foregroundStyle(
                    Chart3DSurfaceStyle.heightBased(
                        Gradient(colors: [
                            .blue, .cyan, .green, .yellow, .orange, .red
                        ]),
                        yRange: -0.5...1.0
                    )
                )
                .roughness(0.4)
            }
            .chart3DPose($pose)
            .chart3DCameraProjection(.perspective)
        }
        .padding()
    }
}

チャートデザインの基本(WWDC22)

これらの基本は、2D、3Dを問わず、作成するすべてのチャートに当てはまります。柱となるのは、明確さ、親しみやすさ、アクセシビリティです。

  • 目的に応じてマークを選びます:パターン、範囲、個々の値には棒、外れ値の発見には点、変化率には線を使います。
  • 早い段階で、現実のノイズを含むデータを使ってテストします — 仮の正弦波ではレイアウトの不具合が隠れてしまいます。データが欠けている折れ線グラフの隙間など、エッジケースを明示的にデザインします。
  • 横のグリッド線は約4本を基本とし、直感的な間隔を選びます。個数なら20の倍数、日単位の軸なら7の倍数が適しています。
  • 一般的なラベル(「売上」)ではなく、説明的で要点が伝わるタイトル(「売上合計:パンケーキ1,234枚」)を付け、チャートの周囲にも文脈を添えます。
  • タッチターゲットはチャートの高さ全体に広げます — 棒の列のどこをタップしても選択できるようにし、描画されたピクセルだけに限定しません。
  • タッチ、キーボード、Voice Control、Switch Control、VoiceOverを同等にサポートします — すべての操作にポインターを使わない方法が必要です。
  • 色は情報を補強するために使い、色だけで情報を伝えないようにします — まず記号や形で系列を区別し、次に色を使います。ダーク、ライト、コントラストを上げる設定でコントラストを確認し、どの系列も視覚的に目立ちすぎないよう彩度のバランスを取ります。
  • 視覚的なエンコーディングには、必ず非視覚的な表現も用意します — マークごとのVoiceOverラベルとAudio Graphの記述子が必要です。Swift Chartsはどちらもデフォルトで提供するため、カスタム描画で損なわないようにしてください。カスタム描画のチャートでは、これらを手作業で実装します — 下記の「Audio Graphs: AXChartDescriptor」を参照してください。

Swift Chartsの構築リファレンス(2D)

上記のデザインの基本はすべてのチャートに当てはまります。ここでは、標準的な2DチャートのAPI層を説明します(特記がない限りiOS 16以降。選択、SectorMark、スクロールはiOS 17以降)。

マークの組み合わせ

Chartはマークを組み合わせて構成します — BarMark、LineMark、PointMark、AreaMark、RuleMark、RectangleMarkを使います。.value("Label", v)ファクトリーの引数には2つの役割があります。データに関連付けられるだけでなく、自動生成される軸と凡例にも使われるため、意味が分かるラベルを付けます。

Chart(salesData) { sale in          // Identifiable data — no explicit ForEach needed
    BarMark(
        x: .value("Day", sale.day, unit: .day),   // unit: .day buckets temporal values per day
        y: .value("Sales", sale.count)
    )
}
  • x/yの値のペアを入れ替えると、チャートを転置できます — 横棒グラフにするために、それ以外の変更は必要ありません。
  • unit:(.day、.month、.hour)で時間の区分を制御します。省略すると、すべてのタイムスタンプがそれぞれ独立した位置になります。
系列:スタイルと記号。色だけに頼らない

.foregroundStyle(by:)でマークを系列に分け、.symbol(by:)と組み合わせると、色がなくても系列を見分けられます(WWDC22)。

Chart(data) { point in
    LineMark(x: .value("Day", point.day), y: .value("Sales", point.sales))
        .foregroundStyle(by: .value("City", point.city))
        .symbol(by: .value("City", point.city))
}
  • .chartForegroundStyleScale(["Cupertino": .indigo, "San Francisco": .teal])で系列の色を割り当て直します。
  • .position(by: .value("City", point.city))を使うと、積み上げ棒グラフをグループ化された棒グラフに変換できます。
安定性を保つためにスケールを固定する

フィルタリングされたデータセットによってチャート全体が跳ねるのを防ぐため、スケールを固定します。

.chartYScale(domain: 0...maxExpectedSales)
.chartXScale(domain: startDate...endDate)
統計情報をチャートに組み込む

マークは自由に組み合わせられるため、要約統計量も追加のマークとして表現できます。

Chart {
    ForEach(data) { point in
        AreaMark(                                    // min–max band
            x: .value("Day", point.day),
            yStart: .value("Min", point.min),
            yEnd: .value("Max", point.max)
        )
        .opacity(0.3)

        LineMark(x: .value("Day", point.day), y: .value("Average", point.average))
    }

    RuleMark(y: .value("Overall", overallAverage))   // overall average line
        .foregroundStyle(.secondary)
        .annotation(position: .top, alignment: .leading) {
            Text("Avg: \(overallAverage, format: .number.precision(.fractionLength(0)))")
                .font(.caption)
                .foregroundStyle(.secondary)
        }
}
軸はビルダー
.chartXAxis {
    AxisMarks(values: .stride(by: .month)) { value in
        AxisGridLine()
        AxisTick()
        AxisValueLabel(format: .dateTime.month(.narrow))
    }
}
.chartYAxis {
    AxisMarks(position: .leading)   // move the value axis to the leading edge
}
  • 条件付きのスタイル設定はビルダーの内側に記述します — クロージャ内でvalue.as(Date.self)を確認すれば、たとえば各四半期の最初の月だけを太字にできます。
  • .chartXAxis(.hidden)で軸全体を非表示にできます。.chartPlotStyle { $0.frame(height: 200).background(.gray.opacity(0.05)).border(.quaternary) }を使うと、プロット領域そのもののサイズとスタイルを設定できます。
インタラクティブ性:まずは選択機能を使う

独自のオーバーレイジェスチャーを実装するより、組み込みの選択バインディングを優先します(iOS 17)。

@State private var selectedDay: Date?

Chart(data) { ... }
    .chartXSelection(value: $selectedDay)

選択状態はマークとして描画します — zIndex(-1)を指定したRuleMarkを使うとデータの背後に描画され、プロット内に収まる注釈も追加できます。

if let selectedDay {
    RuleMark(x: .value("Selected", selectedDay, unit: .day))
        .foregroundStyle(.gray.opacity(0.3))
        .zIndex(-1)
        .annotation(
            position: .top, spacing: 0,
            overflowResolution: .init(x: .fit(to: .chart), y: .disabled)
        ) {
            SelectionDetailCard(day: selectedDay)
        }
}

ヒットテストを完全にカスタマイズする場合は、.chartOverlay内でGeometryReaderとともにChartProxyを使います。proxy.value(atX:)はジェスチャーの位置をデータ値に変換し、proxy.position(forX:)はその逆に変換します。

SectorMark:円グラフとドーナツグラフ(iOS 17以降)
Chart(data) { item in
    SectorMark(
        angle: .value("Sales", item.sales),
        innerRadius: .ratio(0.62),     // donut hole
        angularInset: 1.5              // 1.5 per side = 3pt gaps between sectors
    )
    .cornerRadius(4)
    .foregroundStyle(by: .value("Name", item.name))
}
.chartBackground { proxy in
    GeometryReader { geo in            // headline metric in the donut hole
        if let anchor = proxy.plotFrame {
            let frame = geo[anchor]
            Text("Best: \(topSellerName)")
                .position(x: frame.midX, y: frame.midY)
        }
    }
}
大規模なデータセット:表示範囲をスクロールする

1年分を1画面に詰め込まず、表示範囲を設けてスクロールできるようにします(WWDC23)。

Chart(yearOfData) { ... }
    .chartScrollableAxes(.horizontal)
    .chartXVisibleDomain(length: 3600 * 24 * 30)   // 30-day window
    .chartScrollPosition(x: $scrollDate)           // read/write the scroll offset
    .chartScrollTargetBehavior(
        .valueAligned(
            matching: DateComponents(hour: 0),                    // land on day boundaries
            majorAlignment: .matching(DateComponents(day: 1))     // snap paging to month starts
        )
    )
マークごとのアクセシビリティ

自動生成されたVoiceOverの説明では生の値が読み上げられますが、マークごとのラベルを優先できます。

BarMark(x: .value("Day", sale.day, unit: .day), y: .value("Sales", sale.count))
    .accessibilityLabel(sale.day.formatted(date: .abbreviated, time: .omitted))
    .accessibilityValue("\(sale.count) pancakes sold")

ポイントが数百または数千ある場合は、適切な間隔ごとにまとめ、ポイントごとではなく間隔ごとに1つのアクセシビリティ要素を公開します(WWDC21 10122)。

オーディオグラフ:AXChartDescriptor(WWDC21 10122)

オーディオグラフでは、VoiceOverでデータ系列を連続した音として再生できます。ピッチはY値、時間はX位置に対応します。エクスプローラ表示(ローター →「オーディオグラフ」→「チャートの詳細」)では、音声化したデータを再生したり、スクラブ操作したり(二本指でダブルタップして長押しし、停止すると現在位置の値を読み上げます)、自動計算された要約統計量を表示したりできます。Swift Chartsはデフォルトの記述子を生成します。独自に描画したチャートでは、AXChart(UIKit)に準拠するか、AXChartDescriptorRepresentableと.accessibilityChartDescriptor(_:)修飾子(SwiftUI、iOS 15以降)を使用します。

var accessibilityChartDescriptor: AXChartDescriptor? {
    let xAxis = AXNumericDataAxisDescriptor(
        title: "Cups of coffee",
        range: 0...10,
        gridlinePositions: [],                              // gridlines render as haptics during playback
        valueDescriptionProvider: { "\(Int($0)) cups" })    // "5 cups", never a bare "5"

    let yAxis = AXNumericDataAxisDescriptor(
        title: "Lines of code",
        range: 0...100,
        gridlinePositions: [],
        valueDescriptionProvider: { "\(Int($0)) lines of code" })

    let series = AXDataSeriesDescriptor(
        name: "Productivity",
        isContinuous: true,      // line → one continuous tone; false for bars/points → discrete tones
        dataPoints: model.points.map { AXDataPoint(x: $0.x, y: $0.y) })

    return AXChartDescriptor(
        title: model.title,
        summary: model.summary,  // 1–2 sentence alt text; spoken in the explorer view
        xAxis: xAxis,
        yAxis: yAxis,
        additionalAxes: [],
        series: [series])
}
  • カテゴリ軸にはAXCategoricalDataAxisDescriptorを使用します。本番環境では、valueDescriptionProviderの出力をローカライズし、複数形に対応させてください。
  • すべてのチャートで確保すべき視認性(WWDC21 10122):前景色と背景色のコントラスト比は4.5:1以上(Accessibility InspectorのColor Contrast Calculatorで確認してください)。最も一般的な色覚異常で見分けにくい赤と緑の組み合わせは避けてください。2番目に一般的な色覚異常で見分けにくい青と黄色の組み合わせも避けてください。色をまったく認識できない場合でも系列を区別できるよう、色だけでなく記号も使用してください。

よくある間違い

#間違い修正方法
1import Chartsを忘れるSwiftUIとChartsの両方をインポートする必要があります
2.chart3DPose(.default)を使い、ドラッグで回転できると思い込むインタラクティブな回転には、@Stateバインディングを使って.chart3DPose($pose)を指定します
3実際の関数出力をカバーしないyRangeを設定する.heightBased()のyRangeを、関数出力の実際の最小値と最大値に合わせます
4.foregroundStyle()を適用せずに.roughness()を使うRoughnessは既存の表面の見た目を変更します。先に前景スタイルを設定してください
5プレゼンテーションやデモで正射影を使う見た目を重視する場合は.perspectiveを優先し、データを正確に読み取る場合は.orthographicを使います

レビューチェックリスト

インポートとセットアップ
  • import SwiftUIとimport Chartsの両方がある
  • デプロイメントターゲットがiOS 26/macOS 26以降である
  • Chart3DがすべてのSurfacePlotコンテンツを囲んでいる
サーフェスの設定
  • 軸ラベル(x:、y:、z:)が具体的で意味のあるものになっている
  • 各SurfacePlotにforegroundStyleが適用され、視覚的に明確に区別できる
  • .heightBased()のyRangeが関数の実際の出力範囲に合っている
  • roughnessの値が用途に適している(0 = 反射、1 = マット)
インタラクティブ操作
  • ドラッグでの回転を意図する場合、Poseが@Stateバインディングになっている
  • 回転をロックする場合、Poseが定数(バインディングではない)になっている
  • 初期Poseの角度によって、データを適切なデフォルト表示で確認できる
カメラ
  • カメラの投影法が用途に適している(見た目重視なら.perspective、精度重視なら.orthographic)
  • .automaticを使う場合、システムが選択した表示が適切であることを確認している
複数のサーフェス
  • 区別できるよう、各サーフェスに異なる色または不透明度が設定されている
  • 軸ラベルがサーフェスごとに固有であるか、適切な箇所で共有されている
アクセシビリティ
  • コントラスト比が4.5:1以上である。赤と緑、または青と黄色の系列の組み合わせを避け、色に加えて記号も使用している(WWDC21 10122)
  • 独自に描画したチャートがAXChartDescriptor(オーディオグラフ)を公開している。または、Swift Chartsのデフォルト設定をそのまま使用している
  • 大規模なデータセットでは、ポイントごとではなく間隔ごとに1つのアクセシビリティ要素を公開している

References

コメント