summaryrefslogtreecommitdiff
path: root/src/piechart.c
blob: 7883636115f8a5bd25205b990a70389a7e933b5a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
/*
** Copyright (c) 2015 D. Richard Hipp
**
** This program is free software; you can redistribute it and/or
** modify it under the terms of the Simplified BSD License (also
** known as the "2-Clause License" or "FreeBSD License".)

** This program is distributed in the hope that it will be useful,
** but without any warranty; without even the implied warranty of
** merchantability or fitness for a particular purpose.
**
** Author contact information:
**   drh@hwaci.com
**   http://www.hwaci.com/drh/
**
*******************************************************************************
**
** This file contains code for generating pie charts on web pages.
**
*/
#include "config.h"
#include "piechart.h"
#include <math.h>

#ifndef M_PI
# define M_PI 3.1415926535897932385
#endif

/*
** Return an RGB color name given HSV values.  The HSV values
** must each be between 0 and 255.  The string
** returned is held in a static buffer and is overwritten
** on each call.
*/
const char *rgbName(unsigned char h, unsigned char s, unsigned char v){
  static char zColor[8];
  unsigned char A, B, C, r, g, b;
  unsigned int i, m;
  if( s==0 ){
    r = g = b = v;
  }else{
    i = (h*6)/256;
    m = (h*6)&0xff;
    A = v*(256-s)/256;
    B = v*(65536-s*m)/65536;
    C = v*(65536-s*(256-m))/65536;
    @ <!-- hsv=%d(h),%d(s),%d(v) i=%d(i) m=%d(m) ABC=%d(A),%d(B),%d(C) -->
    switch( i ){
      case 0:  r=v; g=C; b=A;  break;
      case 1:  r=B; g=v; b=A;  break;
      case 2:  r=A; g=v; b=C;  break;
      case 3:  r=A; g=B; b=v;  break;
      case 4:  r=C; g=A; b=v;  break;
      default: r=v; g=A; b=B;  break;
    }
  }
  sqlite3_snprintf(sizeof(zColor),zColor,"#%02x%02x%02x",r,g,b);
  return zColor;
}

/*
** Flags that can be passed into the pie-chart generator
*/
#if INTERFACE
#define PIE_OTHER     0x0001    /* No wedge less than 1/60th of the circle */
#define PIE_CHROMATIC 0x0002    /* Wedge colors are in chromatic order */
#define PIE_PERCENT   0x0004    /* Add "(XX%)" marks on each label */
#endif

/*
** A pie-chart wedge label
*/
struct WedgeLabel {
  double rCos, rSin;   /* Sine and Cosine of center angle of wedge */
  char *z;             /* Label to draw on this wedge */
};
typedef struct WedgeLabel WedgeLabel;

/*
** Comparison callback for qsort() to sort labels in order of increasing
** distance above and below the horizontal centerline.
*/
static int wedgeCompare(const void *a, const void *b){
  const WedgeLabel *pA = (const WedgeLabel*)a;
  const WedgeLabel *pB = (const WedgeLabel*)b;
  double rA = fabs(pA->rCos);
  double rB = fabs(pB->rCos);
  if( rA<rB ) return -1;
  if( rA>rB ) return +1;
  return 0;
}

