Misc: improve statistics page
This commit is contained in:
parent
d5ccd42cfd
commit
9021c372ad
4 changed files with 75 additions and 43 deletions
|
@ -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])
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue