[iOS]FRD3DBarChartで3Dグラフを図示してみる

2014.05.16

iOSのグラフプロットライブラリ

Cocoapodsにある豊富なOSSのライブラリの海に潜ってみればさまざまなグラフのプロットライブラリが見つかります。

$ pod search plot
$ pod search graph

二次元上に棒グラフや折れ線グラフ、円グラフなどを描画するライブラリはだいたいこのサーチで出てきたライブラリで間に合わせられます。有名どころとしてはCorePlotがあります。

三次元空間上のグラフを描画するライブラリに関してもCocoapodのOSSとして登録されており、それが今回紹介するFRD3DBarChartになります。

ライセンス

修正BSD

導入法

Podfile

pod 'FRD3DBarChart'

をプロジェクトディレクトリ配下においた上で

$ pod install

です。

実装例

今回の実装例のサンプルコードはGithubにあげてあります。サンプルコードでは

の二点を取り上げています。早速例を見ていきましょう。

描画ViewControllerの実装

  1. Storyboard上でUIViewControllerサブクラスのviewにContainerViewを追加します。
  2. 次にFRD3DBarChartViewControllerをStoryboard上に用意します。(UIViewControllerを加えてクラス名をFRD3DBarChartViewControllerに変更, 中のViewのクラス名をGLKViewに変更)
  3. ContainerViewからEmbed-Segueをひっぱってきて作成したFRD3DBarChartViewControllerにつなぎ、SegueIdentifierをFRD3DBarChartViewControllerにリネームします。

storyboard

UIViewControllerサブクラスの実装から3DBarChartの表示設定の部分を抜粋します。

MYGaussianChartViewController.m

#import "MYGaussianChartViewController.h"

#import "MYGaussianDistributionDataSource.h"

#import "FRD3DBarChartViewController.h"

@interface MYGaussianChartViewController ()

/**
 *  コンテナビューに入れられたチャート表示のためのViewControllerです。
 */
@property (weak, nonatomic) FRD3DBarChartViewController *chartViewController;

/**
 *  チャート表示に必要なデータを提供するデータソースオブジェクトです。
 */
@property (nonatomic) id<FRD3DBarChartViewControllerDelegate> chartDataSource;

@end

@implementation MYGaussianChartViewController

#pragma mark - Lifecycle methods

- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder:aDecoder];
    if (self) {
        _chartDataSource = [MYGaussianDistributionDataSource new];
    }
    return self;
}

#pragma mark - UIViewController methods

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([segue.identifier isEqualToString:NSStringFromClass([FRD3DBarChartViewController class])]) {
        MYGaussianDistributionDataSource *chartDataSource = self.chartDataSource;
        chartDataSource.maxValue = 0.20f;

        self.chartViewController = segue.destinationViewController;
        self.chartViewController.frd3dBarChartDelegate = chartDataSource;
    }
}

まず、MYGaussianChartViewControllerを初期化するタイミングでFRD3DBarChartViewControllerの表示に必要なデータ・ソースモデルクラスをセットしています。

MYGaussianChartViewControllerのviewがロードされる前にcontainerviewに繋がれたsegueによってprepareForSegueが呼び出されています。このメソッド内でSegueの識別子を判断してchartViewControllerへの設定を追加しています。

frd3dBarChartDelegateのプロパティにデリゲードメソッドを実装したクラスのインスタンスを設定します。このデリゲードは実質的にデータソースプロトコルなのですが、今回の実装例ではデータソースを別クラスとして切り出しています。そちらの実装を見ていきます。

デリゲード(データ・ソース)系メソッドの実装

FRD3DBarChartViewControllerDelegateの実装必須プロトコルメソッドは次の4つです。

  • バーの行数を要求するメソッド
-(int) frd3DBarChartViewControllerNumberRows:(FRD3DBarChartViewController *) frd3DBarChartViewController;
  • バーの列数を要求するメソッド
-(int) frd3DBarChartViewControllerNumberColumns:(FRD3DBarChartViewController *) frd3DBarChartViewController;
  • バーの表示最大値を要求するメソッド
-(float) frd3DBarChartViewControllerMaxValue:(FRD3DBarChartViewController *) frd3DBarChartViewController;
  • 各バーの値を要求するメソッド
