VisPyマスターガイド：Pythonによる高性能データ可視化の世界

## 第1章: VisPyの紹介

VisPyは、Pythonで高性能な対話型2D/3Dデータ可視化を実現するライブラリです。OpenGLを利用してGPUの計算能力を活用し、非常に大規模なデータセットを表示することができます。科学的な可視化、リアルタイムデータの表示、3Dモデルの対話型表示など、幅広い用途に適しています。

以下は、VisPyをインストールし、簡単な例を実行するコードです：

```python
# VisPyのインストール
!pip install vispy

# 簡単な例
import numpy as np
from vispy import app, gloo

# キャンバスクラスの定義
class Canvas(app.Canvas):
    def __init__(self):
        super().__init__(size=(800, 600), title="VisPy例")
        self.program = gloo.Program(vertex, fragment)
        self.program['position'] = [(-0.5, -0.5), (0.5, -0.5), (0, 0.5)]
        self.program['color'] = [(1, 0, 0, 1), (0, 1, 0, 1), (0, 0, 1, 1)]
        
    def on_draw(self, event):
        gloo.clear('white')
        self.program.draw('triangle_strip')

# シェーダーコード
vertex = """
attribute vec2 position;
attribute vec4 color;
varying vec4 v_color;
void main() {
    gl_Position = vec4(position, 0.0, 1.0);
    v_color = color;
}
"""

fragment = """
varying vec4 v_color;
void main() {
    gl_FragColor = v_color;
}
"""

# アプリケーションの実行
canvas = Canvas()
app.run()
```

この例では、VisPyを使って簡単な三角形を描画しています。キャンバスクラスを定義し、頂点シェーダーとフラグメントシェーダーを使用してGPU上で描画を行っています。

## 第2章: VisPyのアーキテクチャ

VisPyは階層的なアーキテクチャを採用しており、低レベルのOpenGL操作から高レベルの可視化機能まで、様々な抽象化レベルを提供しています。主要なコンポーネントには、gloo（OpenGLラッパー）、app（アプリケーションフレームワーク）、scene（シーングラフシステム）、visuals（グラフィカルプリミティブ）などがあります。

以下は、VisPyの異なる層を使用する例です：

```python
from vispy import app, gloo, scene
import numpy as np

# gloo層の使用
vertex = """
attribute vec2 position;
void main() {
    gl_Position = vec4(position, 0.0, 1.0);
}
"""

fragment = """
void main() {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
"""

# app層の使用
class MyCanvas(app.Canvas):
    def __init__(self):
        super().__init__(size=(800, 600), title="VisPyアーキテクチャ例")
        self.program = gloo.Program(vertex, fragment)
        self.program['position'] = np.array([[-0.5, -0.5], [0.5, -0.5], [0.0, 0.5]], dtype=np.float32)

    def on_draw(self, event):
        gloo.clear('white')
        self.program.draw('triangle_strip')

# scene層の使用
canvas = scene.SceneCanvas(keys='interactive', size=(800, 600), show=True)
view = canvas.central_widget.add_view()
scatter = scene.visuals.Markers()
scatter.set_data(np.random.normal(size=(100, 2)), edge_color=None, size=5)
view.add(scatter)

# アプリケーションの実行
if __name__ == '__main__':
    c = MyCanvas()
    c.show()
    app.run()
```

この例では、gloo層でカスタムシェーダーを使用し、app層でキャンバスを管理し、scene層で散布図を作成しています。これにより、VisPyの異なる抽象化レベルを理解することができます。

## 第3章: 基本的な2D描画

VisPyを使用して基本的な2D図形を描画する方法を学びましょう。線、四角形、円などの基本的な図形を描画し、色やスタイルをカスタマイズする方法を説明します。

```python
from vispy import app, scene
import numpy as np

canvas = scene.SceneCanvas(keys='interactive', size=(800, 600), show=True)
view = canvas.central_widget.add_view()

# 線の描画
line = scene.visuals.Line(pos=np.array([(0, 0), (100, 100), (200, 0)]), color='red', width=3)
view.add(line)

# 四角形の描画
rect = scene.visuals.Rectangle(center=(300, 300), width=100, height=100, color='blue')
view.add(rect)

# 円の描画
circle = scene.visuals.Ellipse(center=(500, 300), radius=50, color='green')
view.add(circle)

# テキストの追加
text = scene.visuals.Text('VisPy 2D描画', pos=(400, 50), color='black')
view.add(text)

# ビューの範囲設定
view.camera = 'panzoom'
view.camera.set_range()

if __name__ == '__main__':
    app.run()
```

この例では、VisPyのscene層を使用して、線、四角形、円、テキストなどの基本的な2D図形を描画しています。各図形はvisuals.クラスを使用して作成され、色やサイズなどの属性を簡単に設定できます。また、カメラをpanzoomモードに設定することで、ユーザーがビューを操作できるようになっています。

## 第4章: データの可視化

VisPyを使用してデータを可視化する方法を探ります。散布図、線グラフ、ヒストグラムなど、さまざまなタイプのプロットを作成する方法を学びます。

