iOS 與 jQuery Chart API 溝通 - jqPlot

来源:互联网 发布:linux下rar解压命令 编辑:程序博客网 时间:2024/06/05 19:33
寫程式時總是會遇到需要呈現許多資料的時候,此時就會想說要選用那一種 Chart API 比較好?在寫文章前先給看文章的朋友們建議,如果只是單就呈現 Chart 圖而不需要很常的互動的話,可以選擇幾個 HTML + Javascript 的 Chart API 來用用,簡單而且美觀。可以參考這個 介紹的幾個,在這個文章筆者是採用 。而如果和 Chart 圖互動很高的話,效能的考量最好用 Native 的 Framework 比如 。 
這篇文章的重點有 
  • 自訂 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/ %3 fname=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 。 

0 0