-(float) frd3DBarChartViewController:(FRD3DBarChartViewController *)frd3DBarChartViewController 
                    valueForBarAtRow:(int)row 
                              column:(int)column;

サンプルプロジェクトではFRD3DBarChartViewControllerDelegateに適合した基底クラスを用意し、そちらにデータソースオブジェクトに共通のバーの行・列・最大値を設定する3つのデリゲードメソッドを実装、更に基底クラスを継承した具体クラスとして、各バーの値を設定するデリゲードメソッドを実装したデータソースクラスを用意してあります。(Skeletal implementation : 詳しくはEffective Javaを参照)

まずは基底クラスから確認していきます。

MY3DChartDataSourceBase.h

#import "FRD3DBarChartViewController.h"

/**
 *  3Dチャートの基底クラスです。
 */
@interface MY3DChartDataSourceBase : NSObject <FRD3DBarChartViewControllerDelegate>

/**
 *  3Dチャートの列の数です。
 */
@property (nonatomic) NSUInteger numberOfColumns;

/**
 *  3Dチャートの行の数です。
 */
@property (nonatomic) NSUInteger numberOfRows;

/**
 *  3Dチャートで表示する値の最大値です。
 */
@property (nonatomic) CGFloat maxValue;

MY3DChartDataSourceBase.m

#import "MY3DChartDataSourceBase.h"

/**
 *  列数の初期値です。
 */
static const NSUInteger InitialNumberOfColumns = 30;

/**
 *  行数の初期値です。
 */
static const NSUInteger InitialNumberOfRows = 30;

/**
 *  表示最大値の初期値です。
 */
static const CGFloat InitialMaxValue = 1.0f;

@implementation MY3DChartDataSourceBase

- (instancetype)init
{
    self = [super init];
    if (self != nil) {

        if ([self isMemberOfClass:[MY3DChartDataSourceBase class]]) {
            @throw
            [NSException
             exceptionWithName:NSInternalInconsistencyException
             reason:[NSString stringWithFormat:@"You can't instantiate this abstract class : %@",
                     NSStringFromClass([self class])]
           userInfo:nil];
        }

        _numberOfColumns = InitialNumberOfColumns;
        _numberOfRows = InitialNumberOfRows;
        _maxValue = InitialMaxValue;
    }
    return self;
}

/**
 *  3Dチャートのバーの列の個数を決定できます。
 *
 *  @param frd3DBarChartViewController 3Dチャート表示のためのViewController
 *
 *  @return バーの列の個数
 */
- (int)frd3DBarChartViewControllerNumberColumns:(FRD3DBarChartViewController *)frd3DBarChartViewController
{
    return (int)self.numberOfColumns;
}

/**
 *  3Dチャートのバーの行の個数を決定できます。
 *
 *  @param frd3DBarChartViewController 3Dチャート表示のためのViewController
 *
 *  @return バーの行の個数
 */
- (int)frd3DBarChartViewControllerNumberRows:(FRD3DBarChartViewController *)frd3DBarChartViewController
{
    return (int)self.numberOfRows;
}

/**
 *  3Dチャートの表示するバーの最大値を決定できます。
 *
 *  @param frd3DBarChartViewController 3Dチャート表示のためのViewController
 *
 *  @return バーの最大値
 */
- (float)frd3DBarChartViewControllerMaxValue:(FRD3DBarChartViewController *)frd3DBarChartViewController
{
    return (float)self.maxValue;
}

- (float)frd3DBarChartViewController:(FRD3DBarChartViewController *)frd3DBarChartViewController
                    valueForBarAtRow:(int)row
                              column:(int)column
{
    @throw
    [NSException
     exceptionWithName:NSInternalInconsistencyException
     reason:[NSString stringWithFormat:@"You must override %@ in a subclass",
             NSStringFromSelector(_cmd)]
     userInfo:nil];
}

@end

ある程度共通化できるようなデリゲードメソッドの実装はこちらのクラスに書き出しています。こちらのクラスは抽象クラスとしているため、イニシャライザでの基底クラスの直接のインスタンス化を行った場合や具体クラスで実装してほしいデリゲードメソッドに関しては例外を吐くようにしています。