```python
from vispy import app, scene
import numpy as np

canvas = scene.SceneCanvas(keys='interactive', size=(800, 600), show=True)
grid = canvas.central_widget.add_grid()

# 散布図
scatter_view = grid.add_view(row=0, col=0)
scatter_data = np.random.normal(size=(1000, 2))
scatter = scene.visuals.Markers()
scatter.set_data(scatter_data, edge_color=None, size=5)
scatter_view.add(scatter)
scatter_view.camera = 'panzoom'
scatter_view.camera.set_range()

# 線グラフ
line_view = grid.add_view(row=0, col=1)
x = np.linspace(0, 10, 100)
y = np.sin(x)
line = scene.visuals.Line(pos=np.column_stack([x, y]), color='red', width=2)
line_view.add(line)
line_view.camera = 'panzoom'
line_view.camera.set_range()

# ヒストグラム
hist_view = grid.add_view(row=1, col=0, colspan=2)
hist_data = np.random.normal(size=1000)
hist, edges = np.histogram(hist_data, bins=50)
hist_plot = scene.visuals.Histogram(hist, edges, orientation='h', color='blue')
hist_view.add(hist_plot)
hist_view.camera = 'panzoom'
hist_view.camera.set_range()

if __name__ == '__main__':
    app.run()
```

この例では、散布図、線グラフ、ヒストグラムを1つのキャンバス上に配置しています。グリッドレイアウトを使用して、各プロットを適切に配置しています。散布図ではMarkersクラスを、線グラフではLineクラスを、ヒストグラムではHistogramクラスを使用しています。各ビューにpanzoomカメラを設定することで、ユーザーが各プロットを個別に操作できるようになっています。

## 第5章: 3D可視化

VisPyの3D可視化機能を探索します。3Dプロット、サーフェスプロット、ボリュームレンダリングなどの高度な3D可視化技術を学びます。

```python
from vispy import app, scene
import numpy as np

canvas = scene.SceneCanvas(keys='interactive', title='VisPy 3D可視化', size=(800, 600), show=True)
view = canvas.central_widget.add_view()

# 3D散布図
pos = np.random.normal(size=(1000, 3))
scatter = scene.visuals.Markers()
scatter.set_data(pos, edge_color=None, size=5)
view.add(scatter)

# 3Dサーフェスプロット
x, y = np.mgrid[-8:8:100j, -8:8:100j]
z = 5 * np.sin(x/2) * np.cos(y/2) / ((x/2)**2 + (y/2)**2 + 0.1)
surface = scene.visuals.SurfacePlot(x=x, y=y, z=z, color=(0.5, 0.5, 1, 1))
view.add(surface)

# 3D線
t = np.linspace(0, 10, 1000)
x = np.cos(t)
y = np.sin(t)
z = t / 3
line = scene.visuals.Line(pos=np.column_stack([x, y, z]), color='red', width=2)
view.add(line)

# カメラ設定
view.camera = 'turntable'
view.camera.fov = 45
view.camera.azimuth = 30
view.camera.elevation = 30

if __name__ == '__main__':
    app.run()
```

この例では、3D散布図、3Dサーフェスプロット、3D線を1つの3Dシーンに組み合わせています。散布図にはMarkersクラス、サーフェスプロットにはSurfacePlotクラス、3D線にはLineクラスを使用しています。'turntable'カメラを使用することで、ユーザーがマウスで3Dシーンを回転させたり、ズームしたりできるようになっています。

## 第6章: インタラクティブ機能

VisPyのインタラクティブ機能を探ります。マウスイベント、キーボードイベント、ピッキング（オブジェクトの選択）などの機能を実装する方法を学びます。

```python
from vispy import app, scene
import numpy as np

canvas = scene.SceneCanvas(keys='interactive', size=(800, 600), show=True)
view = canvas.central_widget.add_view()

# 散布図データ
n = 100
pos = np.random.normal(size=(n, 2))
colors = np.ones((n, 4), dtype=np.float32)
colors[:, 0] = np.linspace(0, 1, n)
colors[:, 1] = np.random.uniform(0.5, 1, n)
colors[:, 2] = np.random.uniform(0, 0.5, n)

# 散布図の作成
scatter = scene.visuals.Markers()
scatter.set_data(pos, edge_color=None, face_color=colors, size=10)
view.add(scatter)

# テキスト表示用
text = scene.visuals.Text('', pos=(10, 10), color='white', parent=canvas.scene)

@canvas.events.mouse_move.connect
def on_mouse_move(event):
    # マウス位置の取得
    tr = canvas.scene.node_transform(scatter)
    pos = tr.map(event.pos)
    text.text = f'マウス位置: {pos[0]:.2f}, {pos[1]:.2f}'

@canvas.events.mouse_press.connect
def on_mouse_press(event):
    if event.button == 1:  # 左クリック
        # クリックされた点を探す
        click_pos = event.pos
        data = scatter.data
        distances = np.sqrt(np.sum((data - click_pos)**2, axis=1))
        nearest = np.argmin(distances)
        
        # 色を変更
        colors[nearest] = [1, 0, 0, 1]  # 赤色に変更
        scatter.set_data(pos, edge_color=None, face_color=colors, size=10)

view.camera = 'panzoom'
view.camera.set_range()

if __name__ == '__main__':
    app.run()
```

