2012年8月21日 星期二

iOS 與 jQuery Chart API 溝通 - jqPlot

寫程式時總是會遇到需要呈現許多資料的時候,此時就會想說要選用那一種 Chart API 比較好?在寫文章前先給看文章的朋友們建議,如果只是單就呈現 Chart 圖而不需要很常的互動的話,可以選擇幾個 HTML + Javascript 的 Chart API 來用用,簡單而且美觀。可以參考這個網頁介紹的幾個,在這個文章筆者是採用 jqPlot。而如果和 Chart 圖互動很高的話,效能的考量最好用 Native 的 Framework 比如 Core Plot
這篇文章的重點有
  • 自訂 cell
  • web view 呈現 html + javascript
  • NSArray 轉成 JSON String
  • web view 呼叫 javascript function,傳資料給 javascript
  • 動態呈現 Pie Chart
首先來開啟一個 Master-Detail Application 的 Project

命名為 PieDemo

自訂 Cell

我們想要產生的效果如下圖
 按下右上的 加號,會隨機產生出數字和類別。而左邊的 Chart 按下去之後會產生各類別比例的 PieChart。
我們先著眼在 MasterViewController.m 加入幾行程式,從 Line 15 開始
@interface MasterViewController () {
    NSMutableArray *_objects;
    NSArray * categories;
 }
@end
新加一個 ivar categories 的 array 在 ViewDidLoad 初始化資料,然後把一個 leftBarButtonItem 去掉。

- (void)viewDidLoad
{
    [super viewDidLoad];
    categories = [NSArray arrayWithObjects:@"食",@"衣",@"住",@"行",@"娛", nil];
  
    // Do any additional setup after loading the view, typically from a nib.
//    self.navigationItem.leftBarButtonItem = self.editButtonItem;

    UIBarButtonItem *addButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(insertNewObject:)];
    self.navigationItem.rightBarButtonItem = addButton;
}

