Fossil SCM
Add a by-hour-of-day report to the /reports page, prompted by a /chat discussion.
Commit
bb6f23313ed6e48a322d41b210da413006e8f747d264b11a55ae59f88ba91f06
Parent
cf563c721773e65…
2 files changed
+80
-9
+1
+80
-9
| --- src/statrep.c | ||
| +++ src/statrep.c | ||
| @@ -197,16 +197,11 @@ | ||
| 197 | 197 | the per-year event totals */ |
| 198 | 198 | int nMaxEvents = 1; /* for calculating length of graph |
| 199 | 199 | bars. */ |
| 200 | 200 | int iterations = 0; /* number of weeks/months we iterate |
| 201 | 201 | over */ |
| 202 | - Blob userFilter = empty_blob; /* Optional user=johndoe query string */ | |
| 203 | 202 | stats_report_init_view(); |
| 204 | - if( zUserName ){ | |
| 205 | - blob_appendf(&userFilter, "user=%s", zUserName); | |
| 206 | - } | |
| 207 | - blob_reset(&userFilter); | |
| 208 | 203 | db_prepare(&query, |
| 209 | 204 | "SELECT substr(date(mtime),1,%d) AS timeframe," |
| 210 | 205 | " count(*) AS eventCount" |
| 211 | 206 | " FROM v_reports" |
| 212 | 207 | " WHERE ifnull(coalesce(euser,user,'')=%Q,1)" |
| @@ -477,20 +472,16 @@ | ||
| 477 | 472 | int nRowNumber = 0; /* current TR number */ |
| 478 | 473 | int rowClass = 0; /* counter for alternating |
| 479 | 474 | row colors */ |
| 480 | 475 | int nMaxEvents = 1; /* max number of events for |
| 481 | 476 | all rows. */ |
| 482 | - Blob userFilter = empty_blob; /* Optional user=johndoe query string */ | |
| 483 | 477 | static const char *const daysOfWeek[] = { |
| 484 | 478 | "Sunday", "Monday", "Tuesday", "Wednesday", |
| 485 | 479 | "Thursday", "Friday", "Saturday" |
| 486 | 480 | }; |
| 487 | 481 | |
| 488 | 482 | stats_report_init_view(); |
| 489 | - if( zUserName ){ | |
| 490 | - blob_appendf(&userFilter, "user=%s", zUserName); | |
| 491 | - } | |
| 492 | 483 | db_prepare(&query, |
| 493 | 484 | "SELECT cast(strftime('%%w', mtime) AS INTEGER) dow," |
| 494 | 485 | " COUNT(*) AS eventCount" |
| 495 | 486 | " FROM v_reports" |
| 496 | 487 | " WHERE ifnull(coalesce(euser,user,'')=%Q,1)" |
| @@ -550,10 +541,85 @@ | ||
| 550 | 541 | rowClass = ++nRowNumber % 2; |
| 551 | 542 | @<tr class='row%d(rowClass)'> |
| 552 | 543 | @ <td>%d(dayNum)</td> |
| 553 | 544 | @ <td>%s(daysOfWeek[dayNum])</td> |
| 554 | 545 | @ <td>%d(nCount)</td> |
| 546 | + @ <td> | |
| 547 | + @ <div class='statistics-report-graph-line' | |
| 548 | + @ style='width:%d(nSize)%%;'> </div> | |
| 549 | + @ </td> | |
| 550 | + @</tr> | |
| 551 | + } | |
| 552 | + @ </tbody></table> | |
| 553 | + db_finalize(&query); | |
| 554 | +} | |
| 555 | + | |
| 556 | +/* | |
| 557 | +** Implements the "byhour" view for /reports. If zUserName is not NULL | |
| 558 | +** then the report is restricted to events created by the named user | |
| 559 | +** account. | |
| 560 | +*/ | |
| 561 | +static void stats_report_hour_of_day(const char *zUserName){ | |
| 562 | + Stmt query = empty_Stmt; | |
| 563 | + int nRowNumber = 0; /* current TR number */ | |
| 564 | + int rowClass = 0; /* counter for alternating | |
| 565 | + row colors */ | |
| 566 | + int nMaxEvents = 1; /* max number of events for | |
| 567 | + all rows. */ | |
| 568 | + | |
| 569 | + stats_report_init_view(); | |
| 570 | + db_prepare(&query, | |
| 571 | + "SELECT cast(strftime('%%H', mtime) AS INTEGER) hod," | |
| 572 | + " COUNT(*) AS eventCount" | |
| 573 | + " FROM v_reports" | |
| 574 | + " WHERE ifnull(coalesce(euser,user,'')=%Q,1)" | |
| 575 | + " GROUP BY hod ORDER BY hod", zUserName); | |
| 576 | + @ <h1>Timeline Events (%h(stats_report_label_for_type())) by Hour of Day | |
| 577 | + if( zUserName ){ | |
| 578 | + @ for user %h(zUserName) | |
| 579 | + } | |
| 580 | + @ </h1> | |
| 581 | + db_multi_exec( | |
| 582 | + "CREATE TEMP VIEW piechart(amt,label) AS" | |
| 583 | + " SELECT count(*), strftime('%%H', mtime) hod" | |
| 584 | + " FROM v_reports" | |
| 585 | + " WHERE ifnull(coalesce(euser,user,'')=%Q,1)" | |
| 586 | + " GROUP BY 2 ORDER BY hod;", | |
| 587 | + zUserName | |
| 588 | + ); | |
| 589 | + if( db_int(0, "SELECT count(*) FROM piechart")>=2 ){ | |
| 590 | + @ <center><svg width=700 height=400> | |
| 591 | + piechart_render(700, 400, PIE_OTHER|PIE_PERCENT); | |
| 592 | + @ </svg></centre><hr /> | |
| 593 | + } | |
| 594 | + style_table_sorter(); | |
| 595 | + @ <table class='statistics-report-table-events sortable' border='0' \ | |
| 596 | + @ cellpadding='2' cellspacing='0' data-column-types='nnx' data-init-sort='1'> | |
| 597 | + @ <thead><tr> | |
| 598 | + @ <th>Hour</th> | |
| 599 | + @ <th>Events</th> | |
| 600 | + @ <th width='90%%'><!-- relative commits graph --></th> | |
| 601 | + @ </tr></thead><tbody> | |
| 602 | + while( SQLITE_ROW == db_step(&query) ){ | |
| 603 | + const int nCount = db_column_int(&query, 1); | |
| 604 | + if(nCount>nMaxEvents){ | |
| 605 | + nMaxEvents = nCount; | |
| 606 | + } | |
| 607 | + } | |
| 608 | + db_reset(&query); | |
| 609 | + while( SQLITE_ROW == db_step(&query) ){ | |
| 610 | + const int hourNum =db_column_int(&query, 0); | |
| 611 | + const int nCount = db_column_int(&query, 1); | |
| 612 | + int nSize = nCount | |
| 613 | + ? (int)(100 * nCount / nMaxEvents) | |
| 614 | + : 0; | |
| 615 | + if(!nCount) continue /* arguable! Possible? */; | |
| 616 | + else if(!nSize) nSize = 1; | |
| 617 | + rowClass = ++nRowNumber % 2; | |
| 618 | + @<tr class='row%d(rowClass)'> | |
| 619 | + @ <td>%d(hourNum)</td> | |
| 620 | + @ <td>%d(nCount)</td> | |
| 555 | 621 | @ <td> |
| 556 | 622 | @ <div class='statistics-report-graph-line' |
| 557 | 623 | @ style='width:%d(nSize)%%;'> </div> |
| 558 | 624 | @ </td> |
| 559 | 625 | @</tr> |
| @@ -709,10 +775,11 @@ | ||
| 709 | 775 | #define RPT_BYUSER 3 |
| 710 | 776 | #define RPT_BYWEEK 4 |
| 711 | 777 | #define RPT_BYWEEKDAY 5 |
| 712 | 778 | #define RPT_BYYEAR 6 |
| 713 | 779 | #define RPT_LASTCHNG 7 /* Last change made for each user */ |
| 780 | +#define RPT_BYHOUR 8 /* hour-of-day */ | |
| 714 | 781 | #define RPT_NONE 0 /* None of the above */ |
| 715 | 782 | |
| 716 | 783 | /* |
| 717 | 784 | ** WEBPAGE: reports |
| 718 | 785 | ** |
| @@ -749,10 +816,11 @@ | ||
| 749 | 816 | { "By Month", "bymonth", RPT_BYMONTH }, |
| 750 | 817 | { "By User", "byuser", RPT_BYUSER }, |
| 751 | 818 | { "By Week", "byweek", RPT_BYWEEK }, |
| 752 | 819 | { "By Weekday", "byweekday", RPT_BYWEEKDAY }, |
| 753 | 820 | { "By Year", "byyear", RPT_BYYEAR }, |
| 821 | + { "By Hour", "byhour", RPT_BYHOUR }, | |
| 754 | 822 | }; |
| 755 | 823 | static const char *const azType[] = { |
| 756 | 824 | "a", "All Changes", |
| 757 | 825 | "ci", "Check-ins", |
| 758 | 826 | "f", "Forum Posts", |
| @@ -817,11 +885,14 @@ | ||
| 817 | 885 | stats_report_day_of_week(zUserName); |
| 818 | 886 | break; |
| 819 | 887 | case RPT_BYFILE: |
| 820 | 888 | stats_report_by_file(zUserName); |
| 821 | 889 | break; |
| 890 | + case RPT_BYHOUR: | |
| 891 | + stats_report_hour_of_day(zUserName); | |
| 892 | + break; | |
| 822 | 893 | case RPT_LASTCHNG: |
| 823 | 894 | stats_report_last_change(); |
| 824 | 895 | break; |
| 825 | 896 | } |
| 826 | 897 | style_finish_page(); |
| 827 | 898 | } |
| 828 | 899 |
| --- src/statrep.c | |
| +++ src/statrep.c | |
| @@ -197,16 +197,11 @@ | |
| 197 | the per-year event totals */ |
| 198 | int nMaxEvents = 1; /* for calculating length of graph |
| 199 | bars. */ |
| 200 | int iterations = 0; /* number of weeks/months we iterate |
| 201 | over */ |
| 202 | Blob userFilter = empty_blob; /* Optional user=johndoe query string */ |
| 203 | stats_report_init_view(); |
| 204 | if( zUserName ){ |
| 205 | blob_appendf(&userFilter, "user=%s", zUserName); |
| 206 | } |
| 207 | blob_reset(&userFilter); |
| 208 | db_prepare(&query, |
| 209 | "SELECT substr(date(mtime),1,%d) AS timeframe," |
| 210 | " count(*) AS eventCount" |
| 211 | " FROM v_reports" |
| 212 | " WHERE ifnull(coalesce(euser,user,'')=%Q,1)" |
| @@ -477,20 +472,16 @@ | |
| 477 | int nRowNumber = 0; /* current TR number */ |
| 478 | int rowClass = 0; /* counter for alternating |
| 479 | row colors */ |
| 480 | int nMaxEvents = 1; /* max number of events for |
| 481 | all rows. */ |
| 482 | Blob userFilter = empty_blob; /* Optional user=johndoe query string */ |
| 483 | static const char *const daysOfWeek[] = { |
| 484 | "Sunday", "Monday", "Tuesday", "Wednesday", |
| 485 | "Thursday", "Friday", "Saturday" |
| 486 | }; |
| 487 | |
| 488 | stats_report_init_view(); |
| 489 | if( zUserName ){ |
| 490 | blob_appendf(&userFilter, "user=%s", zUserName); |
| 491 | } |
| 492 | db_prepare(&query, |
| 493 | "SELECT cast(strftime('%%w', mtime) AS INTEGER) dow," |
| 494 | " COUNT(*) AS eventCount" |
| 495 | " FROM v_reports" |
| 496 | " WHERE ifnull(coalesce(euser,user,'')=%Q,1)" |
| @@ -550,10 +541,85 @@ | |
| 550 | rowClass = ++nRowNumber % 2; |
| 551 | @<tr class='row%d(rowClass)'> |
| 552 | @ <td>%d(dayNum)</td> |
| 553 | @ <td>%s(daysOfWeek[dayNum])</td> |
| 554 | @ <td>%d(nCount)</td> |
| 555 | @ <td> |
| 556 | @ <div class='statistics-report-graph-line' |
| 557 | @ style='width:%d(nSize)%%;'> </div> |
| 558 | @ </td> |
| 559 | @</tr> |
| @@ -709,10 +775,11 @@ | |
| 709 | #define RPT_BYUSER 3 |
| 710 | #define RPT_BYWEEK 4 |
| 711 | #define RPT_BYWEEKDAY 5 |
| 712 | #define RPT_BYYEAR 6 |
| 713 | #define RPT_LASTCHNG 7 /* Last change made for each user */ |
| 714 | #define RPT_NONE 0 /* None of the above */ |
| 715 | |
| 716 | /* |
| 717 | ** WEBPAGE: reports |
| 718 | ** |
| @@ -749,10 +816,11 @@ | |
| 749 | { "By Month", "bymonth", RPT_BYMONTH }, |
| 750 | { "By User", "byuser", RPT_BYUSER }, |
| 751 | { "By Week", "byweek", RPT_BYWEEK }, |
| 752 | { "By Weekday", "byweekday", RPT_BYWEEKDAY }, |
| 753 | { "By Year", "byyear", RPT_BYYEAR }, |
| 754 | }; |
| 755 | static const char *const azType[] = { |
| 756 | "a", "All Changes", |
| 757 | "ci", "Check-ins", |
| 758 | "f", "Forum Posts", |
| @@ -817,11 +885,14 @@ | |
| 817 | stats_report_day_of_week(zUserName); |
| 818 | break; |
| 819 | case RPT_BYFILE: |
| 820 | stats_report_by_file(zUserName); |
| 821 | break; |
| 822 | case RPT_LASTCHNG: |
| 823 | stats_report_last_change(); |
| 824 | break; |
| 825 | } |
| 826 | style_finish_page(); |
| 827 | } |
| 828 |
| --- src/statrep.c | |
| +++ src/statrep.c | |
| @@ -197,16 +197,11 @@ | |
| 197 | the per-year event totals */ |
| 198 | int nMaxEvents = 1; /* for calculating length of graph |
| 199 | bars. */ |
| 200 | int iterations = 0; /* number of weeks/months we iterate |
| 201 | over */ |
| 202 | stats_report_init_view(); |
| 203 | db_prepare(&query, |
| 204 | "SELECT substr(date(mtime),1,%d) AS timeframe," |
| 205 | " count(*) AS eventCount" |
| 206 | " FROM v_reports" |
| 207 | " WHERE ifnull(coalesce(euser,user,'')=%Q,1)" |
| @@ -477,20 +472,16 @@ | |
| 472 | int nRowNumber = 0; /* current TR number */ |
| 473 | int rowClass = 0; /* counter for alternating |
| 474 | row colors */ |
| 475 | int nMaxEvents = 1; /* max number of events for |
| 476 | all rows. */ |
| 477 | static const char *const daysOfWeek[] = { |
| 478 | "Sunday", "Monday", "Tuesday", "Wednesday", |
| 479 | "Thursday", "Friday", "Saturday" |
| 480 | }; |
| 481 | |
| 482 | stats_report_init_view(); |
| 483 | db_prepare(&query, |
| 484 | "SELECT cast(strftime('%%w', mtime) AS INTEGER) dow," |
| 485 | " COUNT(*) AS eventCount" |
| 486 | " FROM v_reports" |
| 487 | " WHERE ifnull(coalesce(euser,user,'')=%Q,1)" |
| @@ -550,10 +541,85 @@ | |
| 541 | rowClass = ++nRowNumber % 2; |
| 542 | @<tr class='row%d(rowClass)'> |
| 543 | @ <td>%d(dayNum)</td> |
| 544 | @ <td>%s(daysOfWeek[dayNum])</td> |
| 545 | @ <td>%d(nCount)</td> |
| 546 | @ <td> |
| 547 | @ <div class='statistics-report-graph-line' |
| 548 | @ style='width:%d(nSize)%%;'> </div> |
| 549 | @ </td> |
| 550 | @</tr> |
| 551 | } |
| 552 | @ </tbody></table> |
| 553 | db_finalize(&query); |
| 554 | } |
| 555 | |
| 556 | /* |
| 557 | ** Implements the "byhour" view for /reports. If zUserName is not NULL |
| 558 | ** then the report is restricted to events created by the named user |
| 559 | ** account. |
| 560 | */ |
| 561 | static void stats_report_hour_of_day(const char *zUserName){ |
| 562 | Stmt query = empty_Stmt; |
| 563 | int nRowNumber = 0; /* current TR number */ |
| 564 | int rowClass = 0; /* counter for alternating |
| 565 | row colors */ |
| 566 | int nMaxEvents = 1; /* max number of events for |
| 567 | all rows. */ |
| 568 | |
| 569 | stats_report_init_view(); |
| 570 | db_prepare(&query, |
| 571 | "SELECT cast(strftime('%%H', mtime) AS INTEGER) hod," |
| 572 | " COUNT(*) AS eventCount" |
| 573 | " FROM v_reports" |
| 574 | " WHERE ifnull(coalesce(euser,user,'')=%Q,1)" |
| 575 | " GROUP BY hod ORDER BY hod", zUserName); |
| 576 | @ <h1>Timeline Events (%h(stats_report_label_for_type())) by Hour of Day |
| 577 | if( zUserName ){ |
| 578 | @ for user %h(zUserName) |
| 579 | } |
| 580 | @ </h1> |
| 581 | db_multi_exec( |
| 582 | "CREATE TEMP VIEW piechart(amt,label) AS" |
| 583 | " SELECT count(*), strftime('%%H', mtime) hod" |
| 584 | " FROM v_reports" |
| 585 | " WHERE ifnull(coalesce(euser,user,'')=%Q,1)" |
| 586 | " GROUP BY 2 ORDER BY hod;", |
| 587 | zUserName |
| 588 | ); |
| 589 | if( db_int(0, "SELECT count(*) FROM piechart")>=2 ){ |
| 590 | @ <center><svg width=700 height=400> |
| 591 | piechart_render(700, 400, PIE_OTHER|PIE_PERCENT); |
| 592 | @ </svg></centre><hr /> |
| 593 | } |
| 594 | style_table_sorter(); |
| 595 | @ <table class='statistics-report-table-events sortable' border='0' \ |
| 596 | @ cellpadding='2' cellspacing='0' data-column-types='nnx' data-init-sort='1'> |
| 597 | @ <thead><tr> |
| 598 | @ <th>Hour</th> |
| 599 | @ <th>Events</th> |
| 600 | @ <th width='90%%'><!-- relative commits graph --></th> |
| 601 | @ </tr></thead><tbody> |
| 602 | while( SQLITE_ROW == db_step(&query) ){ |
| 603 | const int nCount = db_column_int(&query, 1); |
| 604 | if(nCount>nMaxEvents){ |
| 605 | nMaxEvents = nCount; |
| 606 | } |
| 607 | } |
| 608 | db_reset(&query); |
| 609 | while( SQLITE_ROW == db_step(&query) ){ |
| 610 | const int hourNum =db_column_int(&query, 0); |
| 611 | const int nCount = db_column_int(&query, 1); |
| 612 | int nSize = nCount |
| 613 | ? (int)(100 * nCount / nMaxEvents) |
| 614 | : 0; |
| 615 | if(!nCount) continue /* arguable! Possible? */; |
| 616 | else if(!nSize) nSize = 1; |
| 617 | rowClass = ++nRowNumber % 2; |
| 618 | @<tr class='row%d(rowClass)'> |
| 619 | @ <td>%d(hourNum)</td> |
| 620 | @ <td>%d(nCount)</td> |
| 621 | @ <td> |
| 622 | @ <div class='statistics-report-graph-line' |
| 623 | @ style='width:%d(nSize)%%;'> </div> |
| 624 | @ </td> |
| 625 | @</tr> |
| @@ -709,10 +775,11 @@ | |
| 775 | #define RPT_BYUSER 3 |
| 776 | #define RPT_BYWEEK 4 |
| 777 | #define RPT_BYWEEKDAY 5 |
| 778 | #define RPT_BYYEAR 6 |
| 779 | #define RPT_LASTCHNG 7 /* Last change made for each user */ |
| 780 | #define RPT_BYHOUR 8 /* hour-of-day */ |
| 781 | #define RPT_NONE 0 /* None of the above */ |
| 782 | |
| 783 | /* |
| 784 | ** WEBPAGE: reports |
| 785 | ** |
| @@ -749,10 +816,11 @@ | |
| 816 | { "By Month", "bymonth", RPT_BYMONTH }, |
| 817 | { "By User", "byuser", RPT_BYUSER }, |
| 818 | { "By Week", "byweek", RPT_BYWEEK }, |
| 819 | { "By Weekday", "byweekday", RPT_BYWEEKDAY }, |
| 820 | { "By Year", "byyear", RPT_BYYEAR }, |
| 821 | { "By Hour", "byhour", RPT_BYHOUR }, |
| 822 | }; |
| 823 | static const char *const azType[] = { |
| 824 | "a", "All Changes", |
| 825 | "ci", "Check-ins", |
| 826 | "f", "Forum Posts", |
| @@ -817,11 +885,14 @@ | |
| 885 | stats_report_day_of_week(zUserName); |
| 886 | break; |
| 887 | case RPT_BYFILE: |
| 888 | stats_report_by_file(zUserName); |
| 889 | break; |
| 890 | case RPT_BYHOUR: |
| 891 | stats_report_hour_of_day(zUserName); |
| 892 | break; |
| 893 | case RPT_LASTCHNG: |
| 894 | stats_report_last_change(); |
| 895 | break; |
| 896 | } |
| 897 | style_finish_page(); |
| 898 | } |
| 899 |
+1
| --- www/changes.wiki | ||
| +++ www/changes.wiki | ||
| @@ -11,10 +11,11 @@ | ||
| 11 | 11 | commands which still used the former name, for consistency. |
| 12 | 12 | * Rebuilt [/file/Dockerfile | the stock Dockerfile] to create a "from scratch" |
| 13 | 13 | Busybox based container image via an Alpine Linux intermediary |
| 14 | 14 | * Added [/doc/trunk/www/containers.md | a new document] describing how to |
| 15 | 15 | customize, use, and run that container. |
| 16 | + * Added "by hour of day" report to [/reports?view=byhour|the /reports page]. | |
| 16 | 17 | |
| 17 | 18 | <h2 id='v2_19'>Changes for version 2.19 (2022-07-21)</h2> |
| 18 | 19 | * On file listing pages, sort filenames using the "uintnocase" collating |
| 19 | 20 | sequence, so that filenames that contains embedded integers sort in |
| 20 | 21 | numeric order even if they contain a different number of digits. |
| 21 | 22 |
| --- www/changes.wiki | |
| +++ www/changes.wiki | |
| @@ -11,10 +11,11 @@ | |
| 11 | commands which still used the former name, for consistency. |
| 12 | * Rebuilt [/file/Dockerfile | the stock Dockerfile] to create a "from scratch" |
| 13 | Busybox based container image via an Alpine Linux intermediary |
| 14 | * Added [/doc/trunk/www/containers.md | a new document] describing how to |
| 15 | customize, use, and run that container. |
| 16 | |
| 17 | <h2 id='v2_19'>Changes for version 2.19 (2022-07-21)</h2> |
| 18 | * On file listing pages, sort filenames using the "uintnocase" collating |
| 19 | sequence, so that filenames that contains embedded integers sort in |
| 20 | numeric order even if they contain a different number of digits. |
| 21 |
| --- www/changes.wiki | |
| +++ www/changes.wiki | |
| @@ -11,10 +11,11 @@ | |
| 11 | commands which still used the former name, for consistency. |
| 12 | * Rebuilt [/file/Dockerfile | the stock Dockerfile] to create a "from scratch" |
| 13 | Busybox based container image via an Alpine Linux intermediary |
| 14 | * Added [/doc/trunk/www/containers.md | a new document] describing how to |
| 15 | customize, use, and run that container. |
| 16 | * Added "by hour of day" report to [/reports?view=byhour|the /reports page]. |
| 17 | |
| 18 | <h2 id='v2_19'>Changes for version 2.19 (2022-07-21)</h2> |
| 19 | * On file listing pages, sort filenames using the "uintnocase" collating |
| 20 | sequence, so that filenames that contains embedded integers sort in |
| 21 | numeric order even if they contain a different number of digits. |
| 22 |