この例では、マウスの動きに応じてテキストを更新し、散布図上の点をクリックすると色が変わるインタラクティブな機能を実装しています。`canvas.events.mouse_move`と`canvas.events.mouse_press`イベントを使用して、マウスの動きとクリックを検出しています。また、ピッキング機能を実装して、クリックされた点を特定し、その色を変更しています。

## 第7章: シェーダーの基本

VisPyでのシェーダーの使用方法を学びます。頂点シェーダーとフラグメントシェーダーの基本、GLSLの基本構文、シェーダーを使用した簡単なエフェクトの作成方法を説明します。

```python
from vispy import app, gloo
import numpy as np

vertex = """
attribute vec2 position;
attribute vec3 color;
varying vec3 v_color;
uniform float time;

void main() {
    // 時間に基づいて位置を変更
    vec2 pos = position;
    pos.x += 0.1 * sin(time + position.y);
    pos.y += 0.1 * cos(time + position.x);
    gl_Position = vec4(pos, 0.0, 1.0);
    v_color = color;
}
"""

fragment = """
varying vec3 v_color;

void main() {
    gl_FragColor = vec4(v_color, 1.0);
}
"""

class Canvas(app.Canvas):
    def __init__(self):
        super().__init__(size=(800, 600), title="シェーダー基本")
        self.program = gloo.Program(vertex, fragment)

        # 頂点データ
        n = 1000
        position = np.random.uniform(-1, 1, (n, 2)).astype(np.float32)
        color = np.random.uniform(0, 1, (n, 3)).astype(np.float32)

        self.program['position'] = position
        self.program['color'] = color

        self.timer = app.Timer('auto', connect=self.on_timer, start=True)
        self.time = 0

    def on_draw(self, event):
        gloo.clear('black')
        self.program.draw('points')

    def on_timer(self, event):
        self.time += 0.01
        self.program['time'] = self.time
        self.update()

if __name__ == '__main__':
    c = Canvas()
    c.show()
    app.run()
```

この例では、頂点シェーダーとフラグメントシェーダーを使用して、動的な点のアニメーションを作成しています。頂点シェーダーでは、時間に基づいて各点の位置を変更し、波のような動きを生成しています。フラグメントシェーダーは、各点の色を設定しています。

シェーダーは、GPUで直接実行されるため、非常に効率的に大量の点をアニメーション化することができます。この例では、1000個の点をスムーズにアニメーション化しています。

## 第8章: 高度なシェーダーテクニック

より複雑なシェーダーエフェクトを作成する方法を学びます。テクスチャの使用、ライティング計算、ノイズ関数の実装など、高度なシェーダーテクニックを探ります。

```python
from vispy import app, gloo
import numpy as np

vertex = """
attribute vec2 position;
attribute vec2 texcoord;
varying vec2 v_texcoord;
uniform float time;

void main() {
    vec2 pos = position;
    pos.x += 0.1 * sin(time + position.y * 10.0);
    gl_Position = vec4(pos, 0.0, 1.0);
    v_texcoord = texcoord;
}
"""

fragment = """
uniform sampler2D texture;
varying vec2 v_texcoord;
uniform float time;

// 簡単なノイズ関数
float noise(vec2 st) {
    return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);
}

void main() {
    vec2 uv = v_texcoord;
    
    // テクスチャからの色取得
    vec4 color = texture2D(texture, uv);
    
    // ノイズを追加
    float n = noise(uv * 10.0 + time);
    color.rgb += 0.1 * vec3(n);
    
    // 時間に基づく色の変化
    color.r += 0.2 * sin(time + uv.x * 10.0);
    color.g += 0.2 * cos(time + uv.y * 10.0);
    
    gl_FragColor = color;
}
"""

class Canvas(app.Canvas):
    def __init__(self):
        super().__init__(size=(800, 600), title="高度なシェーダーテクニック")
        self.program = gloo.Program(vertex, fragment)

        # 頂点データ
        self.program['position'] = [(-1, -1), (-1, 1), (1, -1), (1, 1)]
        self.program['texcoord'] = [(0, 0), (0, 1), (1, 0), (1, 1)]

        # テクスチャの作成
        self.texture = gloo.Texture2D(np.random.rand(256, 256, 3).astype(np.float32))
        self.program['texture'] = self.texture

        self.timer = app.Timer('auto', connect=self.on_timer, start=True)
        self.time = 0

    def on_draw(self, event):
        gloo.clear('black')
        self.program.draw('triangle_strip')

    def on_timer(self, event):
        self.time += 0.01
        self.program['time'] = self.time
        self.update()

if __name__ == '__main__':
    c = Canvas()
    c.show()
    app.run()
```

この例では、より高度なシェーダーテクニックを使用しています。頂点シェーダーでは、頂点の位置を時間に基づいて変更し、波のような効果を作成しています。フラグメントシェーダーでは、以下の技術を組み合わせています：

1. テクスチャマッピング：ランダムに生成されたテクスチャを使用しています。
2. ノイズ関数：簡単なノイズ関数を実装し、テクスチャに追加のディテールを加えています。
3. 時間ベースのアニメーション：色の値を時間に基づいて変更し、動的な効果を作成しています。

これらの技術を組み合わせることで、複雑で動的な視覚効果を作成しています。シェーダーを使用することで、CPUではなくGPUで直接これらの計算を行うため、非常に効率的に複雑な視覚効果を実現できます。