接著到 insertNewObject: 隨機產生數字和隨機選一個類別
- (void)insertNewObject:(id)sender
{
    if (!_objects) {
        _objects = [[NSMutableArray alloc] init];
    }
    NSNumber * num = [NSNumber numberWithInteger:arc4random()%2000];
    NSString * cat = [categories objectAtIndex:arc4random()%5];
   
    NSDictionary * content = [NSDictionary dictionaryWithObjectsAndKeys:num, @"amount",cat ,@"cat", nil];

   
    [_objects insertObject:content atIndex:0];
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
    [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
粗體字是新加的,其他部分是原本 Xcode 產生的。我們把每個 row 需要的資料放在一個 dictionary 裡面,原本的 _object 就是存這些 dictionary。其中筆者用到一個隨機產生數字的 function 為
 arc4random()
再把數值包成 NSNumber 加到 dictionary content 中,也用 arc4random() % 5 隨機選了一個類別加到 content 中。
資料的部分齊全了,接下來看 cell 的部分,這個時候要打開 MainStoryboad.storyboard
選好 Table View 裡的 Cell 為 Custom 如下圖。
 要注意的是 Style 是 Custom,Identifier 是 Cell,大小寫很重要。仔細看一下 Cell 上面有兩個 Label, amount 和 category 是筆者自行從 Library 加上去的。如下。
接下來為這兩個 Label 設定 tag ,amount 的 tag 是 10,cateogry 的 tag 是 11。如下圖。
category Label 請仿照 amount label 的做法把 Tag 設定為 11.
接著就是修改 MasterViewController.m 的程式碼了。找到 tableView:cellForRowAtIndexPath: 改成如下
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
    UILabel * amount = (UILabel *) [cell viewWithTag:10];
    UILabel * cat = (UILabel *)[cell viewWithTag:11];
    NSDictionary * object = [_objects objectAtIndex:indexPath.row];
    amount.text = [NSString stringWithFormat:@"%d", [[object objectForKey:@"amount"] integerValue]];
    cat.text = [object objectForKey:@"cat"];
//    NSDate *object = [_objects objectAtIndex:indexPath.row];
//    cell.textLabel.text = [object description];
    return cell;
}
利用 viewWithTag: 從 cell 中找到 amount 和 category label 的物件,從 _objects 中讀出相對應的值分別是 amount 和 cat 的 key 再加到 label 的 text 上面。
這樣一來就可以執行玩,按下一開始畫面右上的加號看看,應該會有下圖出現。

web view 呈現 html + javascript

接著我們要在Master 的左邊加上一個 item 名為 Chart,可以直接從 MainStoryboard.storyboard 從 Library 拉一個 bar button item 到 MasterViewController 的 Navigation Bar 上。如下
然後再新增一個 View Controller,拉一個 segue (選擇 push) 從 Chart - Item 拉到新加的這個 View Controller 讀者會看到下圖。

新的 View Controller 也會多了一個 Navigation Bar。接著在 View Controller 上拉一個 web view 上去。如下圖。
 是時候為這個 View Controller 新增一個  Class 命名為 GraphViewController 是繼承自 UIViewController。其中 GraphViewController.h 的程式碼如下
#import <UIKit/UIKit.h>

@interface GraphViewController : UIViewController<UIWebViewDelegate>
@property (weak, nonatomic) IBOutlet UIWebView *myWebView;
@property (strong) NSMutableDictionary * sums;
@end
 其中 IBOutlet  myWebView 是用來連接 storyboard 上面 web view 的元件。sums 這個 dictionary 是用來存 MasterViewController 給的資料。
接著到 GraphViewController.m 的 viewDidLoad 新增程式碼如下
- (void)viewDidLoad
{
    [super viewDidLoad];
   
    NSString * fileURL = [[NSBundle mainBundle] pathForResource:@"graph" ofType:@"html" inDirectory:@"jqplot"];

    NSURL * url = [NSURL URLWithString:[fileURL stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];

    NSURLRequest * urlRequest = [NSURLRequest requestWithURL:url];
    [myWebView loadRequest:urlRequest];
    myWebView.delegate = self;
  
}
其中 fileURL 是一個指向 html 檔的位置,筆者等會要把所有畫圖相關的 html javascript 都放到 main bundle 裡 jqplot 這個資料夾下,所以程式碼會寫
[[NSBundle mainBundle] pathForResource:@"graph" ofType:@"html" inDirectory:@"jqplot"];
接著把 fileURL 這個 string 包到一個 NSURL 底下。就用到
[NSURL URLWithString:[fileURL stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
有一個讀者可能會問的問題什麼是 stringByAddingPercentEscapesUsingEncoding ?
對於 browser 的網址列上的字串比如 http://127.0.0.1/?name=michael 會變轉碼成
http://127.0.0.1/%3fname=michael
也就是說 ? 會被轉成 %3F,更多的 precent escape 可以看一下 wiki
最後的步驟就是把 NSURL 包成 NSURLRequest 然後給 myWebView 來讀取。
[myWebView loadRequest:urlRequest];
寫到這裡,先去 download jqPlot 然後把需要的檔案拉到 Xcode 專案去。 下載好解完壓縮會看到一個 dist 資料夾。如下。
我們移掉一些檔案最後剩下如下圖

圖中新增了一個 html 名為 graph.html。來看看它的程式碼
<html>
    <head>
        <script language="javascript" type="text/javascript" src="jquery.min.js"></script> 
        <script language="javascript" type="text/javascript" src="jquery.jqplot.min.js"></script>
        <script type="text/javascript" src="./plugins/jqplot.pieRenderer.min.js"></script>
        <link rel="stylesheet" type="text/css" href="jquery.jqplot.min.css" />

        <script type="text/javascript">
        $(document).ready(function(){
            var s1 = [['Sony',7], ['Samsumg',13.3], ['LG',14.7], ['Vizio',5.2], ['Insignia', 1.2]];
        
            var plot8 = $.jqplot('pie8', [s1], {
                grid: {
                    drawBorder: false,
                    drawGridlines: false,
                    background: '#ffffff',
                    shadow:false
                },
                axesDefaults: {
            
                },
                seriesDefaults:{
                    renderer:$.jqplot.PieRenderer,
                    rendererOptions: {
                        showDataLabels: true
                    }
                },
                legend: {
                    show: true,
                    rendererOptions: {
                        numberRows: 1
                    },
                    location: 's'
                }
            });
        });
        </script>

    </head>
    <body>
        <div id="pie8" class="jqplot-target" style="height:400px;width:300px;">
        </div>

    </body>
</html>
把整包 dist 改名成 jqplot 然後拉到 Xcode 專案,記得選擇 folder。如下圖示。
然後會在 Xcode 專案看到一個藍色的 folder。如下圖
為了要先測試一下 jqPlot 是否正確,先把 MainStoryboard.storyboard 裡有 Web View 的 View Controller 的 class 改成 GraphViewController。如下
接著把 IBOutlet 連結上去。如下
到目前為止可以先執行一下 App 會,按下 Master 左上的 Chart 會看到如下的圖。
 
如果有看到上方左圖就表示 jqPlot 功能正常我們沒有設定錯誤。

NSArray 轉成 JSON String

到了這個階段我們要試著想怎麼把很多筆的資料,在 MasterViewContorller 整合好之後傳給 GraphViewController 的 sums。
在 MasterViewController.m 的開始新增下面程式碼 
@interface MasterViewController () {
    NSMutableArray *_objects;
    NSArray * categories;
    NSMutableDictionary * sums;
}
@end
然後在 viewDidload 初始化 sums 如下
categories = [NSArray arrayWithObjects:@"食",@"衣",@"住",@"行",@"娛", nil];
    sums = [NSMutableDictionary dictionaryWithCapacity:[categories count]];
    [categories enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        [sums setObject:[NSNumber numberWithInteger:0] forKey:obj];
    }];
準備好 sums 是一個 dictionary 要傳給 GraphViewController 的目前 sums 的內容是
食=0
衣=0
住=0
行=0
娛=0
在 MainStoryboard 的地方給左上角 Chart 相關的 segue 一個 ID 名為 showGraph。如下圖

然後在 prepareForSegue 的地方新增下列程式碼
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([[segue identifier] isEqualToString:@"showDetail"]) {
        NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
        NSDate *object = [_objects objectAtIndex:indexPath.row];
        [[segue destinationViewController] setDetailItem:object];
    }
    if ([[segue identifier] isEqualToString:@"showGraph"]) {

        [_objects enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
            NSInteger current = [[sums objectForKey:[obj objectForKey:@"cat"]] integerValue];
            current += [[obj objectForKey:@"amount"] integerValue];
            [sums setObject:[NSNumber numberWithInteger:current] forKey:[obj objectForKey:@"cat"]];
        }];
       
        GraphViewController * graph = segue.destinationViewController ;
        graph.sums = sums;

    }
}
在 segue identifier 的判斷式裡,這裡有一個 enumerateObjectsUsingBlock: 的用意是在 _object 裡的每一筆資料都是
食=1234
衣=3455
食=590
娛=456
於是要把每一筆資料的數值加到 sums 裡面,用
NSInteger current = [[sums objectForKey:[obj objectForKey:@"cat"]] integerValue];
取得某一個分類的值
current += [[obj objectForKey:@"amount"] integerValue];
把在 sums 裡的值和目前值加起來
[sums setObject:[NSNumber numberWithInteger:current] forKey:[obj objectForKey:@"cat"]];
最後還是寫回到 sums 裡面。
run 完所有的 _objects 裡的元素後,sums 的值也準備好了,就利用
GraphViewController * graph = segue.destinationViewController ;
 graph.sums = sums;
傳給 GraphViewController。目光轉到 GraphViewController.m 身上。新增一個 method
-(void) webViewDidFinishLoad:(UIWebView *)webView{
    NSMutableArray * catArray = [NSMutableArray array];
    NSMutableArray * valueArray = [NSMutableArray array];

    [self.sums enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
        [catArray addObject:key];
        [valueArray addObject:obj];
    }];


    NSString * jsonArray = [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:catArray options:NSJSONWritingPrettyPrinted error:NULL] encoding:NSUTF8StringEncoding];
    NSString * jsonValue = [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:valueArray options:NSJSONWritingPrettyPrinted error:NULL] encoding:NSUTF8StringEncoding];
   
    NSLog(@"json Array %@", jsonArray);
    NSLog(@"value array %@", jsonValue);
    [myWebView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"gotData(%@,%@); ",jsonArray ,jsonValue]];
}
裡面筆者打算把 sums 分開成兩個 array 分別是 catArray 和 valueArray 而
[self.sums enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
        [catArray addObject:key];
        [valueArray addObject:obj];
    }];
