@@ -739,6 +739,10 @@ <h3 style="margin-top: 40px;">Cost and Time (by feedback status)</h3>
739739 < div class ="chart-container ">
740740 < canvas id ="averagesChart "> </ canvas >
741741 </ div >
742+ < h3 style ="margin-top: 40px; "> Patches and Submissions</ h3 >
743+ < div class ="chart-container ">
744+ < canvas id ="volumeChart "> </ canvas >
745+ </ div >
742746 < h3 style ="margin-top: 40px; "> Feedback Breakdown (36-hour rolling)</ h3 >
743747 < div class ="chart-container ">
744748 < canvas id ="feedbackChart "> </ canvas >
@@ -985,6 +989,7 @@ <h3 style="margin-top: 40px;">Feedback Breakdown (36-hour rolling)</h3>
985989 // Initialize and update review chart
986990 let reviewChart = null ;
987991 let averagesChart = null ;
992+ let volumeChart = null ;
988993 let feedbackChart = null ;
989994 loadCharts ( ) ;
990995 // Charts/tables only refresh on user action (filter/limit changes), not periodically
@@ -1030,6 +1035,7 @@ <h3 style="margin-top: 40px;">Feedback Breakdown (36-hour rolling)</h3>
10301035 updateStatsTable ( filteredReviews ) ;
10311036 updateReviewChart ( filteredReviews ) ;
10321037 updateAveragesChart ( filteredReviews ) ;
1038+ updateVolumeChart ( filteredReviews ) ;
10331039 updateFeedbackChart ( filteredReviews ) ;
10341040
10351041 // Also update the reviews list
@@ -1794,6 +1800,109 @@ <h3 style="margin-top: 40px;">Feedback Breakdown (36-hour rolling)</h3>
17941800 }
17951801 }
17961802
1803+ function updateVolumeChart ( reviews ) {
1804+ const bucketSize = parseInt ( document . getElementById ( 'bucketSize' ) . value ) ;
1805+
1806+ // Group reviews by time buckets, counting submissions and patches
1807+ const dataByBucket = { } ;
1808+
1809+ reviews . forEach ( review => {
1810+ if ( review . status !== 'done' ) return ;
1811+
1812+ const timestamp = parseUTCDate ( review . date ) ;
1813+ const bucketMs = bucketSize * 3600000 ;
1814+ const bucketStart = new Date ( Math . floor ( timestamp . getTime ( ) / bucketMs ) * bucketMs ) ;
1815+ const bucketKey = bucketStart . toISOString ( ) ;
1816+
1817+ if ( ! dataByBucket [ bucketKey ] ) {
1818+ dataByBucket [ bucketKey ] = { submissions : 0 , patches : 0 } ;
1819+ }
1820+
1821+ dataByBucket [ bucketKey ] . submissions ++ ;
1822+ dataByBucket [ bucketKey ] . patches += review . patch_count || 0 ;
1823+ } ) ;
1824+
1825+ const buckets = Object . keys ( dataByBucket ) . sort ( ) ;
1826+ const submissionsData = buckets . map ( b => ( { x : b , y : dataByBucket [ b ] . submissions || null } ) ) ;
1827+ const patchesData = buckets . map ( b => ( { x : b , y : dataByBucket [ b ] . patches || null } ) ) ;
1828+
1829+ const ctx = document . getElementById ( 'volumeChart' ) ;
1830+
1831+ if ( volumeChart ) {
1832+ volumeChart . data . datasets [ 0 ] . data = submissionsData ;
1833+ volumeChart . data . datasets [ 1 ] . data = patchesData ;
1834+ volumeChart . update ( ) ;
1835+ } else {
1836+ const textColor = getComputedStyle ( document . documentElement ) . getPropertyValue ( '--text-primary' ) ;
1837+ const borderColor = getComputedStyle ( document . documentElement ) . getPropertyValue ( '--border-color' ) ;
1838+
1839+ volumeChart = new Chart ( ctx , {
1840+ type : 'line' ,
1841+ data : {
1842+ datasets : [
1843+ {
1844+ label : 'Submissions' ,
1845+ data : submissionsData ,
1846+ borderColor : '#007bff' ,
1847+ backgroundColor : 'rgba(0, 123, 255, 0.1)' ,
1848+ tension : 0.1 ,
1849+ fill : true ,
1850+ spanGaps : false ,
1851+ yAxisID : 'y'
1852+ } ,
1853+ {
1854+ label : 'Patches' ,
1855+ data : patchesData ,
1856+ borderColor : '#28a745' ,
1857+ backgroundColor : 'rgba(40, 167, 69, 0.1)' ,
1858+ tension : 0.1 ,
1859+ fill : true ,
1860+ spanGaps : false ,
1861+ yAxisID : 'y'
1862+ }
1863+ ]
1864+ } ,
1865+ options : {
1866+ responsive : true ,
1867+ maintainAspectRatio : false ,
1868+ interaction : {
1869+ mode : 'index' ,
1870+ intersect : false ,
1871+ } ,
1872+ plugins : {
1873+ legend : {
1874+ position : 'top' ,
1875+ labels : { color : textColor }
1876+ } ,
1877+ tooltip : {
1878+ mode : 'index' ,
1879+ intersect : false ,
1880+ }
1881+ } ,
1882+ scales : {
1883+ x : {
1884+ type : 'time' ,
1885+ time : {
1886+ unit : 'hour' ,
1887+ stepSize : 6 ,
1888+ displayFormats : { hour : 'MMM d HH:mm' }
1889+ } ,
1890+ grid : { color : borderColor } ,
1891+ ticks : { color : textColor }
1892+ } ,
1893+ y : {
1894+ type : 'linear' ,
1895+ position : 'left' ,
1896+ beginAtZero : true ,
1897+ grid : { color : borderColor } ,
1898+ ticks : { color : textColor , precision : 0 }
1899+ }
1900+ }
1901+ }
1902+ } ) ;
1903+ }
1904+ }
1905+
17971906 function updateFeedbackChart ( reviews ) {
17981907 // Only process completed reviews
17991908 const completedReviews = reviews . filter ( r => r . status === 'done' ) ;
@@ -2628,6 +2737,15 @@ <h3 style="margin-top: 40px;">Feedback Breakdown (36-hour rolling)</h3>
26282737
26292738 averagesChart . update ( ) ;
26302739 }
2740+
2741+ if ( volumeChart ) {
2742+ volumeChart . options . plugins . legend . labels . color = textColor ;
2743+ volumeChart . options . scales . x . grid . color = borderColor ;
2744+ volumeChart . options . scales . x . ticks . color = textColor ;
2745+ volumeChart . options . scales . y . grid . color = borderColor ;
2746+ volumeChart . options . scales . y . ticks . color = textColor ;
2747+ volumeChart . update ( ) ;
2748+ }
26312749 }
26322750
26332751 function updateThemeButton ( theme ) {
0 commit comments