## 第9章: パフォーマンス最適化

VisPyを使用して大規模なデータセットを効率的に可視化する方法を学びます。データのダウンサンプリング、レベルオブディテール（LOD）技術、GPUバッファの効率的な使用など、パフォーマンスを最適化するテクニックを探ります。

```python
from vispy import app, scene, gloo
import numpy as np

# 大規模なデータセットの生成
N = 1000000
data = np.random.normal(size=(N, 3)).astype(np.float32)

vertex = """
attribute vec3 position;
attribute float size;

void main() {
    gl_Position = $transform(vec4(position, 1.0));
    gl_PointSize = size;
}
"""

fragment = """
void main() {
    float r = length(gl_PointCoord - vec2(0.5));
    if (r > 0.5)
        discard;
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
"""

class OptimizedScatter(scene.visuals.Visual):
    def __init__(self):
        scene.visuals.Visual.__init__(self, vertex, fragment)
        self.set_gl_state('translucent', depth_test=True, cull_face=False)
        self._draw_mode = 'points'
        self.set_data(data)

    def set_data(self, pos, size=10):
        self.shared_program['position'] = pos
        self.shared_program['size'] = size

    def _prepare_transforms(self, view):
        view.view_program.vert['transform'] = view.get_transform()

OptimizedScatter = scene.visuals.create_visual_node(OptimizedScatter)

canvas = scene.SceneCanvas(keys='interactive', show=True)
view = canvas.central_widget.add_view()
scatter = OptimizedScatter()
view.add(scatter)

view.camera = 'turntable'
view.camera.set_range()

def update(event):
    global data
    # データの一部を更新
    data[:1000] += np.random.normal(size=(1000, 3), scale=0.01).astype(np.float32)
    scatter.set_data(data)
    canvas.update()

timer = app.Timer(connect=update, interval=0.05)
timer.start()

if __name__ == '__main__':
    app.run()
```

この例では、100万個の点を含む大規模な3D散布図を効率的に描画しています。パフォーマンスを最適化するために、以下の技術を使用しています：

1. カスタムビジュアルクラス：`OptimizedScatter`クラスを作成し、データの描画を最適化しています。

2. シェーダーの最適化：頂点シェーダーとフラグメントシェーダーを使用して、GPUで直接点を描画しています。

3. 点の描画最適化：フラグメントシェーダーで円形の点を描画し、不要な部分を`discard`しています。

4. データの部分更新：毎フレームでデータの一部のみを更新することで、CPU-GPU間のデータ転送を最小限に抑えています。

5. ビューの最適化：'turntable'カメラを使用して、3Dシーンの操作を効率化しています。

これらの最適化により、非常に大規模なデータセットでもスムーズな描画とインタラクションが可能になります。実際のアプリケーションでは、データのダウンサンプリングやレベルオブディテール（LOD）技術を追加することで、さらにパフォーマンスを向上させることができます。

## 第10章: カスタムビジュアルの作成

VisPyを使用してカスタムビジュアルを作成する方法を学びます。基本的なビジュアルクラスの構造、シェーダーの統合、データの設定方法などを説明します。

```python
from vispy import app, scene, gloo
import numpy as np

vertex = """
attribute vec2 position;
attribute float phase;
uniform float time;
varying float v_phase;

void main() {
    float t = time + phase;
    vec2 pos = position;
    pos.y += 0.1 * sin(5.0 * t);
    gl_Position = vec4(pos, 0.0, 1.0);
    v_phase = phase;
}
"""

fragment = """
varying float v_phase;
uniform float time;

void main() {
    float t = time + v_phase;
    float r = 0.5 + 0.5 * sin(t);
    float g = 0.5 + 0.5 * sin(t + 2.094);
    float b = 0.5 + 0.5 * sin(t + 4.189);
    gl_FragColor = vec4(r, g, b, 1.0);
}
"""

class WaveVisual(scene.visuals.Visual):
    def __init__(self, n_points=1000):
        scene.visuals.Visual.__init__(self, vertex, fragment)

        # データの生成
        position = np.zeros((n_points, 2), dtype=np.float32)
        position[:, 0] = np.linspace(-1, 1, n_points)
        phase = np.linspace(0, 2*np.pi, n_points).astype(np.float32)

        # データの設定
        self.shared_program['position'] = position
        self.shared_program['phase'] = phase
        self._draw_mode = 'line_strip'

        # タイマーの設定
        self.time = 0
        self.timer = app.Timer('auto', connect=self.update_time, start=True)

    def update_time(self, event):
        self.time += 0.01
        self.shared_program['time'] = self.time
        self.update()

    def _prepare_transforms(self, view):
        view.view_program.vert['transform'] = view.get_transform()

WaveVisual = scene.visuals.create_visual_node(WaveVisual)

canvas = scene.SceneCanvas(keys='interactive', show=True)
view = canvas.central_widget.add_view()
wave = WaveVisual()
view.add(wave)

view.camera = 'panzoom'
view.camera.set_range()

if __name__ == '__main__':
    app.run()
```

この例では、カスタムビジュアル「WaveVisual」を作成しています。このビジュアルは、時間とともに色が変化する波形を描画します。カスタムビジュアルの作成プロセスには以下の要素が含まれています：

