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

View file

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

View file

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

View file

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