@@ -191,46 +191,129 @@ function countUp(node, target) {
191191( function history ( ) {
192192 const totalEl = document . getElementById ( "history-total" ) ;
193193 const countsEl = document . getElementById ( "history-counts" ) ;
194+ const chartEl = document . getElementById ( "history-chart" ) ;
194195 const listEl = document . getElementById ( "history-list" ) ;
195- if ( ! totalEl || ! countsEl || ! listEl ) return ;
196+ if ( ! totalEl || ! countsEl || ! chartEl || ! listEl ) return ;
196197
197198 const order = [ "smartphones" , "socs" , "gpus" , "cpus" , "brands" ] ;
198199 const label = { smartphones : "Phones" , socs : "SoCs" , gpus : "GPUs" , cpus : "CPUs" , brands : "Brands" } ;
200+ const shortLabel = { smartphones : "phones" , socs : "socs" , gpus : "gpus" , cpus : "cpus" , brands : "brands" } ;
201+ const dumpPath = "site/public/v1/index.json" ;
202+ const countRows = ( manifest ) => order
203+ . map ( ( key ) => ( { key, count : manifest . collections ?. [ key ] ?. count } ) )
204+ . filter ( ( row ) => row . count != null ) ;
205+ const totalRecords = ( manifest ) => countRows ( manifest ) . reduce ( ( sum , row ) => sum + row . count , 0 ) ;
206+ const sumByKey = ( rows ) => rows . reduce ( ( out , row ) => {
207+ out [ row . key ] = row . count ;
208+ return out ;
209+ } , { } ) ;
199210
200- getJSON ( "v1/index.json" ) . then ( ( manifest ) => {
201- const rows = order
202- . map ( ( key ) => ( { key, count : manifest . collections ?. [ key ] ?. count } ) )
203- . filter ( ( row ) => row . count != null ) ;
211+ function renderSnapshot ( manifest ) {
212+ const rows = countRows ( manifest ) ;
204213 const total = rows . reduce ( ( sum , row ) => sum + row . count , 0 ) ;
205214 totalEl . textContent = total . toLocaleString ( ) + " records" ;
206215 countsEl . innerHTML = rows . map ( ( row ) =>
207216 `<div class="history-count"><span>${ label [ row . key ] } </span><b>${ row . count . toLocaleString ( ) } </b></div>`
208217 ) . join ( "" ) ;
218+ }
219+
220+ function largestChanges ( prevRows , nextRows ) {
221+ if ( ! prevRows ) return [ ] ;
222+ const prev = sumByKey ( prevRows ) ;
223+ return nextRows
224+ . map ( ( row ) => ( { key : row . key , delta : row . count - ( prev [ row . key ] || 0 ) } ) )
225+ . filter ( ( row ) => row . delta > 0 )
226+ . sort ( ( a , b ) => b . delta - a . delta )
227+ . slice ( 0 , 2 ) ;
228+ }
229+
230+ function renderHistory ( points ) {
231+ if ( ! points . length ) throw new Error ( "empty history" ) ;
232+ const maxTotal = Math . max ( ...points . map ( ( point ) => point . total ) ) ;
233+ const minTotal = Math . min ( ...points . map ( ( point ) => point . total ) ) ;
234+ const range = Math . max ( 1 , maxTotal - minTotal ) ;
235+ chartEl . innerHTML = points . map ( ( point ) => {
236+ const pct = 18 + ( ( point . total - minTotal ) / range ) * 82 ;
237+ const deltaText = point . delta > 0 ? "+" + point . delta . toLocaleString ( ) : "baseline" ;
238+ return `<a class="history-bar" href="${ esc ( point . url ) } " target="_blank" rel="noopener" style="--h:${ pct . toFixed ( 1 ) } %" title="${ esc ( point . title ) } ">
239+ <span class="history-bar-fill"></span>
240+ <span class="history-bar-total">${ point . total . toLocaleString ( ) } </span>
241+ <span class="history-bar-delta">${ esc ( deltaText ) } </span>
242+ </a>` ;
243+ } ) . join ( "" ) ;
244+
245+ listEl . innerHTML = points . slice ( ) . reverse ( ) . map ( ( point ) => {
246+ const changes = point . changes . length
247+ ? point . changes . map ( ( row ) => `${ shortLabel [ row . key ] } +${ row . delta . toLocaleString ( ) } ` ) . join ( ", " )
248+ : ( point . delta > 0 ? `total +${ point . delta . toLocaleString ( ) } ` : "baseline snapshot" ) ;
249+ return `<li><span class="history-dot"></span><span>
250+ <a href="${ esc ( point . url ) } " target="_blank" rel="noopener">${ esc ( point . title ) } </a>
251+ <small>${ esc ( point . when ) } - ${ esc ( point . sha ) } - ${ esc ( changes ) } </small>
252+ </span></li>` ;
253+ } ) . join ( "" ) ;
254+ }
255+
256+ async function loadCommitHistory ( currentManifest ) {
257+ const commitsUrl = `https://api.github.com/repos/GetTechAPI/TechAPI/commits?path=${ encodeURIComponent ( dumpPath ) } &per_page=8` ;
258+ const response = await fetch ( commitsUrl ) ;
259+ if ( ! response . ok ) throw new Error ( response . statusText ) ;
260+ const commits = await response . json ( ) ;
261+ const items = Array . isArray ( commits ) ? commits . slice ( 0 , 7 ) : [ ] ;
262+ const snapshots = await Promise . all ( items . map ( async ( item ) => {
263+ const sha = String ( item . sha || "" ) ;
264+ const rawUrl = `https://raw.githubusercontent.com/GetTechAPI/TechAPI/${ sha } /${ dumpPath } ` ;
265+ const raw = await fetch ( rawUrl ) ;
266+ if ( ! raw . ok ) return null ;
267+ const manifest = await raw . json ( ) ;
268+ const date = item . commit ?. committer ?. date ? new Date ( item . commit . committer . date ) : null ;
269+ return {
270+ sha : sha . slice ( 0 , 7 ) ,
271+ dateValue : date ? date . getTime ( ) : 0 ,
272+ when : date ? date . toLocaleDateString ( undefined , { month : "short" , day : "numeric" , year : "numeric" } ) : "recent" ,
273+ title : ( item . commit ?. message || "Dataset sync" ) . split ( "\n" ) [ 0 ] ,
274+ url : item . html_url || "https://github.com/GetTechAPI/TechAPI" ,
275+ rows : countRows ( manifest ) ,
276+ total : totalRecords ( manifest ) ,
277+ } ;
278+ } ) ) ;
279+
280+ const points = snapshots . filter ( Boolean ) . sort ( ( a , b ) => a . dateValue - b . dateValue ) ;
281+ if ( ! points . length ) throw new Error ( "empty history" ) ;
282+
283+ const currentTotal = totalRecords ( currentManifest ) ;
284+ const latest = points [ points . length - 1 ] ;
285+ if ( latest . total !== currentTotal ) {
286+ points . push ( {
287+ sha : "current" ,
288+ dateValue : Date . now ( ) ,
289+ when : "current" ,
290+ title : "Current published snapshot" ,
291+ url : base + "v1/index.json" ,
292+ rows : countRows ( currentManifest ) ,
293+ total : currentTotal ,
294+ } ) ;
295+ }
296+
297+ for ( let i = 0 ; i < points . length ; i ++ ) {
298+ const prev = points [ i - 1 ] ;
299+ points [ i ] . delta = prev ? points [ i ] . total - prev . total : 0 ;
300+ points [ i ] . changes = largestChanges ( prev ?. rows , points [ i ] . rows ) ;
301+ }
302+ renderHistory ( points ) ;
303+ }
304+
305+ getJSON ( "v1/index.json" ) . then ( ( manifest ) => {
306+ renderSnapshot ( manifest ) ;
307+ return loadCommitHistory ( manifest ) . catch ( ( ) => {
308+ chartEl . innerHTML = '<div class="history-empty">Growth chart unavailable</div>' ;
309+ listEl . innerHTML = '<li><span class="history-dot"></span><span>Current static dump is available; commit history could not be loaded.<small>GitHub API unavailable</small></span></li>' ;
310+ } ) ;
209311 } ) . catch ( ( ) => {
210312 totalEl . textContent = "sync unavailable" ;
211313 countsEl . innerHTML = '<div class="history-count"><span>Static dump</span><b>offline</b></div>' ;
314+ chartEl . innerHTML = '<div class="history-empty">Growth chart unavailable</div>' ;
315+ listEl . innerHTML = '<li><span class="history-dot"></span><span>Current static dump could not be loaded.<small>Build the public data first</small></span></li>' ;
212316 } ) ;
213-
214- fetch ( "https://api.github.com/repos/GetTechAPI/TechAPI/commits?path=site/public/v1&per_page=5" )
215- . then ( ( response ) => {
216- if ( ! response . ok ) throw new Error ( response . statusText ) ;
217- return response . json ( ) ;
218- } )
219- . then ( ( commits ) => {
220- const items = Array . isArray ( commits ) ? commits . slice ( 0 , 4 ) : [ ] ;
221- if ( ! items . length ) throw new Error ( "empty history" ) ;
222- listEl . innerHTML = items . map ( ( item ) => {
223- const message = ( item . commit ?. message || "Dataset sync" ) . split ( "\n" ) [ 0 ] ;
224- const date = item . commit ?. committer ?. date ? new Date ( item . commit . committer . date ) : null ;
225- const when = date ? date . toLocaleDateString ( undefined , { month : "short" , day : "numeric" , year : "numeric" } ) : "recent" ;
226- const sha = String ( item . sha || "" ) . slice ( 0 , 7 ) ;
227- const url = item . html_url || "https://github.com/GetTechAPI/TechAPI" ;
228- return `<li><span class="history-dot"></span><span><a href="${ esc ( url ) } " target="_blank" rel="noopener">${ esc ( message ) } </a><small>${ esc ( when ) } · ${ esc ( sha ) } </small></span></li>` ;
229- } ) . join ( "" ) ;
230- } )
231- . catch ( ( ) => {
232- listEl . innerHTML = '<li><span class="history-dot"></span><span>Current static dump is available; repository history could not be loaded.<small>GitHub API unavailable</small></span></li>' ;
233- } ) ;
234317} ) ( ) ;
235318
236319/* ============================================================
0 commit comments