1. シェーダーの定義：頂点シェーダーとフラグメントシェーダーを定義して、波形の形状と色を制御します。

2. ビジュアルクラスの作成：`scene.visuals.Visual`を継承して、カスタムビジュアルクラスを作成します。

3. データの生成と設定：ビジュアルに必要なデータ（この場合は点の位置と位相）を生成し、シェーダープログラムに設定します。

4. 描画モードの設定：`_draw_mode`を設定して、ビジュアルの描画方法を指定します。

5. アニメーションの実装：タイマーを使用して、時間パラメータを更新し、ビジュアルをアニメーション化します。

6. トランスフォームの準備：`_prepare_transforms`メソッドを実装して、ビューの変換をシェーダーに適用します。

このカスタムビジュアルは、標準のVisPyビジュアルと同様に使用でき、シーンに追加したり、他のビジュアルと組み合わせたりすることができます。カスタムビジュアルを作成することで、VisPyの機能を拡張し、特定のニーズに合わせた独自の可視化を実現することができます。

## 第11章: イベント処理とインタラクション

VisPyでのイベント処理とユーザーインタラクションの実装方法を学びます。マウス、キーボード、タッチイベントの処理、カスタムイベントの作成、そしてインタラクティブな可視化の構築方法を説明します。

```python
from vispy import app, scene
import numpy as np

canvas = scene.SceneCanvas(keys='interactive', size=(800, 600), show=True)
view = canvas.central_widget.add_view()

# 散布図データの生成
n = 1000
pos = np.random.normal(size=(n, 2), scale=0.2)
colors = np.ones((n, 4), dtype=np.float32)
colors[:, 0] = np.linspace(0, 1, n)
colors[:, 1] = np.random.uniform(0.5, 1, n)
colors[:, 2] = np.random.uniform(0, 0.5, n)

# 散布図の作成
scatter = scene.visuals.Markers()
scatter.set_data(pos, edge_color=None, face_color=colors, size=10)
view.add(scatter)

# テキスト表示用
text = scene.visuals.Text('', pos=(10, 10), color='white', parent=canvas.scene)

# 選択された点を保存するリスト
selected_points = []

@canvas.events.mouse_move.connect
def on_mouse_move(event):
    # マウス位置の取得と表示
    tr = canvas.scene.node_transform(scatter)
    pos = tr.map(event.pos)
    text.text = f'マウス位置: {pos[0]:.2f}, {pos[1]:.2f}'

@canvas.events.mouse_press.connect
def on_mouse_press(event):
    if event.button == 1:  # 左クリック
        # クリックされた点を探す
        click_pos = event.pos
        data = scatter.data
        distances = np.sqrt(np.sum((data - click_pos)**2, axis=1))
        nearest = np.argmin(distances)
        
        if nearest not in selected_points:
            selected_points.append(nearest)
            colors[nearest] = [1, 0, 0, 1]  # 赤色に変更
        else:
            selected_points.remove(nearest)
            colors[nearest] = [0, 1, 0, 1]  # 緑色に変更
        
        scatter.set_data(pos, edge_color=None, face_color=colors, size=10)

@canvas.events.key_press.connect
def on_key_press(event):
    global pos, colors
    if event.key == 'R':
        # データのリセット
        pos = np.random.normal(size=(n, 2), scale=0.2)
        colors = np.ones((n, 4), dtype=np.float32)
        colors[:, 0] = np.linspace(0, 1, n)
        colors[:, 1] = np.random.uniform(0.5, 1, n)
        colors[:, 2] = np.random.uniform(0, 0.5, n)
        scatter.set_data(pos, edge_color=None, face_color=colors, size=10)
        selected_points.clear()
    elif event.key == 'S':
        # 選択された点の保存
        if selected_points:
            np.savetxt('selected_points.txt', pos[selected_points])
            print(f'{len(selected_points)}個の点を保存しました。')

view.camera = 'panzoom'
view.camera.set_range()

if __name__ == '__main__':
    app.run()
```

この例では、以下のようなイベント処理とインタラクション機能を実装しています：

1. マウス移動イベント（`mouse_move`）：
   - マウスの現在位置を取得し、キャンバス上に表示します。

2. マウスクリックイベント（`mouse_press`）：
   - 左クリックで最も近い点を選択または選択解除します。
   - 選択された点は赤色に、選択解除された点は緑色に変更されます。

3. キーボードイベント（`key_press`）：
   - 'R'キーを押すとデータをリセットします。
   - 'S'キーを押すと選択された点のデータを保存します。

4. カメラ操作：
   - 'panzoom'カメラを使用して、ユーザーがビューをパンやズームできるようにしています。

これらの機能により、ユーザーは散布図と対話的にやり取りし、データを探索したり、特定の点を選択したりすることができます。

イベント処理とインタラクション機能を追加することで、静的な可視化をダイナミックで対話的なものに変えることができます。これにより、ユーザーはデータをより深く理解し、隠れたパターンや関係性を発見することができます。

## 第12章: 複数のビューとレイアウト

VisPyを使用して複数のビューを作成し、複雑なレイアウトを構築する方法を学びます。グリッドレイアウト、分割ビュー、ネストされたレイアウトなどの技術を探ります。

