New statistcs view
| 
						 | 
					@ -1,78 +0,0 @@
 | 
				
			||||||
import json
 | 
					 | 
				
			||||||
import sys
 | 
					 | 
				
			||||||
from datetime import datetime
 | 
					 | 
				
			||||||
from matplotlib import pyplot
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def pyplot_init():
 | 
					 | 
				
			||||||
    pyplot.close('all')
 | 
					 | 
				
			||||||
    pyplot.figure(figsize=(14, 8), dpi=200)
 | 
					 | 
				
			||||||
    pyplot.xticks(rotation='vertical')
 | 
					 | 
				
			||||||
    pyplot.grid()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def genKeys(data, type):
 | 
					 | 
				
			||||||
    keys = map(lambda kv: kv["key"], data)
 | 
					 | 
				
			||||||
    if type == "date":
 | 
					 | 
				
			||||||
        keys = map(lambda key: datetime.strptime(key, "%Y-%m-%dT%H:%M:%S.000Z"), keys)
 | 
					 | 
				
			||||||
    return list(keys)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def createPie(options):
 | 
					 | 
				
			||||||
    data = options["plot"]["count"]
 | 
					 | 
				
			||||||
    keys = genKeys(data, options["interpetKeysAs"])
 | 
					 | 
				
			||||||
    values = list(map(lambda kv: kv["value"], data))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    total = sum(map(lambda kv: kv["value"], data))
 | 
					 | 
				
			||||||
    first_pct = data[0]["value"] / total
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pyplot_init()
 | 
					 | 
				
			||||||
    pyplot.pie(values, labels=keys, startangle=(90 - 360 * first_pct / 2))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def createBar(options):
 | 
					 | 
				
			||||||
    data = options["plot"]["count"]
 | 
					 | 
				
			||||||
    keys = genKeys(data, options["interpetKeysAs"])
 | 
					 | 
				
			||||||
    values = list(map(lambda kv: kv["value"], data))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    color = None
 | 
					 | 
				
			||||||
    if "color" in options["plot"]:
 | 
					 | 
				
			||||||
    	color = options["plot"]["color"] 
 | 
					 | 
				
			||||||
    pyplot.bar(keys, values, label=options["name"], color=color)
 | 
					 | 
				
			||||||
    pyplot.legend()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def createLine(options):
 | 
					 | 
				
			||||||
    data = options["plot"]["count"]
 | 
					 | 
				
			||||||
    keys = genKeys(data, options["interpetKeysAs"])
 | 
					 | 
				
			||||||
    values = list(map(lambda kv: kv["value"], data))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pyplot.plot(keys, values, label=options["name"])
 | 
					 | 
				
			||||||
    pyplot.legend()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pyplot_init()
 | 
					 | 
				
			||||||
title = sys.argv[1]
 | 
					 | 
				
			||||||
pyplot.title = title
 | 
					 | 
				
			||||||
names = []
 | 
					 | 
				
			||||||
while (True):
 | 
					 | 
				
			||||||
    line = sys.stdin.readline()
 | 
					 | 
				
			||||||
    if line == "" or line == "\n":
 | 
					 | 
				
			||||||
        if (len(names) > 1):
 | 
					 | 
				
			||||||
            pyplot.legend(loc="upper left", ncol=3)
 | 
					 | 
				
			||||||
        pyplot.savefig(title + ".png", dpi=400, facecolor='w', edgecolor='w',
 | 
					 | 
				
			||||||
                       bbox_inches='tight')
 | 
					 | 
				
			||||||
        break
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    options = json.loads(line)
 | 
					 | 
				
			||||||
    print("Creating " + options["plot"]["type"] + " '" + options["name"] + "'")
 | 
					 | 
				
			||||||
    names.append(options["name"])
 | 
					 | 
				
			||||||
    if (options["plot"]["type"] == "pie"):
 | 
					 | 
				
			||||||
        createPie(options)
 | 
					 | 
				
			||||||
    elif (options["plot"]["type"] == "bar"):
 | 
					 | 
				
			||||||
        createBar(options)
 | 
					 | 
				
			||||||
    elif (options["plot"]["type"] == "line"):
 | 
					 | 
				
			||||||
        createLine(options)
 | 
					 | 
				
			||||||
    else:
 | 
					 | 
				
			||||||
        print("Unkown type: " + options.type)
 | 
					 | 
				
			||||||
print("Plot generated")
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,6 @@
 | 
				
			||||||
import {existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync} from "fs";
 | 
					import {existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync} from "fs";
 | 
				
			||||||
import ScriptUtils from "../../scripts/ScriptUtils";
 | 
					import ScriptUtils from "../../scripts/ScriptUtils";
 | 
				
			||||||
import {Utils} from "../../Utils";
 | 
					import {Utils} from "../../Utils";
 | 
				
			||||||
import {exec} from "child_process"
 | 
					 | 
				
			||||||
