import tables from "data.json" ; import { murmur3 } from "murmurhash-js" ; export interface CoreMetrics { issues : number ; pulls : number ; stars : number ; soQuestions : number ; } export const defaultWeights = Object . freeze ( { issues : 1 , pulls : 1 , stars : 1 , soQuestions : 0 , } ) ; export type CoreMetricTexts = { [ key in keyof CoreMetrics ] : string ; } ; export interface Metrics extends CoreMetrics { mean : number ; } interface DateMetrics extends Metrics { date : string ; } interface Entry extends DateMetrics { name : string ; } export interface Translation { key : string ; reddit : string ; stackoverflow : string ; wikipedia : string ; } type Keyed < Item > = { [ key ] : Item ; } ; interface Data { colors : { [ name ] : string ; } ; dates : string [ ] ; entries : Keyed < Entry [ ] > ; sums : Keyed < DateMetrics > ; } export type MeanParams = { entries : Keyed < Entry [ ] > ; sums : Keyed < DateMetrics > ; weights : CoreMetrics ; } ; function keepFirst < Item > ( keyed ) { return Object . assign ( { } , ... Object . keys ( keyed ) . map ( ( key ) => { return { [ key ] : keyed [ key ] [ 0 ] } ; } ) ) ; } interface KeyOnArgs < Key extends keyof Item , Item > { key : Key ; items : Item [ ] ; } function keyOn < Key extends keyof Item , Item > ( args ) { let { key , items } = args ; let result = { } as Keyed < Item [ ] > ; for ( let item of items ) { let keyVal = ( item [ key ] as unknown ) as string ; let list = result [ keyVal ] ; if ( ! list ) { result [ keyVal ] = list = [ ] ; } list . push ( item ) ; } return result ; } function fillDates ( { dates , entries } ) { for ( let [ name , points ] of Object . entries ( entries ) ) { if ( points . length !== dates . length ) { let result = [ ] ; let p = 0 ; for ( let date of dates ) { let point = points [ p ] ; if ( ! point || point . date > date ) { point = Object . assign ( { } , ... Object . entries ( points [ 0 ] ) . map ( ( [ key , value ] ) => { if ( key === "date" ) { return { date } ; } else { return { [ key ] : typeof value === "number" ? 0 : value } ; } } ) ) ; point . date = date ; } else { if ( point . date !== date ) { console . warn ( "unrepresented date" ) ; } p += 1 ; } result . push ( point ) ; } entries [ name ] = result ; } } } function normalize ( { entries , sums } ) { let keys = Object . keys ( Object . values ( sums ) [ 0 ] ) . filter ( ( key ) => key !== "date" ) as ( keyof Metrics ) [ ] ; for ( let points of Object . values ( entries ) ) { for ( let point of points ) { let sum = sums [ point . date ] ; for ( let key of keys ) { ( point [ key ] as number ) = ( 100 * ( point [ key ] as number ) ) / ( sum [ key ] as number ) ; } } } } export function parseWeights ( texts ) { return ( Object . fromEntries ( Object . entries ( texts ) . map ( ( [ key , value ] ) => { let parsed = parseFloat ( value ) ; if ( isNaN ( parsed ) ) { parsed = 0 ; } return [ key , parsed ] ; } ) ) as unknown ) as Metrics ; } export function putMean ( { entries , sums , weights } ) { const keys = Object . keys ( weights ) as ( keyof CoreMetrics ) [ ] ; const weightTotal = sumOf ( Object . values ( weights ) ) ; for ( const points of Object . values ( entries ) ) { for ( const point of points ) { const sum = sumOf ( keys . map ( ( key ) => weights [ key ] * point [ key ] ) ) ; point . mean = sum / weightTotal ; } } } export function stringifyWeights ( numbers ) { return ( Object . fromEntries ( Object . entries ( numbers ) . map ( ( [ key , value ] ) => [ key , value . toString ( ) ] ) ) as unknown ) as CoreMetricTexts ; } function sumOf ( nums ) { return nums . reduce ( ( x , y ) => x + y , 0 ) ; } interface Table < Key extends keyof Item , Item > { keys : Key [ ] ; rows : Item [ Key ] [ ] [ ] ; } function tableToItems < Item , Key extends keyof Item > ( table ) { let { keys , rows } = table ; let items = rows . map ( ( row ) => Object . assign ( { } , ... keys . map ( ( key , index ) => { return { [ key ] : row [ index ] } ; } ) ) ) as Item [ ] ; return items ; } export interface Color { hue : number ; saturation : number ; } function chooseColor ( name ) { let hash = murmur3 ( name , 95 ) ; let hue = ( 360 * ( ( hash >> 16 ) & 0xffff ) ) / 0xffff ; let saturation = 100 * ( 0.3 + ( 0.7 * ( hash & 0xffff ) ) / 0xffff ) ; return formatColor ( { hue , saturation } ) ; } function filterDate < Metrics extends DateMetrics > ( items ) { return items . filter ( ( item ) => item . date >= "2012Q1" ) ; } function formatColor ( color ) { return `hsl(${ color . hue }, ${ color . saturation }%, 70%)` ; } let sums = keepFirst ( keyOn ( { key : "date" , items : filterDate ( tableToItems ( tables . sums as any ) as DateMetrics [ ] ) , } ) ) ; let dates = Object . keys ( sums ) . sort ( ) ; let entries = keyOn ( { key : "name" , items : filterDate ( tableToItems ( tables . items as any ) as Entry [ ] ) , } ) ; let colors = Object . assign ( { } , ... Object . keys ( entries ) . map ( ( name ) => { return { [ name ] : chooseColor ( name ) } ; } ) ) as { [ name ] : string ; } ; let data = { colors , dates , entries , sums } ; normalize ( data ) ; putMean ( { entries : data . entries , sums : data . sums , weights : defaultWeights } ) ; fillDates ( data ) ; let translations = keepFirst ( keyOn ( { key : "key" , items : tableToItems ( tables . translations as any ) as Translation [ ] , } ) ) ; export { colors , dates , entries , sums , translations } ;