```python
from vispy import app, scene
import numpy as np

canvas = scene.SceneCanvas(keys='interactive', size=(800, 600), show=True)
grid = canvas.central_widget.add_grid()

# 散布図ビュー
scatter_view = grid.add_view(row=0, col=0)
scatter_data = np.random.normal(size=(1000, 2))
scatter = scene.visuals.Markers()
scatter.set_data(scatter_data, edge_color=None, face_color=(1, 0, 0, 0.5), size=5)
scatter_view.add(scatter)
scatter_view.camera = 'panzoom'
scatter_view.camera.set_range()
scatter_view.border_color = (0.5, 0.5, 0.5, 1)

# ヒストグラムビュー（X軸）
hist_x_view = grid.add_view(row=1, col=0)
hist_x_data, _ = np.histogram(scatter_data[:, 0], bins=50)
hist_x = scene.visuals.Histogram(hist_x_data, color='blue', orientation='horizontal')
hist_x_view.add(hist_x)
hist_x_view.camera = 'panzoom'
hist_x_view.camera.set_range(xrange=scatter_view.camera.get_range()[0])
hist_x_view.border_color = (0.5, 0.5, 0.5, 1)

# ヒストグラムビュー（Y軸）
hist_y_view = grid.add_view(row=0, col=1)
hist_y_data, _ = np.histogram(scatter_data[:, 1], bins=50)
hist_y = scene.visuals.Histogram(hist_y_data, color='green', orientation='vertical')
hist_y_view.add(hist_y)
hist_y_view.camera = 'panzoom'
hist_y_view.camera.set_range(yrange=scatter_view.camera.get_range()[1])
hist_y_view.border_color = (0.5, 0.5, 0.5, 1)

# 3Dビュー
view_3d = grid.add_view(row=1, col=1)
sphere = scene.visuals.Sphere(radius=1, method='latitude', parent=view_3d.scene)
view_3d.camera = 'turntable'
view_3d.border_color = (0.5, 0.5, 0.5, 1)

# グリッドの設定
grid.spacing = 0
grid.margin = 10

# ビューのリンク
scatter_view.camera.link(hist_x_view.camera)
scatter_view.camera.link(hist_y_view.camera)

@canvas.events.mouse_move.connect
def on_mouse_move(event):
    if event.button == 1 and event.is_dragging:
        for view in [scatter_view, hist_x_view, hist_y_view]:
            view.camera.viewbox_key_event(event)

if __name__ == '__main__':
    app.run()
```

この例では、複数のビューを使用して複雑なレイアウトを作成しています。主な特徴は以下の通りです：

1. グリッドレイアウト：
   - `canvas.central_widget.add_grid()`を使用して、4つのビューを2x2のグリッドに配置しています。

2. 複数のビュータイプ：
   - 散布図、ヒストグラム（X軸とY軸）、3D球体を異なるビューに表示しています。

3. ビューのリンク：
   - 散布図ビューのカメラをヒストグラムビューのカメラにリンクさせ、同期したパンとズームを実現しています。

4. カスタムイベント処理：
   - マウスドラッグイベントを処理して、リンクされたビュー間でパンとズームを同期させています。

5. ビューの装飾：
   - 各ビューにボーダーカラーを設定して、視覚的に区別しやすくしています。

6. レイアウトの調整：
   - グリッドのスペーシングとマージンを設定して、ビュー間の間隔を調整しています。

このような複雑なレイアウトを使用することで、データの異なる側面を同時に表示し、より豊かな情報を提供することができます。例えば、この例では2D散布図とそれに対応するヒストグラム、さらに3Dオブジェクトを同時に表示しています。

複数のビューとレイアウトを効果的に使用することで、データの多面的な分析が可能になり、ユーザーはデータ間の関係をより直感的に理解することができます。

## 第13章: データストリーミングと動的更新

VisPyを使用してリアルタイムデータの可視化を行う方法を学びます。データストリーミング、動的なデータ更新、アニメーションの実装方法を説明します。

```python
from vispy import app, scene
import numpy as np
import time

# キャンバスの設定
canvas = scene.SceneCanvas(keys='interactive', show=True)
grid = canvas.central_widget.add_grid()
view1 = grid.add_view(row=0, col=0)
view2 = grid.add_view(row=1, col=0)

# 時系列データの初期化
n_points = 1000
time_data = np.linspace(0, 10, n_points)
signal_data = np.zeros(n_points)

# ラインプロットの作成
line1 = scene.visuals.Line(pos=np.column_stack((time_data, signal_data)), color='red', parent=view1.scene)
line2 = scene.visuals.Line(pos=np.column_stack((time_data, signal_data)), color='blue', parent=view2.scene)

# カメラの設定
view1.camera = scene.PanZoomCamera(rect=(0, -1.1, 10, 2.2))
view2.camera = scene.PanZoomCamera(rect=(0, -1.1, 10, 2.2))

# テキスト表示
text1 = scene.visuals.Text('Signal 1', pos=(5, 1), color='white', parent=view1.scene)
text2 = scene.visuals.Text('Signal 2', pos=(5, 1), color='white', parent=view2.scene)

# データ更新関数
def update_data(frame):
    global signal_data
    t = time.time()
    new_data = np.sin(2 * np.pi * 0.1 * t + time_data) + 0.1 * np.random.randn(n_points)
    signal_data = 0.9 * signal_data + 0.1 * new_data
    
    line1.set_data(pos=np.column_stack((time_data, signal_data)))
    line2.set_data(pos=np.column_stack((time_data, np.cumsum(signal_data) / n_points)))
    
    text1.text = f'Signal 1: {signal_data[-1]:.2f}'
    text2.text = f'Signal 2: {np.cumsum(signal_data)[-1] / n_points:.2f}'
    
    canvas.update()

# アニメーションタイマーの設定
timer = app.Timer(interval=0.05, connect=update_data, start=True)

if __name__ == '__main__':
    app.run()
```

