寫程式時總是會遇到需要呈現許多資料的時候,此時就會想說要選用那一種 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。