summaryrefslogtreecommitdiff
path: root/src/piechart.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/piechart.c')
-rw-r--r--src/piechart.c335
1 files changed, 335 insertions, 0 deletions
diff --git a/src/piechart.c b/src/piechart.c
new file mode 100644
index 0000000..7883636
--- /dev/null
+++ b/src/piechart.c
@@ -0,0 +1,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();
+}