Misc: improve statistics page

This commit is contained in:
Pieter Vander Vennet 2025-02-08 01:27:33 +01:00
parent d5ccd42cfd
commit 9021c372ad
4 changed files with 75 additions and 43 deletions

View file

@ -105,8 +105,8 @@ class StatsDownloader {
): Promise<ChangeSetData[]> {
let page = 1
let allFeatures: ChangeSetData[] = []
let endDay = new Date(year, month - 1 /* Zero-indexed: 0 = january*/, day + 1)
let endDate = `${endDay.getFullYear()}-${Utils.TwoDigits(
const endDay = new Date(year, month - 1 /* Zero-indexed: 0 = january*/, day + 1)
const endDate = `${endDay.getFullYear()}-${Utils.TwoDigits(
endDay.getMonth() + 1
)}-${Utils.TwoDigits(endDay.getDate())}`
let url = this.urlTemplate
@ -117,7 +117,7 @@ class StatsDownloader {
.replace("{end_date}", endDate)
.replace("{page}", "" + page)
let headers = {
const headers = {
"User-Agent":
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0",
"Accept-Language": "en-US,en;q=0.5",
@ -148,6 +148,9 @@ class StatsDownloader {
allFeatures = Utils.NoNull(allFeatures)
allFeatures.forEach((f) => {
f.properties = { ...f.properties, ...f.properties.metadata }
if (f.properties.editor.toLowerCase().indexOf("android") >= 0) {
f.properties["android"] = "yes"
}
delete f.properties.metadata
f.properties["id"] = f.id
})
@ -212,8 +215,8 @@ class GenerateSeries extends Script {
}
private async downloadStatistics(targetDir: string) {
let year = 2020
let month = 5
let year = 2025
let month = 1
let day = 1
if (!isNaN(Number(process.argv[2]))) {
year = Number(process.argv[2])

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { Utils } from "../../Utils"
import Loading from "../Base/Loading.svelte"
import type { FeatureCollection } from "geojson"
@ -17,6 +17,7 @@
import SingleStat from "./SingleStat.svelte"
import { DownloadIcon } from "@rgossiaux/svelte-heroicons/solid"
import { GeoOperations } from "../../Logic/GeoOperations"
import Filter from "../../assets/svg/Filter.svelte"
export let paths: string[]
@ -24,23 +25,31 @@
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)))
const downloadData: () => Promise<(ChangeSetData & OsmFeature)[]> = async () => {
const results = []
for (const p of paths) {
const r = await Utils.downloadJson<FeatureCollection>(p)
console.log("Downloaded", p)
downloaded++
if (Array.isArray(r)) {
results.push(...r)
} else {
results.push(...r.features ?? [])
}
}
return results
}
let overview = allData.mapD(
let allData = <UIEventSource<(ChangeSetData & OsmFeature)[]>>UIEventSource.FromPromise(downloadData())
let overview: Store<ChangesetsOverview | undefined> = allData.mapD(
(data) =>
ChangesetsOverview.fromDirtyData(data).filter((cs) =>
filteredLayer.isShown(<any>cs.properties)
),
[filteredLayer.currentFilter]
)
overview.addCallbackAndRunD(d => console.log(d))
const trs = layer.tagRenderings
.filter((tr) => tr.mappings?.length > 0 || tr.freeform?.key !== undefined)
@ -56,10 +65,10 @@
function offerAsDownload() {
const data = GeoOperations.toCSV($overview._meta, {
ignoreTags: /^((deletion:node)|(import:node)|(move:node)|(soft-delete:))/,
ignoreTags: /^((deletion:node)|(import:node)|(move:node)|(soft-delete:))/
})
Utils.offerContentsAsDownloadableFile(data, "statistics.csv", {
mimetype: "text/csv",
mimetype: "text/csv"
})
}
</script>
@ -68,21 +77,28 @@
<Loading>Loaded {downloaded} out of {paths.length}</Loading>
{:else}
<AccordionSingle>
<span slot="header">Filters</span>
<div slot="header" class="flex items-center">
<Filter class="h-6 w-6 pr-2" />
Filters
</div>
<Filterview {filteredLayer} state={undefined} showLayerTitle={false} />
</AccordionSingle>
<Accordion>
{#each trs as tr}
<AccordionItem paddingDefault="p-0" inactiveClass="text-black">
{#if !$overview || $overview._meta.length === 0}
<div class="alert">Filter matches no items</div>
{:else}
<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="h-6 w-6" />
Download as CSV
</button>
<SingleStat {tr} overview={$overview} diffInDays={$diffInDays} />
</AccordionItem>
{/each}
</Accordion>
<button on:click={() => offerAsDownload()}>
<DownloadIcon class="h-6 w-6" />
Download as CSV
</button>
{/if}
{/if}

View file

@ -1,6 +1,7 @@
import { Utils } from "../../Utils"
import { Feature, Polygon } from "geojson"
import { OsmFeature } from "../../Models/OsmFeature"
export interface ChangeSetData extends Feature<Polygon> {
id: number
type: "Feature"
@ -92,8 +93,12 @@ export class ChangesetsOverview {
if (cs === undefined) {
return undefined
}
if (cs.properties.android) {
console.log("Found an ANDROID:", cs.properties)
}
if (cs.properties.editor?.startsWith("iD")) {
// We also fetch based on hashtag, so some edits with iD show up as well
// Sometimes, iD reuses a previous changeset, mimicking (!) mapcomplete accidentally
return undefined
}
if (cs.properties.theme === undefined) {

View file

@ -3,24 +3,32 @@
import { Utils } from "../../Utils"
import Loading from "../Base/Loading.svelte"
import AllStats from "./AllStats.svelte"
import TitledPanel from "../Base/TitledPanel.svelte"
let homeUrl =
"https://data.mapcomplete.org/changeset-metadata/"
let stats_files = "file-overview.json"
let indexFile = UIEventSource.FromPromise(Utils.downloadJson<string[]>(homeUrl + stats_files))
let prefix = /^stats.202[45]/
let filteredIndex = indexFile.mapD(index => index.filter(path => path.match(prefix)))
filteredIndex.addCallbackAndRunD(filtered => console.log("Filtered items are", filtered, indexFile.data))
</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>
<main class="h-screen">
<TitledPanel>
<div slot="title" class="flex w-full justify-between">
<span>Statistics of changes made with MapComplete</span>
</div>
<a slot="title-end" href="/" class="button">Back to index</a>
{#if $indexFile === undefined}
<Loading>Loading index file...</Loading>
{:else}
<AllStats
paths={$filteredIndex.map((p) => homeUrl + "/" + p)}
/>
{/if}
</TitledPanel>
</main>