|
1
|
/* |
|
2
|
** Copyright (c) 2015 D. Richard Hipp |
|
3
|
** |
|
4
|
** This program is free software; you can redistribute it and/or |
|
5
|
** modify it under the terms of the Simplified BSD License (also |
|
6
|
** known as the "2-Clause License" or "FreeBSD License".) |
|
7
|
|
|
8
|
** This program is distributed in the hope that it will be useful, |
|
9
|
** but without any warranty; without even the implied warranty of |
|
10
|
** merchantability or fitness for a particular purpose. |
|
11
|
** |
|
12
|
** Author contact information: |
|
13
|
** [email protected] |
|
14
|
** http://www.hwaci.com/drh/ |
|
15
|
** |
|
16
|
******************************************************************************* |
|
17
|
** |
|
18
|
** This file contains code for generating pie charts on web pages. |
|
19
|
** |
|
20
|
*/ |
|
21
|
#include "config.h" |
|
22
|
#include "piechart.h" |
|
23
|
#include <math.h> |
|
24
|
|
|
25
|
#ifndef M_PI |
|
26
|
# define M_PI 3.1415926535897932385 |
|
27
|
#endif |
|
28
|
|
|
29
|
/* |
|
30
|
** Return an RGB color name given HSV values. The HSV values |
|
31
|
** must each be between 0 and 255. The string |
|
32
|
** returned is held in a static buffer and is overwritten |
|
33
|
** on each call. |
|
34
|
*/ |
|
35
|
const char *rgbName(unsigned char h, unsigned char s, unsigned char v){ |
|
36
|
static char zColor[8]; |
|
37
|
unsigned char A, B, C, r, g, b; |
|
38
|
unsigned int i, m; |
|
39
|
if( s==0 ){ |
|
40
|
r = g = b = v; |
|
41
|
}else{ |
|
42
|
i = (h*6)/256; |
|
43
|
m = (h*6)&0xff; |
|
44
|
A = v*(256-s)/256; |
|
45
|
B = v*(65536-s*m)/65536; |
|
46
|
C = v*(65536-s*(256-m))/65536; |
|
47
|
@ <!-- hsv=%d(h),%d(s),%d(v) i=%d(i) m=%d(m) ABC=%d(A),%d(B),%d(C) --> |
|
48
|
switch( i ){ |
|
49
|
case 0: r=v; g=C; b=A; break; |
|
50
|
case 1: r=B; g=v; b=A; break; |
|
51
|
case 2: r=A; g=v; b=C; break; |
|
52
|
case 3: r=A; g=B; b=v; break; |
|
53
|
case 4: r=C; g=A; b=v; break; |
|
54
|
default: r=v; g=A; b=B; break; |
|
55
|
} |
|
56
|
} |
|
57
|
sqlite3_snprintf(sizeof(zColor),zColor,"#%02x%02x%02x",r,g,b); |
|
58
|
return zColor; |
|
59
|
} |
|
60
|
|
|
61
|
/* |
|
62
|
** Flags that can be passed into the pie-chart generator |
|
63
|
*/ |
|
64
|
#if INTERFACE |
|
65
|
#define PIE_OTHER 0x0001 /* No wedge less than 1/60th of the circle */ |
|
66
|
#define PIE_CHROMATIC 0x0002 /* Wedge colors are in chromatic order */ |
|
67
|
#define PIE_PERCENT 0x0004 /* Add "(XX%)" marks on each label */ |
|
68
|
#endif |
|
69
|
|
|
70
|
/* |
|
71
|
** A pie-chart wedge label |
|
72
|
*/ |
|
73
|
struct WedgeLabel { |
|
74
|
double rCos, rSin; /* Sine and Cosine of center angle of wedge */ |
|
75
|
char *z; /* Label to draw on this wedge */ |
|
76
|
}; |
|
77
|
typedef struct WedgeLabel WedgeLabel; |
|
78
|
|
|
79
|
/* |
|
80
|
** Comparison callback for qsort() to sort labels in order of increasing |
|
81
|
** distance above and below the horizontal centerline. |
|
82
|
*/ |
|
83
|
static int wedgeCompare(const void *a, const void *b){ |
|
84
|
const WedgeLabel *pA = (const WedgeLabel*)a; |
|
85
|
const WedgeLabel *pB = (const WedgeLabel*)b; |
|
86
|
double rA = fabs(pA->rCos); |
|
87
|
double rB = fabs(pB->rCos); |
|
88
|
if( rA<rB ) return -1; |
|
89
|
if( rA>rB ) return +1; |
|
90
|
return 0; |
|
91
|
} |
|
92
|
|
|
93
|
/* |
|
94
|
** Output HTML that will render a pie chart using data from |
|
95
|
** the PIECHART temporary table. |
|
96
|
** |
|
97
|
** The schema for the PIECHART table should be: |
|
98
|
** |
|
99
|
** CREATE TEMP TABLE piechart(amt REAL, label TEXT); |
|
100
|
*/ |
|
101
|
void piechart_render(int width, int height, unsigned int pieFlags){ |
|
102
|
Stmt q; |
|
103
|
double cx, cy; /* center of the pie */ |
|
104
|
double r, r2; /* Radius of the pie */ |
|
105
|
double x1,y1; /* Start of the slice */ |
|
106
|
double x2,y2; /* End of the slice */ |
|
107
|
double x3,y3; /* Middle point of the slice */ |
|
108
|
double x4,y4; /* End of line extending from x3,y3 */ |
|
109
|
double x5,y5; /* Text anchor */ |
|
110
|
double d1; /* radius to x4,y4 */ |
|
111
|
const char *zAnc; /* Anchor point for text */ |
|
112
|
double a1 = 0.0; /* Angle for first edge of slice */ |
|
113
|
double a2; /* Angle for second edge */ |
|
114
|
double a3; /* Angle at middle of slice */ |
|
115
|
unsigned char h; /* Hue */ |
|
116
|
const char *zClr; /* Color */ |
|
117
|
int l; /* Large arc flag */ |
|
118
|
int j; /* Wedge number */ |
|
119
|
double rTotal; /* Total piechart.amt */ |
|
120
|
double rTooSmall; /* Sum of pieChart.amt entries less than 1/60th */ |
|
121
|
int nTotal; /* Total number of entries in piechart */ |
|
122
|
int nTooSmall; /* Number of pieChart.amt entries less than 1/60th */ |
|
123
|
const char *zFg; /* foreground color for lines and text */ |
|
124
|
int nWedgeAlloc = 0; /* Slots allocated for aWedge[] */ |
|
125
|
int nWedge = 0; /* Slots used for aWedge[] */ |
|
126
|
WedgeLabel *aWedge = 0; /* Labels */ |
|
127
|
double rUprRight; /* Floor for next label in the upper right quadrant */ |
|
128
|
double rUprLeft; /* Floor for next label in the upper left quadrant */ |
|
129
|
double rLwrRight; /* Ceiling for label in the lower right quadrant */ |
|
130
|
double rLwrLeft; /* Ceiling for label in the lower left quadrant */ |
|
131
|
int i; /* Loop counter looping over wedge labels */ |
|
132
|
|
|
133
|
# define SATURATION 128 |
|
134
|
# define VALUE 192 |
|
135
|
# define OTHER_CUTOFF 90.0 |
|
136
|
# define TEXT_HEIGHT 15.0 |
|
137
|
|
|
138
|
cx = 0.5*width; |
|
139
|
cy = 0.5*height; |
|
140
|
r2 = cx<cy ? cx : cy; |
|
141
|
r = r2 - 80.0; |
|
142
|
if( r<0.33333*r2 ) r = 0.33333*r2; |
|
143
|
h = 0; |
|
144
|
zFg = skin_detail_boolean("white-foreground") ? "white" : "black"; |
|
145
|
|
|
146
|
db_prepare(&q, "SELECT sum(amt), count(*) FROM piechart"); |
|
147
|
if( db_step(&q)!=SQLITE_ROW ){ |
|
148
|
db_finalize(&q); |
|
149
|
return; |
|
150
|
} |
|
151
|
rTotal = db_column_double(&q, 0); |
|
152
|
nTotal = db_column_int(&q, 1); |
|
153
|
db_finalize(&q); |
|
154
|
rTooSmall = 0.0; |
|
155
|
nTooSmall = 0; |
|
156
|
if( (pieFlags & PIE_OTHER)!=0 && nTotal>1 ){ |
|
157
|
db_prepare(&q, "SELECT sum(amt), count(*) FROM piechart WHERE amt<:amt"); |
|
158
|
db_bind_double(&q, ":amt", rTotal/OTHER_CUTOFF); |
|
159
|
if( db_step(&q)==SQLITE_ROW ){ |
|
160
|
rTooSmall = db_column_double(&q, 0); |
|
161
|
nTooSmall = db_column_int(&q, 1); |
|
162
|
} |
|
163
|
db_finalize(&q); |
|
164
|
} |
|
165
|
if( nTooSmall>1 ){ |
|
166
|
db_prepare(&q, "SELECT amt, label FROM piechart WHERE amt>=:limit" |
|
167
|
" UNION ALL SELECT %.17g, '%d others';", |
|
168
|
rTooSmall, nTooSmall); |
|
169
|
db_bind_double(&q, ":limit", rTotal/OTHER_CUTOFF); |
|
170
|
nTotal += 1 - nTooSmall; |
|
171
|
}else{ |
|
172
|
db_prepare(&q, "SELECT amt, label FROM piechart"); |
|
173
|
} |
|
174
|
if( nTotal<=10 ) pieFlags |= PIE_CHROMATIC; |
|
175
|
for(j=0; db_step(&q)==SQLITE_ROW; j++){ |
|
176
|
double x = db_column_double(&q,0)/rTotal; |
|
177
|
const char *zLbl = db_column_text(&q,1); |
|
178
|
/* @ <!-- x=%g(x) zLbl="%h(zLbl)" h=%d(h) --> */ |
|
179
|
if( x<=0.0 ) continue; |
|
180
|
x1 = cx + sin(a1)*r; |
|
181
|
y1 = cy - cos(a1)*r; |
|
182
|
a2 = a1 + x*2.0*M_PI; |
|
183
|
x2 = cx + sin(a2)*r; |
|
184
|
y2 = cy - cos(a2)*r; |
|
185
|
a3 = 0.5*(a1+a2); |
|
186
|
if( nWedge+1>nWedgeAlloc ){ |
|
187
|
nWedgeAlloc = nWedgeAlloc*2 + 40; |
|
188
|
aWedge = fossil_realloc(aWedge, sizeof(aWedge[0])*nWedgeAlloc); |
|
189
|
} |
|
190
|
if( pieFlags & PIE_PERCENT ){ |
|
191
|
int pct = (int)(x*100.0 + 0.5); |
|
192
|
aWedge[nWedge].z = mprintf("%s (%d%%)", zLbl, pct); |
|
193
|
}else{ |
|
194
|
aWedge[nWedge].z = fossil_strdup(zLbl); |
|
195
|
} |
|
196
|
aWedge[nWedge].rSin = sin(a3); |
|
197
|
aWedge[nWedge].rCos = cos(a3); |
|
198
|
nWedge++; |
|
199
|
if( (j&1)==0 || (pieFlags & PIE_CHROMATIC)!=0 ){ |
|
200
|
h = 256*j/nTotal; |
|
201
|
}else if( j+2<nTotal ){ |
|
202
|
h = 256*(j+2)/nTotal; |
|
203
|
}else{ |
|
204
|
h = 256*((j+2+(nTotal&1))%nTotal)/nTotal; |
|
205
|
} |
|
206
|
zClr = rgbName(h,SATURATION,VALUE); |
|
207
|
l = x>=0.5; |
|
208
|
a1 = a2; |
|
209
|
@ <path class='piechartWedge' |
|
210
|
@ stroke="black" stroke-width="1" fill="%s(zClr)" |
|
211
|
@ d='M%g(cx),%g(cy)L%g(x1),%g(y1)A%g(r),%g(r) 0 %d(l),1 %g(x2),%g(y2)z'/> |
|
212
|
} |
|
213
|
qsort(aWedge, nWedge, sizeof(aWedge[0]), wedgeCompare); |
|
214
|
rUprLeft = height; |
|
215
|
rLwrLeft = 0; |
|
216
|
rUprRight = height; |
|
217
|
rLwrRight = 0; |
|
218
|
d1 = r*1.1; |
|
219
|
for(i=0; i<nWedge; i++){ |
|
220
|
WedgeLabel *p = &aWedge[i]; |
|
221
|
x3 = cx + p->rSin*r; |
|
222
|
y3 = cy - p->rCos*r; |
|
223
|
x4 = cx + p->rSin*d1; |
|
224
|
y4 = cy - p->rCos*d1; |
|
225
|
if( y4<=cy ){ |
|
226
|
if( x4>=cx ){ |
|
227
|
if( y4>rUprRight ){ |
|
228
|
y4 = rUprRight; |
|
229
|
} |
|
230
|
rUprRight = y4 - TEXT_HEIGHT; |
|
231
|
}else{ |
|
232
|
if( y4>rUprLeft ){ |
|
233
|
y4 = rUprLeft; |
|
234
|
} |
|
235
|
rUprLeft = y4 - TEXT_HEIGHT; |
|
236
|
} |
|
237
|
}else{ |
|
238
|
if( x4>=cx ){ |
|
239
|
if( y4<rLwrRight ){ |
|
240
|
y4 = rLwrRight; |
|
241
|
} |
|
242
|
rLwrRight = y4 + TEXT_HEIGHT; |
|
243
|
}else{ |
|
244
|
if( y4<rLwrLeft ){ |
|
245
|
y4 = rLwrLeft; |
|
246
|
} |
|
247
|
rLwrLeft = y4 + TEXT_HEIGHT; |
|
248
|
} |
|
249
|
} |
|
250
|
if( x4<cx ){ |
|
251
|
x5 = x4 - 1.0; |
|
252
|
zAnc = "end"; |
|
253
|
}else{ |
|
254
|
x5 = x4 + 1.0; |
|
255
|
zAnc = "start"; |
|
256
|
} |
|
257
|
y5 = y4 - 3.0 + 6.0*(1.0 - p->rCos); |
|
258
|
@ <line stroke-width='1' stroke='%s(zFg)' class='piechartLine' |
|
259
|
@ x1='%g(x3)' y1='%g(y3)' x2='%g(x4)' y2='%g(y4)'/> |
|
260
|
@ <text text-anchor="%s(zAnc)" fill='%s(zFg)' class="piechartLabel" |
|
261
|
@ x='%g(x5)' y='%g(y5)'>%h(p->z)</text> |
|
262
|
fossil_free(p->z); |
|
263
|
} |
|
264
|
db_finalize(&q); |
|
265
|
fossil_free(aWedge); |
|
266
|
} |
|
267
|
|
|
268
|
/* |
|
269
|
** WEBPAGE: test-piechart |
|
270
|
** |
|
271
|
** Generate a pie-chart based on data input from a form. |
|
272
|
*/ |
|
273
|
void piechart_test_page(void){ |
|
274
|
const char *zData; |
|
275
|
Stmt ins; |
|
276
|
int n = 0; |
|
277
|
int width; |
|
278
|
int height; |
|
279
|
int i, j; |
|
280
|
|
|
281
|
login_check_credentials(); |
|
282
|
style_set_current_feature("test"); |
|
283
|
style_header("Pie Chart Test"); |
|
284
|
db_multi_exec("CREATE TEMP TABLE piechart(amt REAL, label TEXT);"); |
|
285
|
db_prepare(&ins, "INSERT INTO piechart(amt,label) VALUES(:amt,:label)"); |
|
286
|
zData = PD("data",""); |
|
287
|
width = atoi( |