/*
** Output HTML that will render a pie chart using data from
** the PIECHART temporary table.
**
** The schema for the PIECHART table should be:
**
**     CREATE TEMP TABLE piechart(amt REAL, label TEXT);
*/
void piechart_render(int width, int height, unsigned int pieFlags){
  Stmt q;
  double cx, cy;          /* center of the pie */
  double r, r2;           /* Radius of the pie */
  double x1,y1;           /* Start of the slice */
  double x2,y2;           /* End of the slice */
  double x3,y3;           /* Middle point of the slice */
  double x4,y4;           /* End of line extending from x3,y3 */
  double x5,y5;           /* Text anchor */
  double d1;              /* radius to x4,y4 */
  const char *zAnc;       /* Anchor point for text */
  double a1 = 0.0;        /* Angle for first edge of slice */
  double a2;              /* Angle for second edge */
  double a3;              /* Angle at middle of slice */
  unsigned char h;        /* Hue */
  const char *zClr;       /* Color */
  int l;                  /* Large arc flag */
  int j;                  /* Wedge number */
  double rTotal;          /* Total piechart.amt */
  double rTooSmall;       /* Sum of pieChart.amt entries less than 1/60th */
  int nTotal;             /* Total number of entries in piechart */
  int nTooSmall;          /* Number of pieChart.amt entries less than 1/60th */
  const char *zFg;        /* foreground color for lines and text */
  int nWedgeAlloc = 0;    /* Slots allocated for aWedge[] */
  int nWedge = 0;         /* Slots used for aWedge[] */
  WedgeLabel *aWedge = 0; /* Labels */
  double rUprRight;       /* Floor for next label in the upper right quadrant */
  double rUprLeft;        /* Floor for next label in the upper left quadrant */
  double rLwrRight;       /* Ceiling for label in the lower right quadrant */
  double rLwrLeft;        /* Ceiling for label in the lower left quadrant */
  int i;                  /* Loop counter looping over wedge labels */

# define SATURATION    128
# define VALUE         192
# define OTHER_CUTOFF  90.0
# define TEXT_HEIGHT   15.0

  cx = 0.5*width;
  cy = 0.5*height;
  r2 = cx<cy ? cx : cy;
  r = r2 - 80.0;
  if( r<0.33333*r2 ) r = 0.33333*r2;
  h = 0;
  zFg = skin_detail_boolean("white-foreground") ? "white" : "black";

  db_prepare(&q, "SELECT sum(amt), count(*) FROM piechart");
  if( db_step(&q)!=SQLITE_ROW ){
    db_finalize(&q);
    return;
  }
  rTotal = db_column_double(&q, 0);
  nTotal = db_column_int(&q, 1);
  db_finalize(&q);
  rTooSmall = 0.0;
  nTooSmall = 0;
  if( (pieFlags & PIE_OTHER)!=0 && nTotal>1 ){
    db_prepare(&q, "SELECT sum(amt), count(*) FROM piechart WHERE amt<:amt");
    db_bind_double(&q, ":amt", rTotal/OTHER_CUTOFF);
    if( db_step(&q)==SQLITE_ROW ){
      rTooSmall = db_column_double(&q, 0);
      nTooSmall = db_column_int(&q, 1);
    }
    db_finalize(&q);
  }
  if( nTooSmall>1 ){
    db_prepare(&q, "SELECT amt, label FROM piechart WHERE amt>=:limit"
                   " UNION ALL SELECT %.17g, '%d others';",
                    rTooSmall, nTooSmall);
    db_bind_double(&q, ":limit", rTotal/OTHER_CUTOFF);
    nTotal += 1 - nTooSmall;
  }else{
    db_prepare(&q, "SELECT amt, label FROM piechart");
  }
  if( nTotal<=10 ) pieFlags |= PIE_CHROMATIC;
  for(j=0; db_step(&q)==SQLITE_ROW; j++){
    double x = db_column_double(&q,0)/rTotal;
    const char *zLbl = db_column_text(&q,1);
    /* @ <!-- x=%g(x) zLbl="%h(zLbl)" h=%d(h) --> */
    if( x<=0.0 ) continue;
    x1 = cx + sin(a1)*r;
    y1 = cy - cos(a1)*r;
    a2 = a1 + x*2.0*M_PI;
    x2 = cx + sin(a2)*r;
    y2 = cy - cos(a2)*r;
    a3 = 0.5*(a1+a2);
    if( nWedge+1>nWedgeAlloc ){
      nWedgeAlloc = nWedgeAlloc*2 + 40;
      aWedge = fossil_realloc(aWedge, sizeof(aWedge[0])*nWedgeAlloc);
    }
    if( pieFlags & PIE_PERCENT ){
      int pct = (int)(x*100.0 + 0.5);
      aWedge[nWedge].z = mprintf("%s (%d%%)", zLbl, pct);
    }else{
      aWedge[nWedge].z = fossil_strdup(zLbl);
    }
    aWedge[nWedge].rSin = sin(a3);
    aWedge[nWedge].rCos = cos(a3);
    nWedge++;
    if( (j&1)==0 || (pieFlags & PIE_CHROMATIC)!=0 ){
      h = 256*j/nTotal;
    }else if( j+2<nTotal ){
      h = 256*(j+2)/nTotal;
    }else{
      h = 256*((j+2+(nTotal&1))%nTotal)/nTotal;
    }
    zClr = rgbName(h,SATURATION,VALUE);
    l = x>=0.5;
    a1 = a2;
    @ <path class='piechartWedge'
    @  stroke="black" stroke-width="1" fill="%s(zClr)"
    @  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'/>
  }
  qsort(aWedge, nWedge, sizeof(aWedge[0]), wedgeCompare);
  rUprLeft = height;
  rLwrLeft = 0;
  rUprRight = height;
  rLwrRight = 0;
  d1 = r*1.1;
  for(i=0; i<nWedge; i++){
    WedgeLabel *p = &aWedge[i];
    x3 = cx + p->rSin*r;
    y3 = cy - p->rCos*r;
    x4 = cx + p->rSin*d1;
    y4 = cy - p->rCos*d1;
    if( y4<=cy ){
      if( x4>=cx ){
        if( y4>rUprRight ){
          y4 = rUprRight;
        }
        rUprRight = y4 - TEXT_HEIGHT;
      }else{
        if( y4>rUprLeft ){
          y4 = rUprLeft;
        }
        rUprLeft = y4 - TEXT_HEIGHT;
      }
    }else{
      if( x4>=cx ){
        if( y4<rLwrRight ){
          y4 = rLwrRight;
        }
        rLwrRight = y4 + TEXT_HEIGHT;
      }else{
        if( y4<rLwrLeft ){
          y4 = rLwrLeft;
        }
        rLwrLeft = y4 + TEXT_HEIGHT;
      }
    }
    if( x4<cx ){
      x5 = x4 - 1.0;
      zAnc = "end";
    }else{
      x5 = x4 + 1.0;
      zAnc = "start";
    }
    y5 = y4 - 3.0 + 6.0*(1.0 - p->rCos);
    @ <line stroke-width='1' stroke='%s(zFg)' class='piechartLine'
    @  x1='%g(x3)' y1='%g(y3)' x2='%g(x4)' y2='%g(y4)'/>
    @ <text text-anchor="%s(zAnc)" fill='%s(zFg)' class="piechartLabel"
    @  x='%g(x5)' y='%g(y5)'>%h(p->z)</text>
    fossil_free(p->z);
  }
  db_finalize(&q);
  fossil_free(aWedge);
}

