forked from MapComplete/MapComplete
More work on statistics
This commit is contained in:
parent
56b1337743
commit
5ef9a57bb0
5 changed files with 325 additions and 173 deletions
|
@ -154,57 +154,7 @@ interface ChangeSetData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const theme_remappings = {
|
|
||||||
"metamap": "maps",
|
|
||||||
"groen": "buurtnatuur",
|
|
||||||
"updaten van metadata met mapcomplete": "buurtnatuur",
|
|
||||||
"Toevoegen of dit natuurreservaat toegangkelijk is": "buurtnatuur",
|
|
||||||
"wiki:mapcomplete/fritures": "fritures",
|
|
||||||
"wiki:MapComplete/Fritures": "fritures",
|
|
||||||
"lits": "lit",
|
|
||||||
"pomp": "cyclofix",
|
|
||||||
"wiki:user:joost_schouppe/campersite": "campersite",
|
|
||||||
"wiki-user-joost_schouppe-geveltuintjes": "geveltuintjes",
|
|
||||||
"wiki-user-joost_schouppe-campersite": "campersite",
|
|
||||||
"wiki-User-joost_schouppe-campersite": "campersite",
|
|
||||||
"wiki-User-joost_schouppe-geveltuintjes": "geveltuintjes",
|
|
||||||
"wiki:User:joost_schouppe/campersite": "campersite",
|
|
||||||
"arbres": "arbres_llefia",
|
|
||||||
"aed_brugge": "aed",
|
|
||||||
"https://llefia.org/arbres/mapcomplete.json": "arbres_llefia",
|
|
||||||
"https://llefia.org/arbres/mapcomplete1.json": "arbres_llefia",
|
|
||||||
"toevoegen of dit natuurreservaat toegangkelijk is": "buurtnatuur",
|
|
||||||
"testing mapcomplete 0.0.0": "buurtnatuur",
|
|
||||||
"https://raw.githubusercontent.com/osmbe/play/master/mapcomplete/geveltuinen/geveltuinen.json": "geveltuintjes"
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChangesetDataTools {
|
|
||||||
|
|
||||||
public static cleanChangesetData(cs: ChangeSetData): ChangeSetData {
|
|
||||||
if (cs.properties.metadata.theme === undefined) {
|
|
||||||
cs.properties.metadata.theme = cs.properties.comment.substr(cs.properties.comment.lastIndexOf("#") + 1)
|
|
||||||
}
|
|
||||||
cs.properties.metadata.theme = cs.properties.metadata.theme.toLowerCase()
|
|
||||||
const remapped = theme_remappings[cs.properties.metadata.theme]
|
|
||||||
cs.properties.metadata.theme = remapped ?? cs.properties.metadata.theme
|
|
||||||
if (cs.properties.metadata.theme.startsWith("https://raw.githubusercontent.com/")) {
|
|
||||||
cs.properties.metadata.theme = "gh://" + cs.properties.metadata.theme.substr("https://raw.githubusercontent.com/".length)
|
|
||||||
}
|
|
||||||
if (cs.properties.modify + cs.properties.delete + cs.properties.create == 0) {
|
|
||||||
cs.properties.metadata.theme = "EMPTY CS"
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
cs.properties.metadata.host = new URL(cs.properties.metadata.host).host
|
|
||||||
} catch (e) {
|
|
||||||
|
|
||||||
}
|
|
||||||
if (cs.properties.metadata["answer"] > 100) {
|
|
||||||
console.log("Lots of answers for https://osm.org/changeset/" + cs.id)
|
|
||||||
}
|
|
||||||
return cs
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PlotSpec {
|
interface PlotSpec {
|
||||||
name: string,
|
name: string,
|
||||||
|
@ -827,8 +777,7 @@ async function main(): Promise<void> {
|
||||||
const allPaths = readdirSync(targetDir)
|
const allPaths = readdirSync(targetDir)
|
||||||
.filter(p => p.startsWith("stats.") && p.endsWith(".json"));
|
.filter(p => p.startsWith("stats.") && p.endsWith(".json"));
|
||||||
let allFeatures: ChangeSetData[] = [].concat(...allPaths
|
let allFeatures: ChangeSetData[] = [].concat(...allPaths
|
||||||
.map(path => JSON.parse(readFileSync("Docs/Tools/stats/" + path, "utf-8")).features
|
.map(path => JSON.parse(readFileSync("Docs/Tools/stats/" + path, "utf-8")).features));
|
||||||
.map(cs => ChangesetDataTools.cleanChangesetData(cs))));
|
|
||||||
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")
|
const emptyCS = allFeatures.filter(f => f.properties.metadata.theme === "EMPTY CS")
|
||||||
|
@ -843,18 +792,16 @@ 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)
|
await createMiscGraphs(allFeatures, emptyCS)
|
||||||
|
|
||||||
const grbOnly = allFeatures.filter(f => f.properties.metadata.theme === "grb")
|
const grbOnly = allFeatures.filter(f => f.properties.metadata.theme === "grb")
|
||||||
allFeatures = allFeatures.filter(f => f.properties.metadata.theme !== "grb")
|
allFeatures = allFeatures.filter(f => f.properties.metadata.theme !== "grb")
|
||||||
await createGraphs(allFeatures, "")
|
await createGraphs(allFeatures, "")
|
||||||
await createGraphs(allFeatures.filter(f => f.properties.date.startsWith("2020")), " in 2020")
|
/*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("2021")), " in 2021")
|
||||||
await createGraphs(allFeatures.filter(f => f.properties.date.startsWith("2022")), " in 2022")
|
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(allFeatures.filter(f => f.properties.metadata.theme === "toerisme_vlaanderen"), " met pin je punt", 0)
|
||||||
await createGraphs(grbOnly, " with the GRB import tool", 0)
|
await createGraphs(grbOnly, " with the GRB import tool", 0)*/
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main().then(_ => console.log("All done!"))
|
main().then(_ => console.log("All done!"))
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
import ChartJs from "../Base/ChartJs";
|
import ChartJs from "../Base/ChartJs";
|
||||||
import {OsmFeature} from "../../Models/OsmFeature";
|
|
||||||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
|
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
|
||||||
import {ChartConfiguration} from 'chart.js';
|
import {ChartConfiguration} from 'chart.js';
|
||||||
import Combine from "../Base/Combine";
|
import Combine from "../Base/Combine";
|
||||||
import {TagUtils} from "../../Logic/Tags/TagUtils";
|
import {TagUtils} from "../../Logic/Tags/TagUtils";
|
||||||
|
|
||||||
|
export interface TagRenderingChartOptions {
|
||||||
|
|
||||||
|
groupToOtherCutoff?: 3 | number,
|
||||||
|
sort?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export default class TagRenderingChart extends Combine {
|
export default class TagRenderingChart extends Combine {
|
||||||
|
|
||||||
private static readonly unkownColor = 'rgba(128, 128, 128, 0.2)'
|
private static readonly unkownColor = 'rgba(128, 128, 128, 0.2)'
|
||||||
|
@ -16,7 +21,7 @@ export default class TagRenderingChart extends Combine {
|
||||||
private static readonly notApplicableBorderColor = 'rgba(255, 0, 0)'
|
private static readonly notApplicableBorderColor = 'rgba(255, 0, 0)'
|
||||||
|
|
||||||
|
|
||||||
private static readonly backgroundColors = [
|
public static readonly backgroundColors = [
|
||||||
'rgba(255, 99, 132, 0.2)',
|
'rgba(255, 99, 132, 0.2)',
|
||||||
'rgba(54, 162, 235, 0.2)',
|
'rgba(54, 162, 235, 0.2)',
|
||||||
'rgba(255, 206, 86, 0.2)',
|
'rgba(255, 206, 86, 0.2)',
|
||||||
|
@ -37,98 +42,35 @@ export default class TagRenderingChart extends Combine {
|
||||||
/**
|
/**
|
||||||
* Creates a chart about this tagRendering for the given data
|
* Creates a chart about this tagRendering for the given data
|
||||||
*/
|
*/
|
||||||
constructor(features: OsmFeature[], tagRendering: TagRenderingConfig, options?: {
|
constructor(features: { properties: Record<string, string> }[], tagRendering: TagRenderingConfig, options?: TagRenderingChartOptions & { chartclasses?: string,
|
||||||
chartclasses?: string,
|
|
||||||
chartstyle?: string,
|
chartstyle?: string,
|
||||||
includeTitle?: boolean,
|
includeTitle?: boolean,
|
||||||
groupToOtherCutoff?: 3 | number
|
chartType?: "pie" | "bar" | "doughnut" }) {
|
||||||
}) {
|
if (tagRendering.mappings?.length === 0 && tagRendering.freeform?.key === undefined) {
|
||||||
|
|
||||||
const mappings = tagRendering.mappings ?? []
|
|
||||||
if (mappings.length === 0 && tagRendering.freeform?.key === undefined) {
|
|
||||||
super([])
|
super([])
|
||||||
this.SetClass("hidden")
|
this.SetClass("hidden")
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let unknownCount = 0;
|
|
||||||
const categoryCounts = mappings.map(_ => 0)
|
|
||||||
const otherCounts: Record<string, number> = {}
|
|
||||||
let notApplicable = 0;
|
|
||||||
let barchartMode = tagRendering.multiAnswer;
|
|
||||||
for (const feature of features) {
|
|
||||||
const props = feature.properties
|
|
||||||
if (tagRendering.condition !== undefined && !tagRendering.condition.matchesProperties(props)) {
|
|
||||||
notApplicable++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tagRendering.IsKnown(props)) {
|
const {labels, data} = TagRenderingChart.extractDataAndLabels(tagRendering, features, options)
|
||||||
unknownCount++;
|
if (labels === undefined || data === undefined) {
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let foundMatchingMapping = false;
|
|
||||||
if (!tagRendering.multiAnswer) {
|
|
||||||
for (let i = 0; i < mappings.length; i++) {
|
|
||||||
const mapping = mappings[i];
|
|
||||||
if (mapping.if.matchesProperties(props)) {
|
|
||||||
categoryCounts[i]++
|
|
||||||
foundMatchingMapping = true
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (let i = 0; i < mappings.length; i++) {
|
|
||||||
const mapping = mappings[i];
|
|
||||||
if (TagUtils.MatchesMultiAnswer( mapping.if, props)) {
|
|
||||||
categoryCounts[i]++
|
|
||||||
foundMatchingMapping = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!foundMatchingMapping) {
|
|
||||||
if (tagRendering.freeform?.key !== undefined && props[tagRendering.freeform.key] !== undefined) {
|
|
||||||
const otherValue = props[tagRendering.freeform.key]
|
|
||||||
otherCounts[otherValue] = (otherCounts[otherValue] ?? 0) + 1
|
|
||||||
} else {
|
|
||||||
unknownCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (unknownCount + notApplicable === features.length) {
|
|
||||||
super([])
|
super([])
|
||||||
this.SetClass("hidden")
|
this.SetClass("hidden")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let otherGrouped = 0;
|
|
||||||
const otherLabels: string[] = []
|
|
||||||
const otherData : number[] = []
|
|
||||||
for (const v in otherCounts) {
|
|
||||||
const count = otherCounts[v]
|
|
||||||
if(count >= (options.groupToOtherCutoff ?? 3)){
|
|
||||||
otherLabels.push(v)
|
|
||||||
otherData.push(otherCounts[v])
|
|
||||||
}else{
|
|
||||||
otherGrouped++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const labels = ["Unknown", "Other", "Not applicable", ...mappings?.map(m => m.then.txt) ?? [], ...otherLabels]
|
|
||||||
const data = [unknownCount, otherGrouped, notApplicable, ...categoryCounts, ... otherData]
|
|
||||||
const borderColor = [TagRenderingChart.unkownBorderColor, TagRenderingChart.otherBorderColor, TagRenderingChart.notApplicableBorderColor]
|
const borderColor = [TagRenderingChart.unkownBorderColor, TagRenderingChart.otherBorderColor, TagRenderingChart.notApplicableBorderColor]
|
||||||
const backgroundColor = [TagRenderingChart.unkownColor, TagRenderingChart.otherColor, TagRenderingChart.notApplicableColor]
|
const backgroundColor = [TagRenderingChart.unkownColor, TagRenderingChart.otherColor, TagRenderingChart.notApplicableColor]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
while (borderColor.length < data.length) {
|
while (borderColor.length < data.length) {
|
||||||
borderColor.push(...TagRenderingChart.borderColors)
|
borderColor.push(...TagRenderingChart.borderColors)
|
||||||
backgroundColor.push(...TagRenderingChart.backgroundColors)
|
backgroundColor.push(...TagRenderingChart.backgroundColors)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = data.length; i >= 0; i--) {
|
for (let i = data.length; i >= 0; i--) {
|
||||||
if (data[i] === 0) {
|
if (data[i]?.length === 0) {
|
||||||
labels.splice(i, 1)
|
labels.splice(i, 1)
|
||||||
data.splice(i, 1)
|
data.splice(i, 1)
|
||||||
borderColor.splice(i, 1)
|
borderColor.splice(i, 1)
|
||||||
|
@ -136,16 +78,19 @@ export default class TagRenderingChart extends Combine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(labels.length > 9){
|
|
||||||
|
|
||||||
|
let barchartMode = tagRendering.multiAnswer;
|
||||||
|
if (labels.length > 9) {
|
||||||
barchartMode = true;
|
barchartMode = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = <ChartConfiguration>{
|
const config = <ChartConfiguration>{
|
||||||
type: barchartMode ? 'bar' : 'doughnut',
|
type: options.chartType ?? (barchartMode ? 'bar' : 'doughnut'),
|
||||||
data: {
|
data: {
|
||||||
labels,
|
labels,
|
||||||
datasets: [{
|
datasets: [{
|
||||||
data,
|
data: data.map(l => l.length),
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
borderColor,
|
borderColor,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
|
@ -169,11 +114,91 @@ export default class TagRenderingChart extends Combine {
|
||||||
|
|
||||||
|
|
||||||
super([
|
super([
|
||||||
options?.includeTitle ? (tagRendering.question.Clone() ?? tagRendering.id) : undefined,
|
options?.includeTitle ? (tagRendering.question.Clone() ?? tagRendering.id) : undefined,
|
||||||
chart])
|
chart])
|
||||||
|
|
||||||
this.SetClass("block")
|
this.SetClass("block")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static extractDataAndLabels<T extends {properties: Record<string, string>}>(tagRendering: TagRenderingConfig, features: T[], options?:TagRenderingChartOptions): {labels: string[], data: T[][]} {
|
||||||
|
const mappings = tagRendering.mappings ?? []
|
||||||
|
|
||||||
|
options = options ?? {}
|
||||||
|
let unknownCount : T[] = [];
|
||||||
|
const categoryCounts : T[][]= mappings.map(_ => [])
|
||||||
|
const otherCounts: Record<string, T[]> = {}
|
||||||
|
let notApplicable : T[] = [];
|
||||||
|
for (const feature of features) {
|
||||||
|
const props = feature.properties
|
||||||
|
if (tagRendering.condition !== undefined && !tagRendering.condition.matchesProperties(props)) {
|
||||||
|
notApplicable.push(feature);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tagRendering.IsKnown(props)) {
|
||||||
|
unknownCount.push(feature);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let foundMatchingMapping = false;
|
||||||
|
if (!tagRendering.multiAnswer) {
|
||||||
|
for (let i = 0; i < mappings.length; i++) {
|
||||||
|
const mapping = mappings[i];
|
||||||
|
if (mapping.if.matchesProperties(props)) {
|
||||||
|
categoryCounts[i].push(feature)
|
||||||
|
foundMatchingMapping = true
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < mappings.length; i++) {
|
||||||
|
const mapping = mappings[i];
|
||||||
|
if (TagUtils.MatchesMultiAnswer(mapping.if, props)) {
|
||||||
|
categoryCounts[i].push(feature)
|
||||||
|
foundMatchingMapping = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!foundMatchingMapping) {
|
||||||
|
if (tagRendering.freeform?.key !== undefined && props[tagRendering.freeform.key] !== undefined) {
|
||||||
|
const otherValue = props[tagRendering.freeform.key]
|
||||||
|
otherCounts[otherValue] = (otherCounts[otherValue] ?? [])
|
||||||
|
otherCounts[otherValue] .push(feature)
|
||||||
|
} else {
|
||||||
|
unknownCount.push(feature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unknownCount.length + notApplicable.length === features.length) {
|
||||||
|
console.log("Returning no label nor data: all features are unkown or notApplicable")
|
||||||
|
return {labels: undefined, data: undefined}
|
||||||
|
}
|
||||||
|
|
||||||
|
let otherGrouped : T[] = [];
|
||||||
|
const otherLabels: string[] = []
|
||||||
|
const otherData: T[][] = []
|
||||||
|
const sortedOtherCounts: [string, T[]][] = []
|
||||||
|
for (const v in otherCounts) {
|
||||||
|
sortedOtherCounts.push([v, otherCounts[v]]);
|
||||||
|
}
|
||||||
|
if (options?.sort) {
|
||||||
|
sortedOtherCounts.sort((a, b) => b[1].length - a[1].length)
|
||||||
|
}
|
||||||
|
for (const [v, count] of sortedOtherCounts) {
|
||||||
|
if (count.length >= (options.groupToOtherCutoff ?? 3)) {
|
||||||
|
otherLabels.push(v)
|
||||||
|
otherData.push(otherCounts[v])
|
||||||
|
} else {
|
||||||
|
otherGrouped.push(...count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const labels = ["Unknown", "Other", "Not applicable", ...mappings?.map(m => m.then.txt) ?? [], ...otherLabels]
|
||||||
|
const data : T[][] = [unknownCount, otherGrouped, notApplicable, ...categoryCounts, ...otherData]
|
||||||
|
|
||||||
|
return {labels, data}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -7,61 +7,233 @@ import ChartJs from "./Base/ChartJs";
|
||||||
import Loading from "./Base/Loading";
|
import Loading from "./Base/Loading";
|
||||||
import {Utils} from "../Utils";
|
import {Utils} from "../Utils";
|
||||||
import Combine from "./Base/Combine";
|
import Combine from "./Base/Combine";
|
||||||
|
import BaseUIElement from "./BaseUIElement";
|
||||||
|
import TagRenderingChart from "./BigComponents/TagRenderingChart";
|
||||||
|
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig";
|
||||||
|
import {ChartConfiguration} from "chart.js";
|
||||||
|
import {FixedUiElement} from "./Base/FixedUiElement";
|
||||||
|
|
||||||
export default class StatisticsGUI {
|
export default class StatisticsGUI {
|
||||||
|
|
||||||
public static setup(): void{
|
private static readonly homeUrl = "https://raw.githubusercontent.com/pietervdvn/MapComplete/develop/Docs/Tools/stats/"
|
||||||
|
private static readonly stats_files = "file-overview.json"
|
||||||
|
private readonly index = UIEventSource.FromPromise(Utils.downloadJson(StatisticsGUI.homeUrl + StatisticsGUI.stats_files))
|
||||||
|
|
||||||
|
|
||||||
new VariableUiElement(index.map(paths => {
|
public setup(): void {
|
||||||
|
|
||||||
|
|
||||||
|
new VariableUiElement(this.index.map(paths => {
|
||||||
if (paths === undefined) {
|
if (paths === undefined) {
|
||||||
return new Loading("Loading overview...")
|
return new Loading("Loading overview...")
|
||||||
}
|
}
|
||||||
const downloaded = new UIEventSource<{ features: ChangeSetData[] }[]>([])
|
const downloaded = new UIEventSource<{ features: ChangeSetData[] }[]>([])
|
||||||
|
|
||||||
for (const filepath of paths) {
|
for (const filepath of paths) {
|
||||||
Utils.downloadJson(homeUrl + filepath).then(data => {
|
Utils.downloadJson(StatisticsGUI.homeUrl + filepath).then(data => {
|
||||||
|
data.features.forEach(item => {
|
||||||
|
item.properties = {...item.properties, ...item.properties.metadata}
|
||||||
|
delete item.properties.metadata
|
||||||
|
})
|
||||||
downloaded.data.push(data)
|
downloaded.data.push(data)
|
||||||
downloaded.ping()
|
downloaded.ping()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return new VariableUiElement(downloaded.map(downloaded => {
|
return new Combine([
|
||||||
const themeBreakdown = new Map<string, number>()
|
new VariableUiElement(downloaded.map(dl => "Downloaded " + dl.length + " items")),
|
||||||
for (const feats of downloaded) {
|
new VariableUiElement(downloaded.map(l => [...l]).stabilized(250).map(downloaded => {
|
||||||
console.log("Feats:", feats)
|
const overview = ChangesetsOverview.fromDirtyData([].concat(...downloaded.map(d => d.features)))
|
||||||
for (const feat of feats.features) {
|
.filter(cs => new Date(cs.properties.date) > new Date(2022,6,1))
|
||||||
const key = feat.properties.metadata.theme
|
|
||||||
const count = themeBreakdown.get(key) ?? 0
|
// return overview.breakdownPerDay(overview.themeBreakdown)
|
||||||
themeBreakdown.set(key, count + 1)
|
return overview.breakdownPer(overview.themeBreakdown, "month")
|
||||||
}
|
})).SetClass("block w-full h-full")
|
||||||
}
|
]).SetClass("block w-full h-full")
|
||||||
|
|
||||||
const keys = Array.from(themeBreakdown.keys())
|
|
||||||
const values = keys.map( k => themeBreakdown.get(k))
|
|
||||||
|
|
||||||
console.log(keys, values)
|
|
||||||
return new Combine([
|
|
||||||
"Got " + downloaded.length + " files out of " + paths.length,
|
|
||||||
new ChartJs({
|
|
||||||
type: "pie",
|
|
||||||
data: {
|
|
||||||
datasets: [{data: values}],
|
|
||||||
labels: keys
|
|
||||||
}
|
|
||||||
}).SetClass("w-1/3 h-full")
|
|
||||||
]).SetClass("block w-full h-full")
|
|
||||||
})).SetClass("block w-full h-full")
|
|
||||||
})).SetClass("block w-full h-full").AttachTo("maindiv")
|
})).SetClass("block w-full h-full").AttachTo("maindiv")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const homeUrl = "https://raw.githubusercontent.com/pietervdvn/MapComplete/develop/Docs/Tools/stats/"
|
class ChangesetsOverview {
|
||||||
const stats_files = "file-overview.json"
|
|
||||||
const index = UIEventSource.FromPromise(Utils.downloadJson(homeUrl + stats_files))
|
|
||||||
|
|
||||||
|
private static readonly theme_remappings = {
|
||||||
|
"metamap": "maps",
|
||||||
|
"groen": "buurtnatuur",
|
||||||
|
"updaten van metadata met mapcomplete": "buurtnatuur",
|
||||||
|
"Toevoegen of dit natuurreservaat toegangkelijk is": "buurtnatuur",
|
||||||
|
"wiki:mapcomplete/fritures": "fritures",
|
||||||
|
"wiki:MapComplete/Fritures": "fritures",
|
||||||
|
"lits": "lit",
|
||||||
|
"pomp": "cyclofix",
|
||||||
|
"wiki:user:joost_schouppe/campersite": "campersite",
|
||||||
|
"wiki-user-joost_schouppe-geveltuintjes": "geveltuintjes",
|
||||||
|
"wiki-user-joost_schouppe-campersite": "campersite",
|
||||||
|
"wiki-User-joost_schouppe-campersite": "campersite",
|
||||||
|
"wiki-User-joost_schouppe-geveltuintjes": "geveltuintjes",
|
||||||
|
"wiki:User:joost_schouppe/campersite": "campersite",
|
||||||
|
"arbres": "arbres_llefia",
|
||||||
|
"aed_brugge": "aed",
|
||||||
|
"https://llefia.org/arbres/mapcomplete.json": "arbres_llefia",
|
||||||
|
"https://llefia.org/arbres/mapcomplete1.json": "arbres_llefia",
|
||||||
|
"toevoegen of dit natuurreservaat toegangkelijk is": "buurtnatuur",
|
||||||
|
"testing mapcomplete 0.0.0": "buurtnatuur",
|
||||||
|
"entrances": "indoor",
|
||||||
|
"https://raw.githubusercontent.com/osmbe/play/master/mapcomplete/geveltuinen/geveltuinen.json": "geveltuintjes"
|
||||||
|
}
|
||||||
|
private readonly _meta: ChangeSetData[];
|
||||||
|
|
||||||
|
public static fromDirtyData(meta: ChangeSetData[]){
|
||||||
|
return new ChangesetsOverview(meta.map(cs => ChangesetsOverview.cleanChangesetData(cs)))
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor(meta: ChangeSetData[]) {
|
||||||
|
this._meta = meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
public filter(predicate: (cs: ChangeSetData) => boolean) {
|
||||||
|
return new ChangesetsOverview(this._meta.filter(predicate))
|
||||||
|
}
|
||||||
|
|
||||||
|
private static cleanChangesetData(cs: ChangeSetData): ChangeSetData {
|
||||||
|
if (cs.properties.theme === undefined) {
|
||||||
|
cs.properties.theme = cs.properties.comment.substr(cs.properties.comment.lastIndexOf("#") + 1)
|
||||||
|
}
|
||||||
|
cs.properties.theme = cs.properties.theme.toLowerCase()
|
||||||
|
const remapped = ChangesetsOverview.theme_remappings[cs.properties.theme]
|
||||||
|
cs.properties.theme = remapped ?? cs.properties.theme
|
||||||
|
if (cs.properties.theme.startsWith("https://raw.githubusercontent.com/")) {
|
||||||
|
cs.properties.theme = "gh://" + cs.properties.theme.substr("https://raw.githubusercontent.com/".length)
|
||||||
|
}
|
||||||
|
if (cs.properties.modify + cs.properties.delete + cs.properties.create == 0) {
|
||||||
|
cs.properties.theme = "EMPTY CS"
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
cs.properties.host = new URL(cs.properties.host).host
|
||||||
|
} catch (e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
return cs
|
||||||
|
}
|
||||||
|
|
||||||
|
public themeBreakdown = new TagRenderingConfig({
|
||||||
|
id: "theme-breakdown",
|
||||||
|
question: "What theme was used?",
|
||||||
|
freeform: {
|
||||||
|
key: "theme"
|
||||||
|
},
|
||||||
|
render: "{theme}"
|
||||||
|
}, "statistics.themes")
|
||||||
|
|
||||||
|
public ThemeBreakdown(): BaseUIElement {
|
||||||
|
return new TagRenderingChart(
|
||||||
|
<any>this._meta,
|
||||||
|
this.themeBreakdown,
|
||||||
|
{
|
||||||
|
chartType: "doughnut",
|
||||||
|
sort: true,
|
||||||
|
groupToOtherCutoff: 25
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAllDays(perMonth = false): string[] {
|
||||||
|
let earliest: Date = undefined
|
||||||
|
let latest: Date = undefined;
|
||||||
|
let allDates = new Set<string>();
|
||||||
|
this._meta.forEach((value, key) => {
|
||||||
|
const d = new Date(value.properties.date);
|
||||||
|
Utils.SetMidnight(d)
|
||||||
|
if(perMonth){
|
||||||
|
d.setUTCDate(1)
|
||||||
|
}
|
||||||
|
if (earliest === undefined) {
|
||||||
|
earliest = d
|
||||||
|
} else if (d < earliest) {
|
||||||
|
earliest = d
|
||||||
|
}
|
||||||
|
if (latest === undefined) {
|
||||||
|
latest = d
|
||||||
|
} else if (d > latest) {
|
||||||
|
latest = d
|
||||||
|
}
|
||||||
|
allDates.add(d.toISOString())
|
||||||
|
})
|
||||||
|
|
||||||
|
while (earliest < latest) {
|
||||||
|
earliest.setDate(earliest.getDate() + 1)
|
||||||
|
allDates.add(earliest.toISOString())
|
||||||
|
}
|
||||||
|
const days = Array.from(allDates)
|
||||||
|
days.sort()
|
||||||
|
return days
|
||||||
|
}
|
||||||
|
|
||||||
|
public breakdownPer(tr: TagRenderingConfig, period: "day" | "month" = "day" ): BaseUIElement {
|
||||||
|
const {labels, data} = TagRenderingChart.extractDataAndLabels(tr, <any>this._meta, {
|
||||||
|
sort: true
|
||||||
|
})
|
||||||
|
if (labels === undefined || data === undefined) {
|
||||||
|
return new FixedUiElement("No labels or data given...")
|
||||||
|
}
|
||||||
|
// labels: ["cyclofix", "buurtnatuur", ...]; data : [ ["cyclofix-changeset", "cyclofix-changeset", ...], ["buurtnatuur-cs", "buurtnatuur-cs"], ... ]
|
||||||
|
|
||||||
|
const datasets: { label: string /*themename*/, data: number[]/*counts per day*/, backgroundColor: string }[] = []
|
||||||
|
|
||||||
|
const allDays = this.getAllDays()
|
||||||
|
for (let i = 0; i < labels.length; i++) {
|
||||||
|
const label = labels[i];
|
||||||
|
const changesetsForTheme = <ChangeSetData[]><any>data[i]
|
||||||
|
const perDay: ChangeSetData[][] = []
|
||||||
|
for (const day of allDays) {
|
||||||
|
const today: ChangeSetData[] = []
|
||||||
|
for (const changeset of changesetsForTheme) {
|
||||||
|
const csDate = new Date(changeset.properties.date)
|
||||||
|
Utils.SetMidnight(csDate)
|
||||||
|
if(period === "month"){
|
||||||
|
csDate.setUTCDate(1)
|
||||||
|
}
|
||||||
|
if (csDate.toISOString() !== day) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
today.push(changeset)
|
||||||
|
}
|
||||||
|
perDay.push(today)
|
||||||
|
}
|
||||||
|
datasets.push({
|
||||||
|
data: perDay.map(cs => cs.length),
|
||||||
|
backgroundColor: TagRenderingChart.backgroundColors[i % TagRenderingChart.backgroundColors.length],
|
||||||
|
label
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const perDayData = {
|
||||||
|
labels: allDays.map(d => d.substr(0, d.indexOf("T"))),
|
||||||
|
datasets
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = <ChartConfiguration>{
|
||||||
|
type: 'bar',
|
||||||
|
data: perDayData,
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
stacked: true,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
stacked: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ChartJs(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
interface ChangeSetData {
|
interface ChangeSetData {
|
||||||
"id": number,
|
"id": number,
|
||||||
|
@ -92,11 +264,9 @@ interface ChangeSetData {
|
||||||
"harmful": any,
|
"harmful": any,
|
||||||
"checked": boolean,
|
"checked": boolean,
|
||||||
"check_date": any,
|
"check_date": any,
|
||||||
"metadata": {
|
"host": string,
|
||||||
"host": string,
|
"theme": string,
|
||||||
"theme": string,
|
"imagery": string,
|
||||||
"imagery": string,
|
"language": string
|
||||||
"language": string
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
7
Utils.ts
7
Utils.ts
|
@ -1039,5 +1039,12 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static SetMidnight(d : Date): void{
|
||||||
|
d.setUTCHours(0)
|
||||||
|
d.setUTCSeconds(0)
|
||||||
|
d.setUTCMilliseconds(0)
|
||||||
|
d.setUTCMinutes(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
3
test.ts
3
test.ts
|
@ -0,0 +1,3 @@
|
||||||
|
import StatisticsGUI from "./UI/StatisticsGUI";
|
||||||
|
|
||||||
|
new StatisticsGUI().setup()
|
Loading…
Reference in a new issue