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,34 +38,36 @@ 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 | ||||||
|                     } |                     } | ||||||
|  |                     let dayFeatures: any[] = undefined | ||||||
|                     try { |                     try { | ||||||
|                          |                         dayFeatures = await this.DownloadStatsForDay(year, month, day, path) | ||||||
|                     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 = [] | ||||||
|  | @ -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 @@ | ||||||
|  | [] | ||||||