/*
** WEBPAGE: test-piechart
**
** Generate a pie-chart based on data input from a form.
*/
void piechart_test_page(void){
  const char *zData;
  Stmt ins;
  int n = 0;
  int width;
  int height;
  int i, j;

  login_check_credentials();
  style_set_current_feature("test");
  style_header("Pie Chart Test");
  db_multi_exec("CREATE TEMP TABLE piechart(amt REAL, label TEXT);");
  db_prepare(&ins, "INSERT INTO piechart(amt,label) VALUES(:amt,:label)");
  zData = PD("data","");
  width = atoi(PD("width","800"));
  height = atoi(PD("height","400"));
  i = 0;
  while( zData[i] ){
    double rAmt;
    char *zLabel;
    while( fossil_isspace(zData[i]) ){ i++; }
    j = i;
    while( fossil_isdigit(zData[j]) ){ j++; }
    if( zData[j]=='.' ){
      j++;
      while( fossil_isdigit(zData[j]) ){ j++; }
    }
    if( i==j ) break;
    rAmt = atof(&zData[i]);
    i = j;
    while( zData[i]==',' || fossil_isspace(zData[i]) ){ i++; }
    n++;
    zLabel = mprintf("label%02d-%g", n, rAmt);
    db_bind_double(&ins, ":amt", rAmt);
    db_bind_text(&ins, ":label", zLabel);
    db_step(&ins);
    db_reset(&ins);
    fossil_free(zLabel);
  }
  db_finalize(&ins);
  if( n>1 ){
    @ <svg width=%d(width) height=%d(height) style="border:1px solid #d3d3d3;">
    piechart_render(width,height, PIE_OTHER|PIE_PERCENT);
    @ </svg>
    @ <hr>
  }
  @ <form method="POST" action='%R/test-piechart'>
  @ <p>Comma-separated list of slice widths:<br>
  @ <input type='text' name='data' size='80' value='%h(zData)'/><br>
  @ Width: <input type='text' size='8' name='width' value='%d(width)'/>
  @ Height: <input type='text' size='8' name='height' value='%d(height)'/><br>
  @ <input type='submit' value='Draw The Pie Chart'/>
  @ </form>
  @ <p>Interesting test cases:
  @ <ul>
  @ <li> <a href='test-piechart?data=44,2,2,2,2,2,3,2,2,2,2,2,44'>Case 1</a>
  @ <li> <a href='test-piechart?data=2,2,2,2,2,44,44,2,2,2,2,2'>Case 2</a>
  @ <li> <a href='test-piechart?data=20,2,2,2,2,2,2,2,2,2,2,80'>Case 3</a>
  @ <li> <a href='test-piechart?data=80,2,2,2,2,2,2,2,2,2,2,20'>Case 4</a>
  @ <li> <a href='test-piechart?data=2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2'>Case 5</a>
  @ </ul>
  style_finish_page();
}