具体データソースクラスを見てみます。

MYGaussianDistributionDataSource.h

#import "MY3DChartDataSourceBase.h"

/**
 *  正規分布3Dチャート表示のためのデータソースクラスです。
 */
@interface MYGaussianDistributionDataSource : MY3DChartDataSourceBase

/**
 *  Xの平均値です
 */
@property (nonatomic) float averageX;

/**
 *  Yの平均値です
 */
@property (nonatomic) float averageY;

/**
 *  分散です
 */
@property (nonatomic) float sigma;

@end

MYGaussianDistributionDataSource.m

#import "MYGaussianDistributionDataSource.h"

#import "UIColor+Hex.h"

/**
 *  Xの平均値の初期値です。
 */
static const float InitialaverageX = 0.0f;

/**
 *  Yの平均値の初期値です。
 */
static const float InitialaverageY = 0.0f;

/**
 *  分散の初期値です。
 */
static const float InitialSigma = 6.0f;

@implementation MYGaussianDistributionDataSource

#pragma mark - Lifecycle methods

- (instancetype)init
{
    self = [super init];
    if (self != nil) {
        _averageX = InitialaverageX;
        _averageY = InitialaverageY;
        _sigma = InitialSigma;
    }
    return self;
}

#pragma mark - FRD3DBarChartViewControllerDelegate

/**
 *  3Dチャートの各バーの値を決定します。
 *
 *  @param frd3DBarChartViewController 3Dチャート表示のためのViewController
 *  @param row                         バーの行
 *  @param column                      バーの列
 *
 *  @return 各バーの値
 */
- (float)frd3DBarChartViewController:(FRD3DBarChartViewController *)frd3DBarChartViewController
                    valueForBarAtRow:(int)row
                              column:(int)column
{
    CGFloat x = row - self.numberOfRows / 2.0f;
    CGFloat y = column - self.numberOfColumns / 2.0f;

    return GaussianDistribution(x, y, self.averageX, self.averageY, self.sigma);
}

/**
 *  3Dチャートの各行につけるラベル文字列を決定できます。nilで何も表示しないようにできます。
 *
 *  @param frd3DBarChartViewController 3Dチャート表示のためのViewController
 *  @param row                         バーの行
 *
 *  @return 各行のラベル文字列
 */
- (NSString *)frd3DBarChartViewController:(FRD3DBarChartViewController *)frd3DBarChartViewController
                             legendForRow:(int)row
{
    CGFloat x = row - self.numberOfRows / 2.0f;
    NSNumber *xNumber = (NSInteger)x % 5 == 0 ?
    @(x) : nil;
    return [xNumber stringValue];
}

/**
 *  3Dチャートの各列につけるラベル文字列を決定できます。nilで何も表示しないようにできます。
 *
 *  @param frd3DBarChartViewController 3Dチャート表示のためのViewController
 *  @param row                         バーの列
 *
 *  @return 各列のラベル文字列
 */
- (NSString *)frd3DBarChartViewController:(FRD3DBarChartViewController *)frd3DBarChartViewController
                          legendForColumn:(int)column
{
    CGFloat y = column - self.numberOfColumns / 2.0f;
    NSNumber *yNumber = (NSInteger)y % 5 == 0 ?
    @(y) : nil;
    return [yNumber stringValue];
}

/**
 *  3Dチャートの各バーのUIColorを決定できます。
 *
 *  @param frd3DBarChartViewController 3Dチャート表示のためのViewController
 *  @param row                         バーの行
 *  @param column                      バーの列
 *
 *  @return 各バーのUIColor
 */
- (UIColor *)frd3DBarChartViewController:(FRD3DBarChartViewController *)frd3DBarChartViewController
                        colorForBarAtRow:(int)row
                                  column:(int)column
{
    CGFloat x = row - self.numberOfRows / 2.0f;
    CGFloat y = column - self.numberOfColumns / 2.0f;
    CGFloat colorDepth =
    GaussianDistribution(x, y, self.averageX, self.averageY, self.sigma) / self.maxValue;
    NSUInteger colorValue =
    (NSUInteger)(0xff * colorDepth) * 0x10000 + 0x0000ff * (1 - colorDepth);
    return [UIColor colorWithHexInteger:colorValue];
}