就是做這件事情。
然後接著就是把 NSArray 物件轉成 JSON String ,我們用的是
[NSJSONSerialization dataWithJSONObject:catArray options:NSJSONWritingPrettyPrinted error:NULL]
是把 NSArray 轉成 JSON 格式的 NSData,然後再用 NSString 的 initWithData:encoding: 來把 NSData 轉成 NSString.
NSString * jsonArray = [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:catArray options:NSJSONWritingPrettyPrinted error:NULL] encoding:NSUTF8StringEncoding];
 catArray 轉成 jsonArray,valueArray 轉成 jsonValue。接下來的就是把 jsonArray和 jsonValue 那兩個 json 格式的物件利用 web view 的 stringByEvaluatingJavaScriptFromString: 傳給 html 的 javascript 接住。如下
[myWebView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"gotData(%@,%@); ",jsonArray ,jsonValue]];
這邊可以看到 javascript 承接的 function 為 gotData(),而 jsonArray 和  jsonValue 透過 %@ 方式進入 gotData 裡去了。
所以接下來我們要在 graph.html 新增 javascript function gotData 來接收 web view 給 javascript 的資料。

web view 呼叫 javascript function,傳資料給 javascript

Xcode 打開 graph.html,在 <script> </script> 中新增如下程式碼。
<script type="text/javascript">
            var catArray = "";
            var valueArray ="";
            var pieData = [];
            function gotData(data1,data2){
                catArray = data1;
                valueArray = data2;
            }

