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 ScriptUtils from "../../scripts/ScriptUtils"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import {exec} from "child_process" | ||||
| import {GeoOperations} from "../../Logic/GeoOperations"; | ||||
| 
 | ||||
| ScriptUtils.fixUtils() | ||||
| 
 | ||||
|  | @ -40,34 +38,36 @@ class StatsDownloader { | |||
|                     continue; | ||||
|                 } | ||||
| 
 | ||||
|                 const features = [] | ||||
|                 for (let day = 1; day <= 31; day++) { | ||||
|                     if (year === currentYear && month === currentMonth && day === today.getDate()) { | ||||
|                         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)) { | ||||
|                         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 | ||||
|                     } | ||||
|                     let dayFeatures: any[] = undefined | ||||
|                     try { | ||||
|                          | ||||
|                     await this.DownloadStatsForDay(year, month, day, path) | ||||
|                         dayFeatures = await this.DownloadStatsForDay(year, month, day, path) | ||||
|                     } catch (e) { | ||||
|                         console.error(e) | ||||
|                         console.error("Could not download " + year + "-" + month + "-" + day + "... Trying again") | ||||
|                         try{ | ||||
|                             await this.DownloadStatsForDay(year, month, day, path) | ||||
|                         }catch(e){ | ||||
|                             console.error("Could not download "+year+"-"+month+"-"+day+", skipping for now") | ||||
|                         } | ||||
|                         dayFeatures = await this.DownloadStatsForDay(year, month, day, path) | ||||
|                     } | ||||
|                     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 allFeatures = [] | ||||
|  | @ -106,11 +106,11 @@ class StatsDownloader { | |||
|         console.log(`Writing ${allFeatures.length} features to `, path, Utils.Times(_ => " ", 80)) | ||||
|         allFeatures = Utils.NoNull(allFeatures) | ||||
|         allFeatures.forEach(f => { | ||||
|             f.properties = {...f.properties, ...f.properties.metadata} | ||||
|             delete f.properties.metadata | ||||
|             f.properties.id = f.id | ||||
|         }) | ||||
|         writeFileSync(path, JSON.stringify({ | ||||
|             features: allFeatures | ||||
|         }, undefined, 2)) | ||||
|         return allFeatures | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -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> { | ||||
|     if (!existsSync("graphs")) { | ||||
|         mkdirSync("graphs") | ||||
|  | @ -772,7 +162,16 @@ async function main(): Promise<void> { | |||
| 
 | ||||
|     const targetDir = "Docs/Tools/stats" | ||||
|     if (process.argv.indexOf("--no-download") < 0) { | ||||
|         do { | ||||
|             try { | ||||
| 
 | ||||
|                 await new StatsDownloader(targetDir).DownloadStats() | ||||
|                 break | ||||
|             } catch (e) { | ||||
|                 console.log(e) | ||||
|             } | ||||
| 
 | ||||
|         } while (true) | ||||
|     } | ||||
|     const allPaths = readdirSync(targetDir) | ||||
|         .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)); | ||||
|     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") | ||||
| 
 | ||||
|     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")) | ||||
|     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!")) | ||||
|  |  | |||
| 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 @@ | |||
| [] | ||||