import {GeoOperations} from "../../Logic/GeoOperations";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
ScriptUtils.fixUtils()
 | 
					ScriptUtils.fixUtils()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -40,39 +38,41 @@ class StatsDownloader {
 | 
				
			||||||
                    continue;
 | 
					                    continue;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                const features = []
 | 
				
			||||||
                for (let day = 1; day <= 31; day++) {
 | 
					                for (let day = 1; day <= 31; day++) {
 | 
				
			||||||
                    if (year === currentYear && month === currentMonth && day === today.getDate() ) {
 | 
					                    if (year === currentYear && month === currentMonth && day === today.getDate()) {
 | 
				
			||||||
                        break;
 | 
					                        break;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    const path = `${this._targetDirectory}/stats.${year}-${month}-${(day < 10 ? "0" : "") + day}.json`
 | 
					                    const path = `${this._targetDirectory}/stats.${year}-${month}-${(day < 10 ? "0" : "") + day}.day.json`
 | 
				
			||||||
                    if(existsSync(path)){
 | 
					                    if (existsSync(path)) {
 | 
				
			||||||
                        console.log("Skipping ", path,": already exists")
 | 
					                        features.push(...JSON.parse(readFileSync(path, "UTF-8")))
 | 
				
			||||||
 | 
					                        console.log("Loaded ", path, "from disk, got", features.length, "features now")
 | 
				
			||||||
                        continue
 | 
					                        continue
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    try{
 | 
					                    let dayFeatures: any[] = undefined
 | 
				
			||||||
                        
 | 
					                    try {
 | 
				
			||||||
                    await this.DownloadStatsForDay(year, month, day, path)
 | 
					                        dayFeatures = await this.DownloadStatsForDay(year, month, day, path)
 | 
				
			||||||
                    }catch(e){
 | 
					                    } catch (e) {
 | 
				
			||||||
                        console.error(e)
 | 
					                        console.error(e)
 | 
				
			||||||
                        console.error("Could not download "+year+"-"+month+"-"+day+"... Trying again")
 | 
					                        console.error("Could not download " + year + "-" + month + "-" + day + "... Trying again")
 | 
				
			||||||
                        try{
 | 
					                        dayFeatures = await this.DownloadStatsForDay(year, month, day, path)
 | 
				
			||||||
                            await this.DownloadStatsForDay(year, month, day, path)
 | 
					 | 
				
			||||||
                        }catch(e){
 | 
					 | 
				
			||||||
                            console.error("Could not download "+year+"-"+month+"-"+day+", skipping for now")
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					                    writeFileSync(path, JSON.stringify(dayFeatures))
 | 
				
			||||||
 | 
					                    features.push(...dayFeatures)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					                writeFileSync("stats." + year + "-" + month + ".json", JSON.stringify({features}))
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public async DownloadStatsForDay(year: number, month: number, day: number, path: string) {
 | 
					    public async DownloadStatsForDay(year: number, month: number, day: number, path: string): Promise<any[]> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let page = 1;
 | 
					        let page = 1;
 | 
				
			||||||
        let allFeatures = []
 | 
					        let allFeatures = []
 | 
				
			||||||
        let endDay = new Date(year,month - 1 /* Zero-indexed: 0 = january*/,day + 1);
 | 
					        let endDay = new Date(year, month - 1 /* Zero-indexed: 0 = january*/, day + 1);
 | 
				
			||||||
        let endDate = `${endDay.getFullYear()}-${Utils.TwoDigits(endDay.getMonth()+1)}-${Utils.TwoDigits(endDay.getDate())}`
 | 
					        let endDate = `${endDay.getFullYear()}-${Utils.TwoDigits(endDay.getMonth() + 1)}-${Utils.TwoDigits(endDay.getDate())}`
 | 
				
			||||||
        let url = this.urlTemplate.replace("{start_date}", year + "-" + Utils.TwoDigits(month) + "-" + Utils.TwoDigits(day))
 | 
					        let url = this.urlTemplate.replace("{start_date}", year + "-" + Utils.TwoDigits(month) + "-" + Utils.TwoDigits(day))
 | 
				
			||||||
            .replace("{end_date}", endDate)
 | 
					            .replace("{end_date}", endDate)
 | 
				
			||||||
            .replace("{page}", "" + page)
 | 
					            .replace("{page}", "" + page)
 | 
				
			||||||
| 
						 | 
					@ -106,11 +106,11 @@ class StatsDownloader {
 | 
				
			||||||
        console.log(`Writing ${allFeatures.length} features to `, path, Utils.Times(_ => " ", 80))
 | 
					        console.log(`Writing ${allFeatures.length} features to `, path, Utils.Times(_ => " ", 80))
 | 
				
			||||||
        allFeatures = Utils.NoNull(allFeatures)
 | 
					        allFeatures = Utils.NoNull(allFeatures)
 | 
				
			||||||
        allFeatures.forEach(f => {
 | 
					        allFeatures.forEach(f => {
 | 
				
			||||||
 | 
					            f.properties = {...f.properties, ...f.properties.metadata}
 | 
				
			||||||
 | 
					            delete f.properties.metadata
 | 
				
			||||||
            f.properties.id = f.id
 | 
					            f.properties.id = f.id
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
        writeFileSync(path, JSON.stringify({
 | 
					        return allFeatures
 | 
				
			||||||
            features: allFeatures
 | 
					 | 
				
			||||||
        }, undefined, 2))
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -155,616 +155,6 @@ interface ChangeSetData {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
interface PlotSpec {
 | 
					 | 
				
			||||||
    name: string,
 | 
					 | 
				
			||||||
    interpetKeysAs: "date" | "number" | "string" | string
 | 
					 | 
				
			||||||
    plot: {
 | 
					 | 
				
			||||||
        type: "pie" | "bar" | "line"
 | 
					 | 
				
			||||||
        count: { key: string, value: number }[]
 | 
					 | 
				
			||||||
    } | {
 | 
					 | 
				
			||||||
        type: "stacked-bar"
 | 
					 | 
				
			||||||
        count: {
 | 
					 | 
				
			||||||
            label: string,
 | 
					 | 
				
			||||||
            values: { key: string | Date, value: number }[],
 | 
					 | 
				
			||||||
            color?: string
 | 
					 | 
				
			||||||
        }[]
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    render(): Promise<void>
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function createGraph(
 | 
					 | 
				
			||||||
    title: string,
 | 
					 | 
				
			||||||
    ...options: PlotSpec[]): Promise<void> {
 | 
					 | 
				
			||||||
    console.log("Creating graph", title, "...")
 | 
					 | 
				
			||||||
    const process = exec("python3 Docs/Tools/GenPlot.py \"graphs/" + title + "\"", ((error, stdout, stderr) => {
 | 
					 | 
				
			||||||
        console.log("Python: ", stdout)
 | 
					 | 
				
			||||||
        if (error !== null) {
 | 
					 | 
				
			||||||
            console.error(error)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (stderr !== "") {
 | 
					 | 
				
			||||||
            console.error(stderr)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for (const option of options) {
 | 
					 | 
				
			||||||
        const d = JSON.stringify(option) + "\n"
 | 
					 | 
				
			||||||
        process.stdin._write(d, "utf-8", undefined)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    process.stdin._write("\n", "utf-8", undefined)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return new Promise((resolve) => {
 | 
					 | 
				
			||||||
        process.on("exit", () => resolve())
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Histogram<K> {
 | 
					 | 
				
			||||||
    public counts: Map<K, number> = new Map<K, number>()
 | 
					 | 
				
			||||||
    private sortAtEnd: K[] = []
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    constructor(keys?: K[]) {
 | 
					 | 
				
			||||||
        const self = this
 | 
					 | 
				
			||||||
        keys?.forEach(key => self.bump(key))
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    total(): number {
 | 
					 | 
				
			||||||
        let total = 0
 | 
					 | 
				
			||||||
        Array.from(this.counts.values()).forEach(i => total = total + i)
 | 
					 | 
				
			||||||
        return total
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public bump(key: K, increase = 1) {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (this.counts.has(key)) {
 | 
					 | 
				
			||||||
            this.counts.set(key, increase + this.counts.get(key))
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            this.counts.set(key, increase)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Adds all the values of the given histogram to this histogram
 | 
					 | 
				
			||||||
     * @param hist
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public bumpHist(hist: Histogram<K>) {
 | 
					 | 
				
			||||||
        const self = this
 | 
					 | 
				
			||||||
        hist.counts.forEach((value, key) => {
 | 
					 | 
				
			||||||
            self.bump(key, value)
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Creates a new histogram. All entries with less then 'cutoff' count are lumped together into the 'other' category
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public createOthersCategory(otherName: K, cutoff: number | ((key: K, value: number) => boolean) = 15): Histogram<K> {
 | 
					 | 
				
			||||||
        const hist = new Histogram<K>()
 | 
					 | 
				
			||||||
        hist.sortAtEnd.push(otherName)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (typeof cutoff === "number") {
 | 
					 | 
				
			||||||
            this.counts.forEach((value, key) => {
 | 
					 | 
				
			||||||
                if (value <= cutoff) {
 | 
					 | 
				
			||||||
                    hist.bump(otherName, value)
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    hist.bump(key, value)
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            this.counts.forEach((value, key) => {
 | 
					 | 
				
			||||||
                if (cutoff(key, value)) {
 | 
					 | 
				
			||||||
                    hist.bump(otherName, value)
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    hist.bump(key, value)
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return hist;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public addCountToName(): Histogram<string> {
 | 
					 | 
				
			||||||
        const self = this;
 | 
					 | 
				
			||||||
        const hist = new Histogram<string>()
 | 
					 | 
				
			||||||
        hist.sortAtEnd = this.sortAtEnd.map(name => `${name} (${self.counts.get(name)})`)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.counts.forEach((value, key) => {
 | 
					 | 
				
			||||||
            hist.bump(`${key} (${value})`, value)
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return hist;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public Clone(): Histogram<K> {
 | 
					 | 
				
			||||||
        const hist = new Histogram<K>()
 | 
					 | 
				
			||||||
        hist.bumpHist(this)
 | 
					 | 
				
			||||||
        hist.sortAtEnd = [...this.sortAtEnd];
 | 
					 | 
				
			||||||
        return hist;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public keyToDate(addMissingDays: boolean = false): Histogram<Date> {
 | 
					 | 
				
			||||||
        const hist = new Histogram<Date>()
 | 
					 | 
				
			||||||
        hist.sortAtEnd = this.sortAtEnd.map(name => new Date("" + name))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let earliest = undefined;
 | 
					 | 
				
			||||||
        let latest = undefined;
 | 
					 | 
				
			||||||
        this.counts.forEach((value, key) => {
 | 
					 | 
				
			||||||
            const d = new Date("" + key);
 | 
					 | 
				
			||||||
            if (earliest === undefined) {
 | 
					 | 
				
			||||||
                earliest = d
 | 
					 | 
				
			||||||
            } else if (d < earliest) {
 | 
					 | 
				
			||||||
                earliest = d
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            if (latest === undefined) {
 | 
					 | 
				
			||||||
                latest = d
 | 
					 | 
				
			||||||
            } else if (d > latest) {
 | 
					 | 
				
			||||||
                latest = d
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            hist.bump(d, value)
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (addMissingDays) {
 | 
					 | 
				
			||||||
            while (earliest < latest) {
 | 
					 | 
				
			||||||
                earliest.setDate(earliest.getDate() + 1)
 | 
					 | 
				
			||||||
                hist.bump(earliest, 0)
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return hist
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public asRunningAverages(convertToRange: ((key: K) => K[])) {
 | 
					 | 
				
			||||||
        const newCount = new Histogram<K>()
 | 
					 | 
				
			||||||
        const self = this
 | 
					 | 
				
			||||||
        this.counts.forEach((_, key) => {
 | 
					 | 
				
			||||||
            const keysToCheck = convertToRange(key)
 | 
					 | 
				
			||||||
            let sum = 0
 | 
					 | 
				
			||||||
            for (const k of keysToCheck) {
 | 
					 | 
				
			||||||
                sum += self.counts.get(k) ?? 0
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            newCount.bump(key, sum / keysToCheck.length)
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        return newCount
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Given a histogram:
 | 
					 | 
				
			||||||
     * 'a': 3
 | 
					 | 
				
			||||||
     * 'b': 5
 | 
					 | 
				
			||||||
     * 'c': 3
 | 
					 | 
				
			||||||
     * 'd': 1
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * This will create a new histogram, which counts how much every count occurs, thus:
 | 
					 | 
				
			||||||
     * 5: 1  // as only 'b' had 5 counts
 | 
					 | 
				
			||||||
     * 3: 2  // as both 'a' and 'c' had 3 counts
 | 
					 | 
				
			||||||
     * 1: 1 // as only 'd' has 1 count
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public binPerCount(): Histogram<number> {
 | 
					 | 
				
			||||||
        const hist = new Histogram<number>()
 | 
					 | 
				
			||||||
        this.counts.forEach((value) => {
 | 
					 | 
				
			||||||
            hist.bump(value)
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        return hist;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public stringifyName(): Histogram<string> {
 | 
					 | 
				
			||||||
        const hist = new Histogram<string>()
 | 
					 | 
				
			||||||
        this.counts.forEach((value, key) => {
 | 
					 | 
				
			||||||
            hist.bump("" + key, value)
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        return hist;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public asPie(options: {
 | 
					 | 
				
			||||||
        name: string
 | 
					 | 
				
			||||||
        compare?: (a: K, b: K) => number
 | 
					 | 
				
			||||||
    }): PlotSpec {
 | 
					 | 
				
			||||||
        const self = this
 | 
					 | 
				
			||||||
        const entriesArray = Array.from(this.counts.entries())
 | 
					 | 
				
			||||||
        let type: string = (typeof entriesArray[0][0])
 | 
					 | 
				
			||||||
        if (entriesArray[0][0] instanceof Date) {
 | 
					 | 
				
			||||||
            type = "date"
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        const entries = entriesArray.map(kv => {
 | 
					 | 
				
			||||||
            return ({key: kv[0], value: kv[1]});
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (options.compare) {
 | 
					 | 
				
			||||||
            entries.sort((a, b) => options.compare(a.key, b.key))
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            entries.sort((a, b) => b.value - a.value)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        entries.sort((a, b) => self.sortAtEnd.indexOf(a.key) - self.sortAtEnd.indexOf(b.key))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const graph: PlotSpec = {
 | 
					 | 
				
			||||||
            name: options.name,
 | 
					 | 
				
			||||||
            interpetKeysAs: type,
 | 
					 | 
				
			||||||
            plot: {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                type: "pie",
 | 
					 | 
				
			||||||
                count: entries.map(kv => {
 | 
					 | 
				
			||||||
                    if (kv.key instanceof Date) {
 | 
					 | 
				
			||||||
                        return ({key: kv.key.toISOString(), value: kv.value})
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    return ({key: kv.key + "", value: kv.value});
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            render: undefined
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        graph.render = async () => await createGraph(graph.name, graph)
 | 
					 | 
				
			||||||
        return graph;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public asBar(options: {
 | 
					 | 
				
			||||||
        name: string
 | 
					 | 
				
			||||||
        compare?: (a: K, b: K) => number,
 | 
					 | 
				
			||||||
        color?: string
 | 
					 | 
				
			||||||
    }): PlotSpec {
 | 
					 | 
				
			||||||
        const spec = this.asPie(options)
 | 
					 | 
				
			||||||
        spec.plot.type = "bar"
 | 
					 | 
				
			||||||
        spec.plot["color"] = options.color
 | 
					 | 
				
			||||||
        return spec;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public asLine(options: {
 | 
					 | 
				
			||||||
        name: string
 | 
					 | 
				
			||||||
        compare?: (a: K, b: K) => number
 | 
					 | 
				
			||||||
    }) {
 | 
					 | 
				
			||||||
        const spec = this.asPie(options)
 | 
					 | 
				
			||||||
        spec.plot.type = "line"
 | 
					 | 
				
			||||||
        return spec
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * A group keeps track of a matrix of changes, e.g.
 | 
					 | 
				
			||||||
 * 'All contributors per day'. This will be stored internally, e.g. as {'2022-03-16' --> ['Pieter Vander Vennet', 'Pieter Vander Vennet', 'Joost Schouppe', 'Pieter Vander Vennet', 'dentonny', ...]}
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
class Group<K, V> {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public groups: Map<K, V[]> = new Map<K, V[]>()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    constructor(features?: any[], fkey?: (feature: any) => K, fvalue?: (feature: any) => V) {
 | 
					 | 
				
			||||||
        const self = this;
 | 
					 | 
				
			||||||
        features?.forEach(f => {
 | 
					 | 
				
			||||||
            self.bump(fkey(f), fvalue(f))
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public static createStackedBarChartPerDay(name: string, features: any, extractV: (feature: any) => string, minNeededTotal = 1): void {
 | 
					 | 
				
			||||||
        const perDay = new Group<string, string>(
 | 
					 | 
				
			||||||
            features,
 | 
					 | 
				
			||||||
            f => f.properties.date.substr(0, 10),
 | 
					 | 
				
			||||||
            extractV
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        createGraph(
 | 
					 | 
				
			||||||
            name,
 | 
					 | 
				
			||||||
            ...Array.from(
 | 
					 | 
				
			||||||
                stackHists<string, string>(
 | 
					 | 
				
			||||||
                    perDay.asGroupedHists()
 | 
					 | 
				
			||||||
                        .filter(tpl => tpl[1].total() > minNeededTotal)
 | 
					 | 
				
			||||||
                        .map(tpl => [`${tpl[0]} (${tpl[1].total()})`, tpl[1]])
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
                .map(
 | 
					 | 
				
			||||||
                    tpl => {
 | 
					 | 
				
			||||||
                        const [name, hist] = tpl
 | 
					 | 
				
			||||||
                        return hist
 | 
					 | 
				
			||||||
                            .keyToDate(true)
 | 
					 | 
				
			||||||
                            .asBar({
 | 
					 | 
				
			||||||
                                name: name
 | 
					 | 
				
			||||||
                            });
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public bump(key: K, value: V) {
 | 
					 | 
				
			||||||
        if (!this.groups.has(key)) {
 | 
					 | 
				
			||||||
            this.groups.set(key, [])
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        this.groups.get(key).push(value)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public asHist(dedup = false): Histogram<K> {
 | 
					 | 
				
			||||||
        const hist = new Histogram<K>()
 | 
					 | 
				
			||||||
        this.groups.forEach((values, key) => {
 | 
					 | 
				
			||||||
            if (dedup) {
 | 
					 | 
				
			||||||
                hist.bump(key, new Set(values).size)
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                hist.bump(key, values.length)
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        return hist
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Given a group, creates a kind of histogram.
 | 
					 | 
				
			||||||
     * E.g: if the Group is {'2022-03-16' --> ['Pieter Vander Vennet', 'Pieter Vander Vennet', 'Seppe Santens']}, the resulting 'groupedHists' will be:
 | 
					 | 
				
			||||||
     * [['Pieter Vander Vennet', {'2022-03-16' --> 2}],['Seppe Santens', {'2022-03-16' --> 1}]]
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    asGroupedHists(): [V, Histogram<K>][] {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const allHists = new Map<V, Histogram<K>>()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const allValues = new Set<V>();
 | 
					 | 
				
			||||||
        Array.from(this.groups.values()).forEach(vs =>
 | 
					 | 
				
			||||||
            vs.forEach(v => {
 | 
					 | 
				
			||||||
                allValues.add(v)
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        allValues.forEach(v => allHists.set(v, new Histogram<K>()))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.groups.forEach((values, key) => {
 | 
					 | 
				
			||||||
            values.forEach(v => {
 | 
					 | 
				
			||||||
                allHists.get(v).bump(key)
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return Array.from(allHists.entries())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * @param hists
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
function stackHists<K, V>(hists: [V, Histogram<K>][]): [V, Histogram<K>][] {
 | 
					 | 
				
			||||||
    const runningTotals = new Histogram<K>()
 | 
					 | 
				
			||||||
    const result: [V, Histogram<K>][] = []
 | 
					 | 
				
			||||||
    hists.forEach(vhist => {
 | 
					 | 
				
			||||||
        const hist = vhist[1]
 | 
					 | 
				
			||||||
        const clone = hist.Clone()
 | 
					 | 
				
			||||||
        clone.bumpHist(runningTotals)
 | 
					 | 
				
			||||||
        runningTotals.bumpHist(hist)
 | 
					 | 
				
			||||||
        result.push([vhist[0], clone])
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
    result.reverse(/* Changes in place, safe copy*/)
 | 
					 | 
				
			||||||
    return result
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Given histograms which should be shown as bars on top of each other, creates a new list of histograms with adjusted heights in order to create a coherent sum
 | 
					 | 
				
			||||||
 * e.g.: for a given day, there are 2 deletions, 3 additions and 5 answers, this will be ordered as 2, 5 and 10 in order to mimic a coherent bar
 | 
					 | 
				
			||||||
 * @param hists
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
function stackHistsSimple<K>(hists: Histogram<K>[]): Histogram<K>[] {
 | 
					 | 
				
			||||||
    const runningTotals = new Histogram<K>()
 | 
					 | 
				
			||||||
    const result: Histogram<K>[] = []
 | 
					 | 
				
			||||||
    for (const hist of hists) {
 | 
					 | 
				
			||||||
        const clone = hist.Clone()
 | 
					 | 
				
			||||||
        clone.bumpHist(runningTotals) // "Copies" one histogram into the other
 | 
					 | 
				
			||||||
        runningTotals.bumpHist(hist)
 | 
					 | 
				
			||||||
        result.push(clone)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    result.reverse(/* Changes in place, safe copy*/)
 | 
					 | 
				
			||||||
    return result
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function createActualChangesGraph(allFeatures: ChangeSetData[], appliedFilterDescription: string) {
 | 
					 | 
				
			||||||
    const metadataOptions = {
 | 
					 | 
				
			||||||
        "answer": "#5b5bdc",
 | 
					 | 
				
			||||||
        "create": "#46ea46",
 | 
					 | 
				
			||||||
        "move": "#ffa600",
 | 
					 | 
				
			||||||
        "deletion": "#ff0000",
 | 
					 | 
				
			||||||
        "soft-delete": "#ff8888",
 | 
					 | 
				
			||||||
        "add-image": "#8888ff",
 | 
					 | 
				
			||||||
        "import": "#00ff00",
 | 
					 | 
				
			||||||
        "conflation": "#ffff00",
 | 
					 | 
				
			||||||
        "split": "#000000",
 | 
					 | 
				
			||||||
        "relation-fix": "#cccccc",
 | 
					 | 
				
			||||||
        "delete-image": "#ff00ff"
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const metadataKeys: string[] = Object.keys(metadataOptions)
 | 
					 | 
				
			||||||
    const histograms: Map<string, Histogram<string>> = new Map<string, Histogram<string>>() // {metakey --> Histogram<date>}
 | 
					 | 
				
			||||||
    allFeatures.forEach(f => {
 | 
					 | 
				
			||||||
        const day = f.properties.date.substr(0, 10)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for (const key of metadataKeys) {
 | 
					 | 
				
			||||||
            const v = f.properties.metadata[key]
 | 
					 | 
				
			||||||
            if (v === undefined) {
 | 
					 | 
				
			||||||
                continue
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            const count = Number(v)
 | 
					 | 
				
			||||||
            if (isNaN(count)) {
 | 
					 | 
				
			||||||
                continue
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            if (!histograms.has(key)) {
 | 
					 | 
				
			||||||
                histograms.set(key, new Histogram<string>())
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            histograms.get(key).bump(day, count)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const entries = stackHists(Array.from(histograms.entries()))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const allGraphs = entries.map(([name, stackedHist]) => {
 | 
					 | 
				
			||||||
            const hist = histograms.get(name)
 | 
					 | 
				
			||||||
            return stackedHist
 | 
					 | 
				
			||||||
                .keyToDate(true)
 | 
					 | 
				
			||||||
                .asBar({name: `${name} (${hist.total()})`, color: metadataOptions[name]});
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    createGraph("Actual changes" + appliedFilterDescription, ...allGraphs)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function createGraphs(allFeatures: ChangeSetData[], appliedFilterDescription: string, cutoff = undefined) {
 | 
					 | 
				
			||||||
    const hist = new Histogram<string>(allFeatures.map(f => f.properties.metadata.theme))
 | 
					 | 
				
			||||||
    await hist
 | 
					 | 
				
			||||||
        .createOthersCategory("other", cutoff ?? 20)
 | 
					 | 
				
			||||||
        .addCountToName()
 | 
					 | 
				
			||||||
        .asBar({name: "Changesets per theme (bar)" + appliedFilterDescription})
 | 
					 | 
				
			||||||
        .render()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    await new Histogram<string>(allFeatures.map(f => f.properties.user))
 | 
					 | 
				
			||||||
        .binPerCount()
 | 
					 | 
				
			||||||
        .stringifyName()
 | 
					 | 
				
			||||||
        .createOthersCategory("25 or more", (key, _) => Number(key) >= (cutoff ?? 25)).asBar(
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                compare: (a, b) => Number(a) - Number(b),
 | 
					 | 
				
			||||||
                name: "Contributors per changeset count" + appliedFilterDescription
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
        .render()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const csPerDay = new Histogram<string>(allFeatures.map(f => f.properties.date.substr(0, 10)))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const perDayLine = csPerDay
 | 
					 | 
				
			||||||
        .keyToDate()
 | 
					 | 
				
			||||||
        .asLine({
 | 
					 | 
				
			||||||
            compare: (a, b) => a.getTime() - b.getTime(),
 | 
					 | 
				
			||||||
            name: "Changesets per day" + appliedFilterDescription
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const perDayAvg = csPerDay.asRunningAverages(key => {
 | 
					 | 
				
			||||||
        const keys = []
 | 
					 | 
				
			||||||
        for (let i = 0; i < 7; i++) {
 | 
					 | 
				
			||||||
            const otherDay = new Date(new Date(key).getTime() - i * 1000 * 60 * 60 * 24)
 | 
					 | 
				
			||||||
            keys.push(otherDay.toISOString().substr(0, 10))
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return keys
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
        .keyToDate(true)
 | 
					 | 
				
			||||||
        .asLine({
 | 
					 | 
				
			||||||
            compare: (a, b) => a.getTime() - b.getTime(),
 | 
					 | 
				
			||||||
            name: "Rolling 7 day average" + appliedFilterDescription
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const perDayAvgMonth = csPerDay.asRunningAverages(key => {
 | 
					 | 
				
			||||||
        const keys = []
 | 
					 | 
				
			||||||
        for (let i = 0; i < 31; i++) {
 | 
					 | 
				
			||||||
            const otherDay = new Date(new Date(key).getTime() - i * 1000 * 60 * 60 * 24)
 | 
					 | 
				
			||||||
            keys.push(otherDay.toISOString().substr(0, 10))
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return keys
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
        .keyToDate()
 | 
					 | 
				
			||||||
        .asLine({
 | 
					 | 
				
			||||||
            compare: (a, b) => a.getTime() - b.getTime(),
 | 
					 | 
				
			||||||
            name: "Rolling 31 day average" + appliedFilterDescription
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    await createGraph("Changesets per day (line)" + appliedFilterDescription, perDayLine, perDayAvg, perDayAvgMonth)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    await new Histogram<string>(allFeatures.map(f => f.properties.metadata.host))
 | 
					 | 
				
			||||||
        .asPie({
 | 
					 | 
				
			||||||
            name: "Changesets per host" + appliedFilterDescription
 | 
					 | 
				
			||||||
        }).render()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    await new Histogram<string>(allFeatures.map(f => f.properties.metadata.theme))
 | 
					 | 
				
			||||||
        .createOthersCategory("< 25 changesets", (cutoff ?? 25))
 | 
					 | 
				
			||||||
        .addCountToName()
 | 
					 | 
				
			||||||
        .asPie({
 | 
					 | 
				
			||||||
            name: "Changesets per theme (pie)" + appliedFilterDescription
 | 
					 | 
				
			||||||
        }).render()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Group.createStackedBarChartPerDay(
 | 
					 | 
				
			||||||
        "Changesets per theme" + appliedFilterDescription,
 | 
					 | 
				
			||||||
        allFeatures,
 | 
					 | 
				
			||||||
        f => f.properties.metadata.theme,
 | 
					 | 
				
			||||||
        cutoff ?? 25
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Group.createStackedBarChartPerDay(
 | 
					 | 
				
			||||||
        "Changesets per version number" + appliedFilterDescription,
 | 
					 | 
				
			||||||
        allFeatures,
 | 
					 | 
				
			||||||
        f => f.properties.editor?.substr("MapComplete ".length, 6)?.replace(/[a-zA-Z-/]/g, '') ?? "UNKNOWN",
 | 
					 | 
				
			||||||
        cutoff ?? 1
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Group.createStackedBarChartPerDay(
 | 
					 | 
				
			||||||
        "Changesets per minor version number" + appliedFilterDescription,
 | 
					 | 
				
			||||||
        allFeatures,
 | 
					 | 
				
			||||||
        f => {
 | 
					 | 
				
			||||||
            const base = f.properties.editor?.substr("MapComplete ".length)?.replace(/[a-zA-Z-/]/g, '') ?? "UNKNOWN"
 | 
					 | 
				
			||||||
            const [major, minor, patch] = base.split(".")
 | 
					 | 
				
			||||||
            return major + "." + minor
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        cutoff ?? 1
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Group.createStackedBarChartPerDay(
 | 
					 | 
				
			||||||
        "Deletion-changesets per theme" + appliedFilterDescription,
 | 
					 | 
				
			||||||
        allFeatures.filter(f => f.properties.delete > 0),
 | 
					 | 
				
			||||||
        f => f.properties.metadata.theme,
 | 
					 | 
				
			||||||
        cutoff ?? 1
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        // Contributors (unique + unique new) per day
 | 
					 | 
				
			||||||
        const contributorCountPerDay = new Group<string, string>()
 | 
					 | 
				
			||||||
        const newContributorsPerDay = new Group<string, string>()
 | 
					 | 
				
			||||||
        const seenContributors = new Set<string>()
 | 
					 | 
				
			||||||
        allFeatures.forEach(f => {
 | 
					 | 
				
			||||||
            const user = f.properties.user
 | 
					 | 
				
			||||||
            const day = f.properties.date.substr(0, 10)
 | 
					 | 
				
			||||||
            contributorCountPerDay.bump(day, user)
 | 
					 | 
				
			||||||
            if (!seenContributors.has(user)) {
 | 
					 | 
				
			||||||
                seenContributors.add(user)
 | 
					 | 
				
			||||||
                newContributorsPerDay.bump(day, user)
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        const total = new Set(allFeatures.map(f => f.properties.user)).size
 | 
					 | 
				
			||||||
        await createGraph(
 | 
					 | 
				
			||||||
            `Contributors per day${appliedFilterDescription}`,
 | 
					 | 
				
			||||||
            contributorCountPerDay
 | 
					 | 
				
			||||||
                .asHist(true)
 | 
					 | 
				
			||||||
                .keyToDate(true)
 | 
					 | 
				
			||||||
                .asBar({
 | 
					 | 
				
			||||||
                    name: `Unique contributors per day (${total} total)`
 | 
					 | 
				
			||||||
                }),
 | 
					 | 
				
			||||||
            newContributorsPerDay
 | 
					 | 
				
			||||||
                .asHist(true)
 | 
					 | 
				
			||||||
                .keyToDate(true)
 | 
					 | 
				
			||||||
                .asBar({
 | 
					 | 
				
			||||||
                    name: "New, unique contributors per day"
 | 
					 | 
				
			||||||
                }),
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        await createActualChangesGraph(allFeatures, appliedFilterDescription);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function createMiscGraphs(allFeatures: ChangeSetData[], emptyCS: ChangeSetData[]) {
 | 
					 | 
				
			||||||
    await new Histogram(emptyCS.map(f => f.properties.date)).keyToDate().asBar({
 | 
					 | 
				
			||||||
        name: "Empty changesets by date"
 | 
					 | 
				
			||||||
    }).render()
 | 
					 | 
				
			||||||
    const geojson = {
 | 
					 | 
				
			||||||
        type: "FeatureCollection",
 | 
					 | 
				
			||||||
        features: Utils.NoNull(allFeatures
 | 
					 | 
				
			||||||
            .map(f => {
 | 
					 | 
				
			||||||
                try {
 | 
					 | 
				
			||||||
                    const point = GeoOperations.centerpoint(f.geometry);
 | 
					 | 
				
			||||||
                    point.properties = {...f.properties, ...f.properties.metadata}
 | 
					 | 
				
			||||||
                    delete point.properties.metadata
 | 
					 | 
				
			||||||
                    for (const key in f.properties.metadata) {
 | 
					 | 
				
			||||||
                        point.properties[key] = f.properties.metadata[key]
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    return point
 | 
					 | 
				
			||||||
                } catch (e) {
 | 
					 | 
				
			||||||
                    console.error("Could not create center point: ", e, f)
 | 
					 | 
				
			||||||
                    return undefined
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }))
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    writeFileSync("centerpoints.geojson", JSON.stringify(geojson, undefined, 2))
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function main(): Promise<void> {
 | 
					async function main(): Promise<void> {
 | 
				
			||||||
    if (!existsSync("graphs")) {
 | 
					    if (!existsSync("graphs")) {
 | 
				
			||||||
        mkdirSync("graphs")
 | 
					        mkdirSync("graphs")
 | 
				
			||||||
| 
						 | 
					@ -772,7 +162,16 @@ async function main(): Promise<void> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const targetDir = "Docs/Tools/stats"
 | 
					    const targetDir = "Docs/Tools/stats"
 | 
				
			||||||
    if (process.argv.indexOf("--no-download") < 0) {
 | 
					    if (process.argv.indexOf("--no-download") < 0) {
 | 
				
			||||||
 | 
					        do {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                await new StatsDownloader(targetDir).DownloadStats()
 | 
					                await new StatsDownloader(targetDir).DownloadStats()
 | 
				
			||||||
 | 
					                break
 | 
				
			||||||
 | 
					            } catch (e) {
 | 
				
			||||||
 | 
					                console.log(e)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        } while (true)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const allPaths = readdirSync(targetDir)
 | 
					    const allPaths = readdirSync(targetDir)
 | 
				
			||||||
        .filter(p => p.startsWith("stats.") && p.endsWith(".json"));
 | 
					        .filter(p => p.startsWith("stats.") && p.endsWith(".json"));
 | 
				
			||||||
| 
						 | 
					@ -780,7 +179,6 @@ async function main(): Promise<void> {
 | 
				
			||||||
        .map(path => JSON.parse(readFileSync("Docs/Tools/stats/" + path, "utf-8")).features));
 | 
					        .map(path => JSON.parse(readFileSync("Docs/Tools/stats/" + path, "utf-8")).features));
 | 
				
			||||||
    allFeatures = allFeatures.filter(f => f.properties.editor === null || f.properties.editor.toLowerCase().startsWith("mapcomplete"))
 | 
					    allFeatures = allFeatures.filter(f => f.properties.editor === null || f.properties.editor.toLowerCase().startsWith("mapcomplete"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const emptyCS = allFeatures.filter(f => f.properties.metadata.theme === "EMPTY CS")
 | 
					 | 
				
			||||||
    allFeatures = allFeatures.filter(f => f.properties.metadata.theme !== "EMPTY CS")
 | 
					    allFeatures = allFeatures.filter(f => f.properties.metadata.theme !== "EMPTY CS")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const noEditor = allFeatures.filter(f => f.properties.editor === null).map(f => "https://www.osm.org/changeset/" + f.id)
 | 
					    const noEditor = allFeatures.filter(f => f.properties.editor === null).map(f => "https://www.osm.org/changeset/" + f.id)
 | 
				
			||||||
| 
						 | 
					@ -792,16 +190,6 @@ async function main(): Promise<void> {
 | 
				
			||||||
    const allFiles = readdirSync("Docs/Tools/stats").filter(p => p.endsWith(".json"))
 | 
					    const allFiles = readdirSync("Docs/Tools/stats").filter(p => p.endsWith(".json"))
 | 
				
			||||||
    writeFileSync("Docs/Tools/stats/file-overview.json", JSON.stringify(allFiles))
 | 
					    writeFileSync("Docs/Tools/stats/file-overview.json", JSON.stringify(allFiles))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
   await createMiscGraphs(allFeatures, emptyCS)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
   const grbOnly = allFeatures.filter(f => f.properties.metadata.theme === "grb")
 | 
					 | 
				
			||||||
   allFeatures = allFeatures.filter(f => f.properties.metadata.theme !== "grb")
 | 
					 | 
				
			||||||
   await createGraphs(allFeatures, "")
 | 
					 | 
				
			||||||
   /*await createGraphs(allFeatures.filter(f => f.properties.date.startsWith("2020")), " in 2020")
 | 
					 | 
				
			||||||
   await createGraphs(allFeatures.filter(f => f.properties.date.startsWith("2021")), " in 2021")
 | 
					 | 
				
			||||||
   await createGraphs(allFeatures.filter(f => f.properties.date.startsWith("2022")), " in 2022")
 | 
					 | 
				
			||||||
   await createGraphs(allFeatures.filter(f => f.properties.metadata.theme === "toerisme_vlaanderen"), " met pin je punt", 0)
 | 
					 | 
				
			||||||
   await createGraphs(grbOnly, " with the GRB import tool", 0)*/
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
main().then(_ => console.log("All done!"))
 | 
					main().then(_ => console.log("All done!"))
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
		 Before Width: | Height: | Size: 82 KiB  | 
| 
		 Before Width: | Height: | Size: 164 KiB  | 
| 
		 Before Width: | Height: | Size: 201 KiB  | 
| 
		 Before Width: | Height: | Size: 167 KiB  | 
| 
		 Before Width: | Height: | Size: 164 KiB  | 
| 
		 Before Width: | Height: | Size: 192 KiB  | 
| 
		 Before Width: | Height: | Size: 319 KiB  | 
| 
		 Before Width: | Height: | Size: 776 KiB  | 
| 
		 Before Width: | Height: | Size: 626 KiB  | 
| 
		 Before Width: | Height: | Size: 549 KiB  | 
| 
		 Before Width: | Height: | Size: 466 KiB  | 
| 
		 Before Width: | Height: | Size: 373 KiB  | 
| 
		 Before Width: | Height: | Size: 79 KiB  | 
| 
		 Before Width: | Height: | Size: 267 KiB  | 
| 
		 Before Width: | Height: | Size: 173 KiB  | 
| 
		 Before Width: | Height: | Size: 135 KiB  | 
| 
		 Before Width: | Height: | Size: 133 KiB  | 
| 
		 Before Width: | Height: | Size: 234 KiB  | 
| 
		 Before Width: | Height: | Size: 110 KiB  | 
| 
		 Before Width: | Height: | Size: 163 KiB  | 
| 
		 Before Width: | Height: | Size: 167 KiB  | 
| 
		 Before Width: | Height: | Size: 144 KiB  | 
| 
		 Before Width: | Height: | Size: 143 KiB  | 
| 
		 Before Width: | Height: | Size: 214 KiB  | 
| 
		 Before Width: | Height: | Size: 229 KiB  | 
| 
		 Before Width: | Height: | Size: 582 KiB  | 
| 
		 Before Width: | Height: | Size: 424 KiB  | 
| 
		 Before Width: | Height: | Size: 133 KiB  | 
| 
		 Before Width: | Height: | Size: 120 KiB  | 
| 
		 Before Width: | Height: | Size: 806 KiB  | 
| 
		 Before Width: | Height: | Size: 250 KiB  | 
| 
		 Before Width: | Height: | Size: 803 KiB  | 
| 
		 Before Width: | Height: | Size: 490 KiB  | 
| 
		 Before Width: | Height: | Size: 90 KiB  | 
| 
		 Before Width: | Height: | Size: 82 KiB  | 
| 
		 Before Width: | Height: | Size: 939 KiB  | 
| 
		 Before Width: | Height: | Size: 173 KiB  | 
| 
		 Before Width: | Height: | Size: 508 KiB  | 
| 
		 Before Width: | Height: | Size: 314 KiB  | 
| 
		 Before Width: | Height: | Size: 104 KiB  | 
| 
		 Before Width: | Height: | Size: 94 KiB  | 
| 
		 Before Width: | Height: | Size: 598 KiB  | 
| 
		 Before Width: | Height: | Size: 177 KiB  | 
| 
		 Before Width: | Height: | Size: 537 KiB  | 
| 
		 Before Width: | Height: | Size: 284 KiB  | 
| 
		 Before Width: | Height: | Size: 230 KiB  | 
| 
		 Before Width: | Height: | Size: 298 KiB  | 
| 
		 Before Width: | Height: | Size: 791 KiB  | 
| 
		 Before Width: | Height: | Size: 141 KiB  | 
| 
		 Before Width: | Height: | Size: 144 KiB  | 
| 
		 Before Width: | Height: | Size: 136 KiB  | 
| 
		 Before Width: | Height: | Size: 112 KiB  | 
| 
		 Before Width: | Height: | Size: 108 KiB  | 
| 
		 Before Width: | Height: | Size: 142 KiB  | 
| 
		 Before Width: | Height: | Size: 125 KiB  | 
| 
		 Before Width: | Height: | Size: 140 KiB  | 
| 
		 Before Width: | Height: | Size: 130 KiB  | 
| 
		 Before Width: | Height: | Size: 121 KiB  | 
| 
		 Before Width: | Height: | Size: 133 KiB  | 
| 
		 Before Width: | Height: | Size: 128 KiB  | 
| 
		 Before Width: | Height: | Size: 82 KiB  | 
| 
		 Before Width: | Height: | Size: 131 KiB  | 
| 
		 Before Width: | Height: | Size: 234 KiB  | 
| 
		 Before Width: | Height: | Size: 117 KiB  | 
| 
		 Before Width: | Height: | Size: 98 KiB  | 
| 
		 Before Width: | Height: | Size: 296 KiB  | 
| 
		 Before Width: | Height: | Size: 98 KiB  | 
| 
						 | 
					@ -1,12 +0,0 @@
 | 
				
			||||||
[
 | 
					 | 
				
			||||||
  "https://www.osm.org/changeset/117826283",
 | 
					 | 
				
			||||||
  "https://www.osm.org/changeset/117742017",
 | 
					 | 
				
			||||||
  "https://www.osm.org/changeset/117522028",
 | 
					 | 
				
			||||||
  "https://www.osm.org/changeset/117244083",
 | 
					 | 
				
			||||||
  "https://www.osm.org/changeset/117010979",
 | 
					 | 
				
			||||||
  "https://www.osm.org/changeset/118422299",
 | 
					 | 
				
			||||||
  "https://www.osm.org/changeset/118344062",
 | 
					 | 
				
			||||||
  "https://www.osm.org/changeset/118281107",
 | 
					 | 
				
			||||||
  "https://www.osm.org/changeset/118228793",
 | 
					 | 
				
			||||||
  "https://www.osm.org/changeset/118092916"
 | 
					 | 
				
			||||||
]
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1 +0,0 @@
 | 
				
			||||||
["stats.2020-10.json","stats.2020-11.json","stats.2020-12.json","stats.2020-5.json","stats.2020-6.json","stats.2020-7.json","stats.2020-8.json","stats.2020-9.json","stats.2021-1.json","stats.2021-10.json","stats.2021-11.json","stats.2021-12.json","stats.2021-2.json","stats.2021-3.json","stats.2021-4.json","stats.2021-5.json","stats.2021-6.json","stats.2021-7.json","stats.2021-8.json","stats.2021-9.json","stats.2022-1.json","stats.2022-2.json","stats.2022-3.json","stats.2022-4.json","stats.2022-5-01.json","stats.2022-5-02.json","stats.2022-5-03.json","stats.2022-5-04.json","stats.2022-5-05.json","stats.2022-5-06.json","stats.2022-5-07.json","stats.2022-5-08.json","stats.2022-5-09.json","stats.2022-5-10.json","stats.2022-5-11.json","stats.2022-5-12.json","stats.2022-5-13.json","stats.2022-5-14.json","stats.2022-5-15.json","stats.2022-5-16.json","stats.2022-5-17.json","stats.2022-5-18.json","stats.2022-5-19.json","stats.2022-5-20.json","stats.2022-5-21.json","stats.2022-5-22.json","stats.2022-5-23.json","stats.2022-5-24.json","stats.2022-5-25.json","stats.2022-5-26.json","stats.2022-5-27.json","stats.2022-5-28.json","stats.2022-5-29.json","stats.2022-5-30.json","stats.2022-5-31.json","stats.2022-6-01.json","stats.2022-6-02.json","stats.2022-6-03.json","stats.2022-6-04.json","stats.2022-6-05.json","stats.2022-6-06.json","stats.2022-6-07.json","stats.2022-6-08.json","stats.2022-6-09.json","stats.2022-6-10.json","stats.2022-6-11.json","stats.2022-6-12.json","stats.2022-6-13.json","stats.2022-6-14.json","stats.2022-6-15.json","stats.2022-6-16.json","stats.2022-6-17.json","stats.2022-6-18.json","stats.2022-6-19.json","stats.2022-6-20.json","stats.2022-6-21.json","stats.2022-6-22.json","stats.2022-6-23.json","stats.2022-6-24.json","stats.2022-6-25.json","stats.2022-6-26.json","stats.2022-6-27.json","stats.2022-6-28.json","stats.2022-6-29.json","stats.2022-6-30.json","stats.2022-7-01.json","stats.2022-7-02.json","stats.2022-7-03.json","stats.2022-7-04.json","stats.2022-7-05.json","stats.2022-7-06.json","stats.2022-7-07.json","stats.2022-7-08.json","stats.2022-7-09.json","stats.2022-7-10.json","stats.2022-7-11.json","stats.2022-7-12.json","stats.2022-7-13.json","stats.2022-7-14.json","stats.2022-7-15.json","stats.2022-7-16.json","stats.2022-7-17.json","stats.2022-7-18.json","stats.2022-7-19.json","stats.2022-7-20.json","stats.2022-7-21.json","stats.2022-7-22.json","stats.2022-7-23.json","stats.2022-7-24.json","stats.2022-7-25.json","stats.2022-7-26.json","stats.2022-7-27.json","stats.2022-7-28.json"]
 | 
					 | 
				
			||||||
							
								
								
									
										1
									
								
								Docs/Tools/stats/stats.2020-5-01.day.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					[]
 | 
				
			||||||
							
								
								
									
										1
									
								
								Docs/Tools/stats/stats.2020-5-02.day.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					[]
 | 
				
			||||||
							
								
								
									
										1
									
								
								Docs/Tools/stats/stats.2020-5-03.day.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					[]
 | 
				
			||||||
							
								
								
									
										1
									
								
								Docs/Tools/stats/stats.2020-5-04.day.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					[]
 | 
				
			||||||
							
								
								
									
										1
									
								
								Docs/Tools/stats/stats.2020-5-05.day.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					[]
 | 
				
			||||||
							
								
								
									
										1
									
								
								Docs/Tools/stats/stats.2020-5-06.day.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					[]
 | 
				
			||||||
							
								
								
									
										1
									
								
								Docs/Tools/stats/stats.2020-5-07.day.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					[]
 | 
				
			||||||
							
								
								
									
										1
									
								
								Docs/Tools/stats/stats.2020-5-08.day.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					[]
 | 
				
			||||||
							
								
								
									
										1
									
								
								Docs/Tools/stats/stats.2020-5-09.day.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					[]
 | 
				
			||||||
							
								
								
									
										1
									
								
								Docs/Tools/stats/stats.2020-5-10.day.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					[]
 | 
				
			||||||
							
								
								
									
										1
									
								
								Docs/Tools/stats/stats.2020-5-11.day.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					[]
 | 
				
			||||||