この例では、リアルタイムデータストリーミングと動的更新を実装しています。主な特徴は以下の通りです：

1. 複数のビュー：
   - 2つのビューを使用して、異なる信号を同時に表示しています。

2. 時系列データの初期化：
   - 時間軸と信号データを初期化し、ラインプロットを作成します。

3. 動的データ更新：
   - `update_data`関数内で、新しいデータポイントを生成し、既存のデータと組み合わせています。
   - 一つ目の信号は正弦波とノイズの組み合わせ、二つ目の信号は一つ目の信号の累積和です。

4. データの視覚化更新：
   - `line1.set_data`と`line2.set_data`を使用して、プロットのデータを更新しています。

5. テキスト更新：
   - 各信号の最新値をテキストとして表示し、毎フレーム更新しています。

6. アニメーションタイマー：
   - `app.Timer`を使用して、定期的に`update_data`関数を呼び出し、アニメーションを実現しています。


このアプローチにより、連続的に変化するデータをリアルタイムで可視化することができます。これは、センサーデータのモニタリング、金融市場のトラッキング、科学実験の結果の可視化など、様々な分野で活用できます。

## 第14章: 3Dボリュームレンダリング

VisPyを使用して3Dボリュームデータを可視化する方法を学びます。医療画像やシミュレーションデータなどの3D空間データを効果的に表現する技術を探ります。

```python
import numpy as np
from vispy import app, scene
from vispy.color import get_colormaps

# ボリュームデータの生成
def make_volume():
    size = 64, 64, 64
    data = np.zeros(size, dtype=np.float32)
    for i in range(size[0]):
        for j in range(size[1]):
            for k in range(size[2]):
                x, y, z = i/size[0], j/size[1], k/size[2]
                data[i, j, k] = np.sin(10*x)*np.cos(10*y)*np.sin(10*z)
    return data

# キャンバスの設定
canvas = scene.SceneCanvas(keys='interactive', size=(800, 600), show=True)
view = canvas.central_widget.add_view()

# ボリュームの作成
volume = make_volume()
volume_data = scene.visuals.Volume(volume, parent=view.scene, threshold=0.1, emulate_texture=False)

# カメラの設定
view.camera = scene.TurntableCamera(fov=60, azimuth=30, elevation=30)

# 転送関数の設定
cmap = get_colormaps()['viridis']
volume_data.cmap = cmap

# カラーバーの追加
scene.visuals.ColorBar(cmap, orientation='right', label_color='white', parent=view.scene, 
                       pos=(0.9, 0.1), size=(0.03, 0.8))

# クリッピングプレーンの設定
volume_data.set_clip_plane((1, 0, 0, 0))

# インタラクション関数
@canvas.events.key_press.connect
def on_key_press(event):
    if event.key == 'C':
        # クリッピングプレーンの切り替え
        if volume_data.clip_plane is None:
            volume_data.set_clip_plane((1, 0, 0, 0))
        else:
            volume_data.set_clip_plane(None)
    elif event.key == 'T':
        # しきい値の変更
        current_threshold = volume_data.threshold
        volume_data.threshold = (current_threshold + 0.1) % 1.0

# 説明テキストの追加
text = scene.visuals.Text('Press C to toggle clipping plane\nPress T to change threshold', 
                          parent=canvas.scene, color='white', pos=(10, 10), font_size=12)

if __name__ == '__main__':
    app.run()
```

この例では、3Dボリュームレンダリングを実装しています。主な特徴は以下の通りです：

1. ボリュームデータの生成：
   - `make_volume`関数で3D空間内の正弦波パターンを生成しています。

2. ボリュームの可視化：
   - `scene.visuals.Volume`を使用して、3Dボリュームデータを可視化しています。

3. カメラ設定：
   - `TurntableCamera`を使用して、ユーザーがボリュームを回転させて見ることができるようにしています。

4. 転送関数とカラーマップ：
   - 'viridis'カラーマップを使用して、データ値を色に変換しています。
   - カラーバーを追加して、値と色の対応を表示しています。

5. クリッピングプレーン：
   - ボリュームの一部を切り取って内部を見ることができるクリッピングプレーンを実装しています。

6. インタラクティブ機能：
   - 'C'キーでクリッピングプレーンの切り替え、'T'キーでしきい値の変更ができるようにしています。

7. 説明テキスト：
   - キー操作の説明をキャンバス上に表示しています。

このアプローチにより、複雑な3Dデータを効果的に可視化し、ユーザーがインタラクティブに探索できるようになります。医療画像（CT、MRIなど）、地球科学データ、流体力学シミュレーションなど、多くの分野で3Dボリュームレンダリングは重要な役割を果たします。

