Refactoring: port "statisticsGUI" to svelte

This commit is contained in:
Pieter Vander Vennet 2024-11-17 02:03:16 +01:00
parent f807f43399
commit dc10a3fe56
8 changed files with 334 additions and 389 deletions

View file

@ -0,0 +1,87 @@
<script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource"
import { Utils } from "../../Utils"
import Loading from "../Base/Loading.svelte"
import type { FeatureCollection } from "geojson"
import type { ChangeSetData } from "./ChangesetsOverview"
import { ChangesetsOverview } from "./ChangesetsOverview"
import ThemeConfig from "../../Models/ThemeConfig/ThemeConfig"
import mcChanges from "../../assets/generated/themes/mapcomplete-changes.json"
import type { ThemeConfigJson } from "../../Models/ThemeConfig/Json/ThemeConfigJson"
import { Accordion, AccordionItem } from "flowbite-svelte"
import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
import Filterview from "../BigComponents/Filterview.svelte"
import FilteredLayer from "../../Models/FilteredLayer"
import type { OsmFeature } from "../../Models/OsmFeature"
import SingleStat from "./SingleStat.svelte"
import { DownloadIcon } from "@rgossiaux/svelte-heroicons/solid"
import { GeoOperations } from "../../Logic/GeoOperations"
export let paths: string[]
let downloaded = 0
const layer = new ThemeConfig(<ThemeConfigJson>mcChanges, true).layers[0]
const filteredLayer = new FilteredLayer(layer)
let allData = <UIEventSource<(ChangeSetData & OsmFeature)[]>>UIEventSource.FromPromise(
Promise.all(paths.map(async p => {
const r = await Utils.downloadJson<FeatureCollection>(p)
downloaded++
return r
}))
).mapD(list => [].concat(...list.map(f => f.features)))
let overview = allData.mapD(data =>
ChangesetsOverview.fromDirtyData(data)
.filter((cs) => filteredLayer.isShown(<any>cs.properties)), [filteredLayer.currentFilter])
const trs = layer.tagRenderings.filter(
(tr) => tr.mappings?.length > 0 || tr.freeform?.key !== undefined
).filter(tr => tr.question !== undefined)
let diffInDays = overview.mapD(overview => {
const dateStrings = Utils.NoNull(
overview._meta.map((cs) => cs.properties.date)
)
const dates: number[] = dateStrings.map((d) => new Date(d).getTime())
const mindate = Math.min(...dates)
const maxdate = Math.max(...dates)
return (maxdate - mindate) / (1000 * 60 * 60 * 24)
})
function offerAsDownload(){
const data = GeoOperations.toCSV($overview._meta, {
ignoreTags:
/^((deletion:node)|(import:node)|(move:node)|(soft-delete:))/,
})
Utils.offerContentsAsDownloadableFile(data, "statistics.csv", {
mimetype: "text/csv",
})
}
</script>
{#if downloaded < paths.length}
<Loading>Loaded {downloaded} out of {paths.length}</Loading>
{:else}
<AccordionSingle>
<span slot="header">Filters</span>
<Filterview {filteredLayer} state={undefined} showLayerTitle={false} />
</AccordionSingle>
<Accordion>
{#each trs as tr}
<AccordionItem paddingDefault="p-0" inactiveClass="text-black">
<span slot="header" class={"w-full p-2 text-base"}>
{tr.question ?? tr.id}
</span>
<SingleStat {tr} overview={$overview} diffInDays={$diffInDays} />
</AccordionItem>
{/each}
</Accordion>
<button on:click={() => offerAsDownload()}>
<DownloadIcon class="w-6 h-6" />
Download as CSV
</button>
{/if}

View file

@ -0,0 +1,139 @@
import { Utils } from "../../Utils"
import { Feature, Polygon } from "geojson"
import { OsmFeature } from "../../Models/OsmFeature"
export interface ChangeSetData extends Feature<Polygon> {
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
host: string
theme: string
imagery: string
language: string
}
}
export 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"
}
public static readonly valuesToSum: ReadonlyArray<string> = [
"create",
"modify",
"delete",
"answer",
"move",
"deletion",
"add-image",
"plantnet-ai-detection",
"import",
"conflation",
"link-image",
"soft-delete",
]
public readonly _meta: (ChangeSetData & OsmFeature)[]
private constructor(meta: (ChangeSetData & OsmFeature)[]) {
this._meta = Utils.NoNull(meta)
}
public static fromDirtyData(meta: (ChangeSetData & OsmFeature)[]): ChangesetsOverview {
return new ChangesetsOverview(meta?.map((cs) => ChangesetsOverview.cleanChangesetData(cs)))
}
private static cleanChangesetData(cs: ChangeSetData & OsmFeature): (ChangeSetData & OsmFeature) {
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
}
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) {
// pass
}
return cs
}
public filter(predicate: (cs: ChangeSetData) => boolean) {
return new ChangesetsOverview(this._meta.filter(predicate))
}
public sum(key: string, excludeThemes: Set<string>): number {
let s = 0
for (const feature of this._meta) {
if (excludeThemes.has(feature.properties.theme)) {
continue
}
const parsed = Number(feature.properties[key])
if (!isNaN(parsed)) {
s += parsed
}
}
return s
}
}

View file

@ -0,0 +1,56 @@
<script lang="ts">
/**
* Shows the statistics for a single item
*/
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
import ToSvelte from "../Base/ToSvelte.svelte"
import TagRenderingChart, { StackedRenderingChart } from "../BigComponents/TagRenderingChart"
import { ChangesetsOverview } from "./ChangesetsOverview"
export let overview: ChangesetsOverview
export let diffInDays: number
export let tr: TagRenderingConfig
let total: number = undefined
if (tr.freeform?.key !== undefined) {
total = new Set(
overview._meta.map((f) => f.properties[tr.freeform.key])
).size
}
</script>
{#if total > 1}
{total} unique values
{/if}
<h3>By number of changesets</h3>
<div class="flex">
<ToSvelte construct={ new TagRenderingChart(overview._meta, tr, {
groupToOtherCutoff:
total > 50 ? 25 : total > 10 ? 3 : 0,
chartstyle: "width: 24rem; height: 24rem",
chartType: "doughnut",
sort: true,
})} />
</div>
<ToSvelte construct={new StackedRenderingChart(tr, overview._meta, {
period: diffInDays <= 367 ? "day" : "month",
groupToOtherCutoff:
total > 50 ? 25 : total > 10 ? 3 : 0,
})} />
<h3>By number of modifications</h3>
<ToSvelte construct={ new StackedRenderingChart( tr, overview._meta,
{
period: diffInDays <= 367 ? "day" : "month",
groupToOtherCutoff: total > 50 ? 10 : 0,
sumFields: ChangesetsOverview. valuesToSum,
}
)} />

View file

@ -0,0 +1,30 @@
<script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource"
import { Utils } from "../../Utils"
import Loading from "../Base/Loading.svelte"
import AllStats from "./AllStats.svelte"
let homeUrl =
"https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/changeset-metadata/"
let stats_files = "file-overview.json"
let indexFile = UIEventSource.FromPromise(
Utils.downloadJson<string[]>(homeUrl + stats_files)
)
</script>
<div class="m-4">
<div class="flex justify-between">
<h2>Statistics of changes made with MapComplete</h2>
<a href="/" class="button">Back to index</a>
</div>
{#if $indexFile === undefined}
<Loading>Loading index file...</Loading>
{:else}
<AllStats paths={$indexFile.filter(p => p.startsWith("stats")).map(p => homeUrl+"/"+p)} />
{/if}
</div>