mapcomplete/UI/StatisticsGUI.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

326 lines
13 KiB
TypeScript
Raw Normal View History

2022-08-02 19:50:17 +02:00
/**
* The statistics-gui shows statistics from previous MapComplete-edits
*/
2022-10-27 01:50:01 +02:00
import { UIEventSource } from "../Logic/UIEventSource"
import { VariableUiElement } from "./Base/VariableUIElement"
2022-08-02 19:50:17 +02:00
import Loading from "./Base/Loading"
2022-10-27 01:50:01 +02:00
import { Utils } from "../Utils"
2022-08-02 19:50:17 +02:00
import Combine from "./Base/Combine"
2022-10-27 01:50:01 +02:00
import { StackedRenderingChart } from "./BigComponents/TagRenderingChart"
import { LayerFilterPanel } from "./BigComponents/FilterView"
import { AllKnownLayouts } from "../Customizations/AllKnownLayouts"
2022-08-22 13:34:47 +02:00
import MapState from "../Logic/State/MapState"
import BaseUIElement from "./BaseUIElement"
import Title from "./Base/Title"
2022-09-02 21:40:13 +02:00
import { FixedUiElement } from "./Base/FixedUiElement"
2022-10-27 01:50:01 +02:00
import List from "./Base/List"
2022-09-08 21:40:48 +02:00
2022-08-22 13:34:47 +02:00
class StatisticsForOverviewFile extends Combine {
constructor(homeUrl: string, paths: string[]) {
2022-10-27 01:50:01 +02:00
paths = paths.filter((p) => !p.endsWith("file-overview.json"))
2022-08-20 12:46:33 +02:00
const layer = AllKnownLayouts.allKnownLayouts.get("mapcomplete-changes").layers[0]
2022-08-22 13:34:47 +02:00
const filteredLayer = MapState.InitializeFilteredLayers(
2022-10-27 01:50:01 +02:00
{ id: "statistics-view", layers: [layer] },
2022-08-22 13:34:47 +02:00
undefined
)[0]
const filterPanel = new LayerFilterPanel(undefined, filteredLayer)
const appliedFilters = filteredLayer.appliedFilters
2022-08-18 23:37:44 +02:00
2022-08-22 13:34:47 +02:00
const downloaded = new UIEventSource<{ features: ChangeSetData[] }[]>([])
2022-08-02 19:50:17 +02:00
2022-08-22 13:34:47 +02:00
for (const filepath of paths) {
2022-10-27 01:50:01 +02:00
if (filepath.endsWith("file-overview.json")) {
2022-09-14 17:58:10 +02:00
continue
}
2022-08-22 13:34:47 +02:00
Utils.downloadJson(homeUrl + filepath).then((data) => {
2022-09-14 17:58:10 +02:00
if (data === undefined) {
2022-09-14 17:58:10 +02:00
return
}
2022-09-14 17:58:10 +02:00
if (data.features === undefined) {
2022-09-14 17:58:10 +02:00
data.features = data
}
2022-08-22 13:34:47 +02:00
data?.features?.forEach((item) => {
2022-10-27 01:50:01 +02:00
item.properties = { ...item.properties, ...item.properties.metadata }
2022-08-22 13:34:47 +02:00
delete item.properties.metadata
2022-08-02 19:50:17 +02:00
})
2022-08-22 13:34:47 +02:00
downloaded.data.push(data)
downloaded.ping()
})
}
2022-08-20 12:46:33 +02:00
2022-08-22 13:34:47 +02:00
const loading = new Loading(
new VariableUiElement(
downloaded.map((dl) => "Downloaded " + dl.length + " items out of " + paths.length)
)
2022-09-08 21:40:48 +02:00
)
2022-08-22 13:34:47 +02:00
super([
filterPanel,
new VariableUiElement(
downloaded.map(
(downloaded) => {
if (downloaded.length !== paths.length) {
return loading
}
let overview = ChangesetsOverview.fromDirtyData(
[].concat(...downloaded.map((d) => d.features))
2022-09-08 21:40:48 +02:00
)
2022-08-22 13:34:47 +02:00
if (appliedFilters.data.size > 0) {
appliedFilters.data.forEach((filterSpec) => {
const tf = filterSpec?.currentFilter
if (tf === undefined) {
2022-09-08 21:40:48 +02:00
return
}
2022-08-22 13:34:47 +02:00
overview = overview.filter((cs) =>
tf.matchesProperties(cs.properties)
2022-09-08 21:40:48 +02:00
)
})
}
2022-09-02 21:40:13 +02:00
if (overview._meta.length === 0) {
2022-08-22 13:34:47 +02:00
return "No data matched the filter"
}
2022-09-08 21:40:48 +02:00
2022-09-02 21:40:13 +02:00
const dateStrings = Utils.NoNull(
overview._meta.map((cs) => cs.properties.date)
2022-09-08 21:40:48 +02:00
)
2022-09-02 21:40:13 +02:00
const dates: number[] = dateStrings.map((d) => new Date(d).getTime())
const mindate = Math.min(...dates)
const maxdate = Math.max(...dates)
2022-09-08 21:40:48 +02:00
2022-09-02 21:40:13 +02:00
const diffInDays = (maxdate - mindate) / (1000 * 60 * 60 * 24)
console.log("Diff in days is ", diffInDays, "got", overview._meta.length)
2022-08-22 13:34:47 +02:00
const trs = layer.tagRenderings.filter(
(tr) => tr.mappings?.length > 0 || tr.freeform?.key !== undefined
2022-09-08 21:40:48 +02:00
)
2022-09-14 17:58:10 +02:00
const allKeys = new Set<string>()
for (const cs of overview._meta) {
for (const propertiesKey in cs.properties) {
allKeys.add(propertiesKey)
}
}
console.log("All keys:", allKeys)
const valuesToSum = [
2022-09-14 19:15:50 +02:00
"create",
"modify",
"delete",
2022-09-14 17:58:10 +02:00
"answer",
"move",
"deletion",
"add-image",
"plantnet-ai-detection",
"import",
"conflation",
"link-image",
2022-10-27 01:50:01 +02:00
"soft-delete",
]
2022-09-14 17:58:10 +02:00
2022-10-27 01:50:01 +02:00
const allThemes = Utils.Dedup(overview._meta.map((f) => f.properties.theme))
2022-09-14 17:58:10 +02:00
2022-09-14 19:15:50 +02:00
const excludedThemes = new Set<string>()
2022-10-27 01:50:01 +02:00
if (allThemes.length > 1) {
excludedThemes.add("grb")
excludedThemes.add("etymology")
}
2022-09-14 19:15:50 +02:00
const summedValues = valuesToSum
2022-10-27 01:50:01 +02:00
.map((key) => [key, overview.sum(key, excludedThemes)])
.filter((kv) => kv[1] != 0)
.map((kv) => kv.join(": "))
2022-09-14 17:58:10 +02:00
const elements: BaseUIElement[] = [
2022-10-27 01:50:01 +02:00
new Title(
allThemes.length === 1
? "General statistics for " + allThemes[0]
: "General statistics (excluding etymology- and GRB-theme changes)"
),
2022-09-14 17:58:10 +02:00
new Combine([
overview._meta.length + " changesets match the filters",
2022-10-27 01:50:01 +02:00
new List(summedValues),
2022-09-14 17:58:10 +02:00
]).SetClass("flex flex-col border rounded-xl"),
2022-10-27 01:50:01 +02:00
new Title("Breakdown"),
2022-09-14 17:58:10 +02:00
]
2022-08-22 13:34:47 +02:00
for (const tr of trs) {
let total = undefined
if (tr.freeform?.key !== undefined) {
total = new Set(
overview._meta.map((f) => f.properties[tr.freeform.key])
).size
2022-08-20 12:46:33 +02:00
}
2022-09-08 21:40:48 +02:00
2022-09-02 21:40:13 +02:00
try {
2022-08-22 13:34:47 +02:00
elements.push(
new Combine([
new Title(tr.question ?? tr.id).SetClass("p-2"),
total > 1 ? total + " unique value" : undefined,
new StackedRenderingChart(tr, <any>overview._meta, {
2022-09-02 21:40:13 +02:00
period: diffInDays <= 367 ? "day" : "month",
2022-08-22 13:34:47 +02:00
groupToOtherCutoff:
total > 50 ? 25 : total > 10 ? 3 : 0,
}).SetStyle("width: 100%; height: 600px"),
]).SetClass("block border-2 border-subtle p-2 m-2 rounded-xl")
)
2022-09-02 21:40:13 +02:00
} catch (e) {
console.log("Could not generate a chart", e)
elements.push(
new FixedUiElement(
"No relevant information for " + tr.question.txt
)
2022-09-08 21:40:48 +02:00
)
2022-09-02 21:40:13 +02:00
}
2022-08-22 13:34:47 +02:00
}
2022-09-08 21:40:48 +02:00
2022-08-22 13:34:47 +02:00
return new Combine(elements)
},
[appliedFilters]
)
).SetClass("block w-full h-full"),
])
this.SetClass("block w-full h-full")
}
}
2022-08-20 12:46:33 +02:00
2022-08-22 13:34:47 +02:00
export default class StatisticsGUI extends VariableUiElement {
2022-08-22 14:33:53 +02:00
private static readonly homeUrl =
2023-01-09 20:38:05 +01:00
"https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/changeset-metadata/"
2022-08-22 13:34:47 +02:00
private static readonly stats_files = "file-overview.json"
2022-08-18 23:37:44 +02:00
2022-08-22 13:34:47 +02:00
constructor() {
const index = UIEventSource.FromPromise(
Utils.downloadJson(StatisticsGUI.homeUrl + StatisticsGUI.stats_files)
2022-09-08 21:40:48 +02:00
)
2022-08-22 13:34:47 +02:00
super(
index.map((paths) => {
if (paths === undefined) {
return new Loading("Loading overview...")
}
2022-08-02 19:50:17 +02:00
2022-08-22 13:34:47 +02:00
return new StatisticsForOverviewFile(StatisticsGUI.homeUrl, paths)
})
2022-09-08 21:40:48 +02:00
)
2022-08-24 16:02:16 +02:00
this.SetClass("block w-full h-full")
2022-08-02 19:50:17 +02:00
}
}
2022-08-18 23:37:44 +02:00
class ChangesetsOverview {
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",
}
2022-08-20 12:46:33 +02:00
public readonly _meta: ChangeSetData[]
2022-08-18 23:37:44 +02:00
2022-08-20 12:46:33 +02:00
public static fromDirtyData(meta: ChangeSetData[]) {
2022-08-22 13:34:47 +02:00
return new ChangesetsOverview(meta?.map((cs) => ChangesetsOverview.cleanChangesetData(cs)))
2022-08-18 23:37:44 +02:00
}
private constructor(meta: ChangeSetData[]) {
2022-08-22 13:34:47 +02:00
this._meta = Utils.NoNull(meta)
2022-08-18 23:37:44 +02:00
}
public filter(predicate: (cs: ChangeSetData) => boolean) {
return new ChangesetsOverview(this._meta.filter(predicate))
}
2022-09-14 19:15:50 +02:00
public sum(key: string, excludeThemes: Set<string>): number {
2022-09-14 17:58:10 +02:00
let s = 0
for (const feature of this._meta) {
2022-10-27 01:50:01 +02:00
if (excludeThemes.has(feature.properties.theme)) {
2022-09-14 19:15:50 +02:00
continue
}
const parsed = Number(feature.properties[key])
2022-09-14 17:58:10 +02:00
if (!isNaN(parsed)) {
s += parsed
}
}
return s
}
2022-08-18 23:37:44 +02:00
private static cleanChangesetData(cs: ChangeSetData): ChangeSetData {
2022-08-22 13:34:47 +02:00
if (cs === undefined) {
return undefined
}
if (cs.properties.editor?.startsWith("iD")) {
// We also fetch based on hashtag, so some edits with iD show up as well
return undefined
}
2022-08-18 23:37:44 +02:00
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
2022-10-27 01:50:01 +02:00
} catch (e) {}
2022-08-18 23:37:44 +02:00
return cs
}
}
2022-08-02 19:50:17 +02:00
interface ChangeSetData {
id: number
type: "Feature"
geometry: {
type: "Polygon"
coordinates: [number, number][][]
}
properties: {
check_user: null
reasons: []
tags: []
features: []
user: string
uid: string
editor: string
comment: string
comments_count: number
source: string
imagery_used: string
date: string
reviewed_features: []
create: number
modify: number
delete: number
area: number
is_suspect: boolean
harmful: any
checked: boolean
check_date: any
2022-08-18 23:37:44 +02:00
host: string
theme: string
imagery: string
language: string
2022-08-02 19:50:17 +02:00
}
}