VisPyの強力なGPUアクセラレーションにより、大規模な3Dデータセットでもスムーズなレンダリングと操作が可能になります。

## 第15章: 高度なビジュアル効果とシェーダー

最後に、VisPyでより高度なビジュアル効果を作成する方法を探ります。カスタムシェーダーの作成、後処理効果の適用、複雑な照明モデルの実装など、高度なテクニックを学びます。

```python
import numpy as np
from vispy import app, gloo, visuals, scene

# カスタム頂点シェーダー
vertex = """
uniform mat4 view;
uniform mat4 model;
uniform mat4 projection;
attribute vec3 position;
attribute vec3 normal;
attribute vec2 texcoord;
varying vec3 v_position;
varying vec3 v_normal;
varying vec2 v_texcoord;

void main() {
    v_normal = normal;
    v_position = position;
    v_texcoord = texcoord;
    gl_Position = projection * view * model * vec4(position, 1.0);
}
"""

# カスタムフラグメントシェーダー
fragment = """
uniform vec3 light_position;
uniform vec3 light_color;
uniform sampler2D texture;
uniform float time;
varying vec3 v_position;
varying vec3 v_normal;
varying vec2 v_texcoord;

void main() {
    vec3 normal = normalize(v_normal);
    vec3 light_dir = normalize(light_position - v_position);
    float diffuse = max(dot(normal, light_dir), 0.0);
    
    vec3 color = texture2D(texture, v_texcoord).rgb;
    
    // 時間に基づく波紋効果
    float dist = length(v_texcoord - vec2(0.5, 0.5));
    float ripple = sin(dist * 50.0 - time * 5.0) * 0.5 + 0.5;
    
    color *= mix(vec3(1.0), light_color, diffuse);
    color *= ripple;
    
    gl_FragColor = vec4(color, 1.0);
}
"""

class CustomMesh(visuals.Visual):
    def __init__(self):
        visuals.Visual.__init__(self, vertex, fragment)
        
        # メッシュデータの生成
        vertices, faces, normals, texcoords = create_sphere(radius=1, rows=100, cols=100)
        
        self.mesh = gloo.IndexBuffer(faces)
        self.program.bind(gloo.VertexBuffer(vertices))
        self.program['normal'] = gloo.VertexBuffer(normals)
        self.program['texcoord'] = gloo.VertexBuffer(texcoords)
        
        # テクスチャの生成
        texture = create_texture()
        self.program['texture'] = gloo.Texture2D(texture)
        
        self.program['light_position'] = [0, 0, 2]
        self.program['light_color'] = [1, 1, 1]
        
        self.time = 0
        
    def _prepare_transforms(self, view):
        view.view_program.vert['model'] = view.get_transform()
        view.view_program.vert['view'] = view.view
        view.view_program.vert['projection'] = view.projection
        
    def _prepare_draw(self, view):
        self.program['time'] = self.time
        self.time += 0.01
        
    def draw(self):
        gloo.set_state(depth_test=True, cull_face=True)
        self.program.draw('triangles', self.mesh)
        
CustomMesh = scene.visuals.create_visual_node(CustomMesh)

def create_sphere(radius, rows, cols):
    # 球体メッシュの生成（詳細は省略）
    # 実際には、球体の頂点、面、法線、テクスチャ座標を計算して返す
    pass

def create_texture():
    # テクスチャの生成（詳細は省略）
    # 実際には、任意のテクスチャデータを生成して返す
    pass

canvas = scene.SceneCanvas(keys='interactive', size=(800, 600), show=True)
view = canvas.central_widget.add_view()
mesh = CustomMesh()
view.add(mesh)
view.camera = 'turntable'

if __name__ == '__main__':
    app.run()
```

この例では、高度なビジュアル効果とカスタムシェーダーを使用しています。主な特徴は以下の通りです：

1. カスタムシェーダー：
   - 頂点シェーダーとフラグメントシェーダーをカスタマイズして、独自の視覚効果を実現しています。

2. 複雑な照明モデル：
   - フラグメントシェーダーで簡単な拡散反射モデルを実装しています。

3. テクスチャマッピング：
   - 球体にテクスチャを適用し、シェーダーで処理しています。

4. 動的エフェクト：
   - 時間に基づく波紋効果を実装し、テクスチャに動きを加えています。

5. カスタムビジュアルクラス：
   - `visuals.Visual`を継承して、カスタムメッシュクラスを作成しています。

6. 3Dジオメトリ：
   - 球体メッシュを生成し、カスタムシェーダーで描画しています。

このアプローチにより、非常に柔軟で高度なビジュアル効果を作成することができます。GPUの力を直接利用することで、複雑な計算や効果をリアルタイムで適用することが可能になります。

高度なシェーダーとビジュアル効果は、科学的可視化、データアート、インタラクティブインスタレーションなど、様々な分野で活用できます。VisPyの低レベルAPIを使用することで、OpenGLの機能を最大限に活用しつつ、Pythonの柔軟性も享受することができます。

以上で、VisPyに関する15章の詳細な解説が完了しました。この内容を通じて、VisPyの基本から高度な使用方法まで、幅広く学ぶことができたと思います。