在筆者幹家教,教 C language 時,曾有位學生拿了學校老師的作業出來問我,作業說明不多,只有一張圖,類似下面這張。
畫出 sin 圖形,另那份作業是需要「標座標」的,也就是要標 x, y 軸刻度,及交點座標。當下是怕太擔誤課程進度,所以並沒當下解,而是回去時候才花了一、二個小時畫出來 (其中半小時是在推座標公式,半小時是在調刻度及標號交點,剩下 Coding 很快)。
解題方向
無論如何記得一件事,一般 y 軸座標遞增方向,和一般數學上 y 軸座標遞增方式是反過來的。
首先要聲明,筆者認為 [在 console 裡畫圖型] 只是練習用而已,然後解法大致分成三大類。
[1] 直接用兩個 for-loop 下去做,配合座標轉換把點標出來,效率較低;
[2] 另一種方式是直接開一個二維字元陣列,char graphic[HEIGHT][WIDTH] ,所有的畫圖動作都對該陣列做處理,最後再一次印出來,這種方式若寫得好,效率非常高!
[3] 直接調用現有 API 去設定 console 游標位置,如 SetConsoleCursorPosition ;若支援 ANSI Escape sequence 也是一樣的原理。
如果用 C 的話,這種題目解過就算了,不要一直窮解這題;用 C++ 的話可以練 OO 或 design pattern 能力,因可擴充的部份太多了,座標非線性、一對一函式(如三角函式)、一對多函式 (如圓的方程式) 等,算是一個不大,但應算是不錯的題目。
另一般而言,在畫 plot 時,確實蠻多人挑用 gnuplot 做繪圖,這裡不是本文重點,有空再發一下筆者常用的 gnuplot 指令。
第一種解法
先看一個題目是,畫出 y = x^2 之曲線圖,畫出來長如此。
第一種解法其實沒什麼技巧,效率低,code 不長,直接放上。
- #include <stdio.h>
- #include <stdlib.h>
- #include <math.h>
- #define YLOW 0.0
- #define YUP 25.0
- #define Yd 1.0
- #define XLOW -10.0
- #define XUP 10.0
- #define Xd 0.2
- #define TEXT_WIDTH 4 // y 標示寬度
- // 修改函式圖形
- double func(double x) {return x*x;}
- int main()
- {
- double x, y, fvalue;
- double area=0.0;
- unsigned cnt=0;
- // 畫圖
- for(y=YUP; y>=YLOW; y-=Yd){
- printf("%*.1lf|",TEXT_WIDTH, y);
- cnt=0;
- for(x=XLOW; x<=XUP; x+=Xd){
- fvalue = func(x); // 算每個 x 之值
- if(fabs(fvalue-y)<=Yd/2) putchar('*'); // 若與現在的 y 誤差在 xd/2 以下,輸出 '*'
- else if(fabs(y)<=Yd/2) putchar('-'); //基準線
- else putchar(' '); // 誤差超過 xd/2, 輸出空白
- cnt++;
- }
- putchar('\n');
- }
- getchar();
- return 0;
- }
要注意的是,這裡的寬度其實沒算好,所以要把 console 的可視寬度調大 ( 筆者是調到 120 )。
陣列作圖 + func-ptr
一個符號先說,i = [a,b] ,代表 for(i=a; i<=b; ++i)
這裡主要記流程,呼叫時必須傳入 5 個參數,分別是 double (*fn)(double) , xlow, xup, ylow, yup,其它的都是在 function 做計算。
由於一般 console window 預設是 W = 80, H = 25,所以一開始先準備 char gra[H][W],然後在 VC (.c) 裡面,靜態陣列也不吃 const 當 size ,所以用 malloc。另外注意三件事,
(a) 每一個 row 的最後一個字元做成 new-line 字元,這樣只要 output 一次便行,另記得補上 gra[H-1][W-1] = 0。
(b) 周圍做邊框,另做x,y 基準線, 但為分析方便,基準線並不一定代表是 x==0 或 y == 0。
(c) 因畫邊框、做換行的關係,所以 gra[i][j] 真正可用之範圍為,i = [1,H-2],j = [1,W-3]
了解上面三件事後,下面的流程就清楚多了 (注意,流程是示意,實際撰碼時一定有 loop 可以合併)。
第一大步驟是先建立空白的座標畫布。
(1) 將 gra 全弄成空白
gra[i][j] = ' ', for all i, j
(2) 畫邊線
gra[i][j] = '#' , for i==0, i==H-1 , j==0, j==W-2
(3) 做 row 換行
gra[i][W-1] = '\n', for i=[0,W-2]
(4) 塞結束字元
gra[H-1][W-1] = '\0'
(5) 畫 x 軸基準線
gra[H/2][j] = '-' , j = [1, W-3]
(6) 畫 y 軸基準線
gra[i][W/2] = '|', i = [1,H-2]
(7) 畫 x, y , 交點
gra[H/2][W/2] = '+', gra[0][W/2] = 'Y', gra[H/2][W-1] = 'X'
第二大步驟是計算 y = fn(x) , 並做座標轉換, 最後一次輸出
(8) 計算 rate_x, rate_y
rate_x = (xup - xlow) / (W-4)
rate_y = (yup - ylow) / (H-3)
(9) for(i = 1 ; i<= W-3 ; ++i) /* 以下動作都在此迴圈裡進行 */
(10) 計算 gra 裡的 i , 實際對應到的 x-value 是多少
x = (i-1) * rate_x + xlow
(11) 呼叫函式計算 y 值
y = fn(x)
(12) 將實際的 y 值對應到 gra 裡之 jtmp 座標, 考慮四捨五入
jtmp = (size_t)floor( (y-ylow) / rate_y + 0.5 )
(13) 將原本的 jtmp 座標,考慮電腦 y 軸和實際 y 軸遞增性相反,做修正
j = height - 2 - jtmp < -2 是因邊界不畫 >
(14) 將 gra[j][i] 畫上點
記得判斷 j>0 && j<height-1 是否成立。
上面做完後再一次輸出 gra 即可。講完了,看起來很複雜,實際上把回圈併過之後,其實碼不會很長。這裡的示例完全以上述流程,不再併回圈。
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <math.h>
- /**
- * plot f(x) function in console, C version
- *
- */
- void plot_fn(
- double (*fn)(double), /* f(x) function */
- const double xlow, /* lower bound of x */
- const double xup, /* upper bound of x */
- const double ylow, /* max bound of y */
- const double yup /* min bound of y */
- )
- {
- const size_t width = 80;
- const size_t height= 25;
- const size_t tsize = width * height;
- size_t i, j, jtmp;
- size_t ox = width/2, oy=height/2;
- double x, y, rate_x, rate_y;
- /* malloc memory */
- char **gra = (char**)malloc(sizeof(char*) * height + tsize);
- char * trunk = (char*)(gra + height);
- for(i=0; i<height; ++i)
- gra[i]=trunk, trunk+=width;
- /* initialize graphic , boarder*/
- memset((void*)*gra, ' ', tsize);
- memset((void*)(gra[0]), '#', width);
- memset((void*)(gra[height-1]), '#', width);
- for(i=0; i<height; ++i){
- gra[i][0] = '#';
- gra[i][width-2] = '#';
- gra[i][width-1] = '\n';
- }
- gra[height-1][width-1]='\0';
- /* basement line for x axis*/
- memset((void*)(gra[oy]+1), '-', width-3);
- gra[oy][width-2]='X';
- /* basement line for y axis */
- gra[0][ox]='Y';
- for(i=1; i<height-1; ++i) gra[i][ox] = '|';
- gra[oy][ox]='+';
- /* graphic */
- rate_x = (xup - xlow) / (width-4);
- rate_y = (yup - ylow) / (height - 3);
- for(i=1 ; i<=width-3; ++i){
- x = (i-1)*rate_x + xlow;
- y = fn(x);
- jtmp = (size_t)floor((y-ylow)/rate_y + 0.5);
- j = height-jtmp-2;
- if(j>0 && j<height-1)
- gra[j][i] = '*';
- }
- puts(*gra);
- free((void*)gra);
- }
- int main()
- {
- const double PI = 3.1415926;
- plot_fn(sin, -2.0*PI, +2.0*PI, -1.0, +1.0);
- getchar();
- return 0;
- }
結果圖就是第一張圖所示。
其他擴充功能
筆者稍找了一下,目前沒找到有 console plot library (不過並不意外就是了),有幾個功能有興趣自己再加
(1) 標刻度
(2) 標交點
(3) 給定 xmin, xup 時,程式裡面再自己計算 ymin, yup
(4) 支援 1 對 2 函式,如 x^2 + y^2 = C
(5) 非線性刻度,如 log10 刻度、exp 刻度、ln 刻度等
(6) 加入函式標題列、交點資訊、刻度資訊等
其他的筆者沒太大興趣 ( 主要是覺得不太實用 ),本文就介紹至此。
留言列表