#pragma mark - Private methods

/**
 *  ガウシアンを返す関数です
 *
 *  @param x        xの入力値
 *  @param y        yの入力値
 *  @param averageX xの平均値
 *  @param averageY yの平均値
 *  @param sigma    分散
 *
 *  @return ガウシアンの値
 */
CGFloat GaussianDistribution(CGFloat x, CGFloat y, CGFloat averageX, CGFloat averageY, CGFloat sigma)
{
    CGFloat sigmaSquare = sigma * sigma;
    CGFloat deltaXSquare = (x - averageX) * (x - averageX);
    CGFloat deltaYSquare = (y - averageY) * (y - averageY);
    return
    1.0f / sqrtf(2 * M_PI * sigmaSquare) *
    expf(- (deltaXSquare + deltaYSquare) / 2.0f / sigmaSquare);
}

@end

具体クラスでは各バーの値を設定する必須デリゲードメソッドの他にチャートの色を設定したり、グラフの軸に追加できるラベルの文字列の設定をするデリゲードメソッドを追加しています。各バーの値を計算する為の正規分布関数はつぎのような計算式になります。

[math] P(x, y, \overline{x}, \overline{y}, \sigma) = \frac{1}{\sqrt{ 2 \pi \sigma ^ 2} } \exp \bigg( { - \frac{ (x - \overline{x})^2 + (y - \overline{y})^2 }{2 \sigma ^ 2}} \bigg) [/math]

グラフの形をアニメーション付きで変更

サンプルプロジェクトでは上記実装の他に平均値や分散をUISliderで動かし、グラフの形をアニメーションで変えるための実装をしてあります。グラフを動かすためのメソッドとして

-(void) updateChartAnimated:(BOOL) animated 
          animationDuration:(NSTimeInterval)duration 
                    options:(kUpdateChartOptions)options;

があります。animatedの値でアニメーションするかどうか、durationでアニメーションの時間、kUpdateChartOptionsでグラフの軸に追加されたラベルの値も変更するかどうかが指定できます。

グラフの縦軸に線を加える

ポアッソン分布の実装に関してもいままでと同様MY3DChartDataSourceBase.hを継承したクラスを作った上でUIViewControllerサブクラス上でインスタンスを生成してFRD3DBarChartViewControllerDelegateにセットしています。データソースオブジェクトの方では以下のように線を加えるデリゲードメソッドを実装しています。

MYPoissonDistributionDataSource.m

/**
 *  3Dチャートの底を除いた線の数を決定します。
 *
 *  @param frd3DBarChartViewController 3Dチャート表示のためのViewController
 *
 *  @return 線の数
 */
- (int)frd3DBarChartViewControllerNumberHeightLines:(FRD3DBarChartViewController *)frd3DBarChartViewController
{
    return NumberOfLines;
}

/**
 *  3Dチャートの各線につけるラベル文字列を決定できます。nilで何も表示しないようにできます。
 *
 *  @param frd3DBarChartViewController 3Dチャート表示のためのViewController
 *  @param line                        ラインのインデックス
 *
 *  @return 各線のラベル文字列
 */
- (NSString *)frd3DBarChartViewController:(FRD3DBarChartViewController *)frd3DBarChartViewController
                       legendForValueLine:(int)line
{
    CGFloat valueForLine = self.maxValue * (line + 1) / (CGFloat)NumberOfLines;
    return @(valueForLine).stringValue;
}

尚、ポアッソン分布を計算するための関数は以下の様な計算式になります。

[math] P(x | \lambda) = \frac{\lambda ^ x e ^ {- \lambda}}{x!} [/math]

グラフを円柱状に変更する

FRD3DBarChartViewControllerクラスの

@property (nonatomic) BOOL useCylinders;

をYESにすれば円柱状のグラフを描画できます。

サンプルプロジェクト動作例

上記のような実装をしたサンプルプロジェクトを動かしてみます。

正規分布

garalley1 garalley2

ポアッソン分布

garalley3 garalley4

パラメータをいじった時のアニメーションを含んだ動作や値同士の比較を立体的に行いたいときに便利ですね。

参考サイト