</script>
筆者新增三個變數,catArray 用來存 jsonArray 的資料,valueArray 用來存 jsonValue的資料,而pieData 則是用來畫 pie chart 的資料,也是 catArray 和 valueArray 的組合。接著我們看到 gotData這個 function
function gotData(data1,data2){
                catArray = data1;
                valueArray = data2;
 }

在 web view 執行這個 function的時候就會透過 data1, data2 把資料給 catArray 和 valueArray。

動態呈現 Pie Chart

為了要符合 jqPlot 的運作方式,筆者要再新增一個 function 名為 drawPie() 如下
function drawPie(){
                for(var index in catArray){
                    pieData.push([catArray[index],valueArray[index]]);
                }

                var plot8 = $.jqplot('pie8', [pieData], {
                                     grid: {
                                     drawBorder: false,
                                     drawGridlines: false,
                                     background: '#ffffff',
                                     shadow:false
                                     },
                                     axesDefaults: {
                                    
                                     },
                                     seriesDefaults:{
                                     renderer:$.jqplot.PieRenderer,
                                     rendererOptions: {
                                     showDataLabels: true
                                     }
                                     },
                                     legend: {
                                     show: true,
                                     rendererOptions: {
                                     numberRows: 1
                                     },
                                     location: 's'
                                     }
                                     });
            }
這個 function 主要是依照 jqPlot 畫圖的資料格式,之前是
[['Sony',7], ['Samsumg',13.3], ['LG',14.7], ['Vizio',5.2], ['Insignia', 1.2]]
就一個 array 裡面每一筆資料都是只有兩個元素的 array,第一個元素是代表 label 第二個元素 代表數值,所以這邊用
  for(var index in catArray){
           pieData.push([catArray[index],valueArray[index]]);
  }
把 catArray[index],valueArray[index] 組合起來的 array 加到 pieData 裡面,當 index 是所有 catArray 的 index 之後,pieData 就準備好了,就可以直接丟給 $.jqplot 去畫圖。
存檔之後就可以執行看到如下畫面。眼尖的讀者可以看一下有沒有算錯?
一樣地,所有的程式碼放在GitHub

 

沒有留言:

張貼留言