From 6ae258c48d4599b05a8b995585e8bd1a8b08fc3e Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Sat, 12 Jul 2025 15:53:51 +0200 Subject: [PATCH] Refactoring: Histogram now uses chartJS, simplify code --- .../ImageProviders/WikimediaImageProvider.ts | 2 +- src/UI/Base/ChartJsUtils.ts | 54 +++++- src/UI/Base/FixedUiElement.ts | 14 -- src/UI/Base/Table.ts | 81 --------- src/UI/BaseUIElement.ts | 15 -- src/UI/BigComponents/Histogram.svelte | 35 ++++ src/UI/BigComponents/Histogram.ts | 157 ------------------ src/UI/Popup/HistogramViz.ts | 46 +---- src/UI/i18n/Translation.ts | 2 +- 9 files changed, 92 insertions(+), 314 deletions(-) delete mode 100644 src/UI/Base/Table.ts create mode 100644 src/UI/BigComponents/Histogram.svelte delete mode 100644 src/UI/BigComponents/Histogram.ts diff --git a/src/Logic/ImageProviders/WikimediaImageProvider.ts b/src/Logic/ImageProviders/WikimediaImageProvider.ts index 9b9b38c9a4..e6c6d183e3 100644 --- a/src/Logic/ImageProviders/WikimediaImageProvider.ts +++ b/src/Logic/ImageProviders/WikimediaImageProvider.ts @@ -119,7 +119,7 @@ export class WikimediaImageProvider extends ImageProvider { } SourceIcon(): BaseUIElement { - return new SvelteUIElement(Wikimedia_commons_white).SetStyle("width:2em;height: 2em") + return new SvelteUIElement(Wikimedia_commons_white) } public PrepUrl(value: NonNullable): ProvidedImage diff --git a/src/UI/Base/ChartJsUtils.ts b/src/UI/Base/ChartJsUtils.ts index 292d4e4a5d..6e6d856f5a 100644 --- a/src/UI/Base/ChartJsUtils.ts +++ b/src/UI/Base/ChartJsUtils.ts @@ -3,8 +3,10 @@ import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" import { TagUtils } from "../../Logic/Tags/TagUtils" import { OsmFeature } from "../../Models/OsmFeature" import { Utils } from "../../Utils" +import { labels } from "wikibase-sdk/dist/src/helpers/simplify" +import { isInteger } from "tailwind-merge/dist/lib/validators" -export class ChartJsColours { +class ChartJsColours { public static readonly unknownColor = "rgba(128, 128, 128, 0.2)" public static readonly unknownBorderColor = "rgba(128, 128, 128, 0.2)" @@ -205,7 +207,7 @@ export class ChartJsUtils { hideUnknown?: boolean hideNotApplicable?: boolean } - ) { + ): ChartConfiguration { const { labels, data } = ChartJsUtils.extractDataAndLabels(tr, features, { sort: true, groupToOtherCutoff: options?.groupToOtherCutoff, @@ -336,7 +338,7 @@ export class ChartJsUtils { * @returns undefined if not enough parameters */ static createConfigForTagRendering }>(tagRendering: TagRenderingConfig, features: T[], - options?: TagRenderingChartOptions){ + options?: TagRenderingChartOptions): ChartConfiguration{ if (tagRendering.mappings?.length === 0 && tagRendering.freeform?.key === undefined) { return undefined } @@ -380,7 +382,7 @@ export class ChartJsUtils { barchartMode = true } - const config = { + return { type: options?.chartType ?? (barchartMode ? "bar" : "pie"), data: { labels, @@ -402,7 +404,49 @@ export class ChartJsUtils { }, }, } - return config } + + static createHistogramConfig(keys: string[], counts: Map){ + + const borderColor = [ + ] + const backgroundColor = [ + ] + + while (borderColor.length < keys.length) { + borderColor.push(...ChartJsColours.borderColors) + backgroundColor.push(...ChartJsColours.backgroundColors) + } + + return { + type: "bar", + data: { + labels: keys, + datasets: [ + { + data: keys.map((k) => counts.get(k)), + backgroundColor, + borderColor, + borderWidth: 1, + label: undefined, + }, + ], + }, + options: { scales: { + y: { + ticks: { + stepSize: 1, + callback: (value) =>Number(value).toFixed(0), + }, + }, + }, + plugins: { + legend: { + display: false, + }, + }, + }, + } + } } diff --git a/src/UI/Base/FixedUiElement.ts b/src/UI/Base/FixedUiElement.ts index 2fc1e46f22..2afaf56683 100644 --- a/src/UI/Base/FixedUiElement.ts +++ b/src/UI/Base/FixedUiElement.ts @@ -10,20 +10,6 @@ export class FixedUiElement extends BaseUIElement { super() this.content = html ?? "" } - - AsMarkdown(): string { - if (this.HasClass("code")) { - if (this.content.indexOf("\n") > 0 || this.HasClass("block")) { - return "\n```\n" + this.content + "\n```\n" - } - return "`" + this.content + "`" - } - if (this.HasClass("font-bold")) { - return "*" + this.content + "*" - } - return this.content - } - protected InnerConstructElement(): HTMLElement { const e = document.createElement("span") e.innerHTML = Utils.purify(this.content) diff --git a/src/UI/Base/Table.ts b/src/UI/Base/Table.ts deleted file mode 100644 index 515cc9931d..0000000000 --- a/src/UI/Base/Table.ts +++ /dev/null @@ -1,81 +0,0 @@ -import BaseUIElement from "../BaseUIElement" -import { Utils } from "../../Utils" -import Translations from "../i18n/Translations" - -/** - * @deprecated - */ -export default class Table extends BaseUIElement { - private readonly _header: BaseUIElement[] - private readonly _contents: BaseUIElement[][] - private readonly _contentStyle: string[][] - - constructor( - header: (BaseUIElement | string)[], - contents: (BaseUIElement | string)[][], - options?: { - contentStyle?: string[][] - } - ) { - super() - this._contentStyle = options?.contentStyle ?? [["min-width: 9rem"]] - this._header = header?.map(Translations.W) - this._contents = contents.map((row) => row.map(Translations.W)) - } - - protected InnerConstructElement(): HTMLElement { - const table = document.createElement("table") - - const headerElems = Utils.NoNull( - (this._header ?? []).map((elem) => elem.ConstructElement()) - ) - if (headerElems.length > 0) { - const thead = document.createElement("thead") - - const tr = document.createElement("tr") - headerElems.forEach((headerElem) => { - const td = document.createElement("th") - td.appendChild(headerElem) - tr.appendChild(td) - }) - thead.appendChild(tr) - table.appendChild(thead) - } - - for (let i = 0; i < this._contents.length; i++) { - const row = this._contents[i] - const tr = document.createElement("tr") - for (let j = 0; j < row.length; j++) { - try { - const elem = row[j] - if (elem?.ConstructElement === undefined) { - continue - } - const htmlElem = elem?.ConstructElement() - if (htmlElem === undefined) { - continue - } - - let style = undefined - if ( - this._contentStyle !== undefined && - this._contentStyle[i] !== undefined && - this._contentStyle[j] !== undefined - ) { - style = this._contentStyle[i][j] - } - - const td = document.createElement("td") - td.style.cssText = style - td.appendChild(htmlElem) - tr.appendChild(td) - } catch (e) { - console.error("Could not render an element in a table due to", e, row[j]) - } - } - table.appendChild(tr) - } - - return table - } -} diff --git a/src/UI/BaseUIElement.ts b/src/UI/BaseUIElement.ts index f697af7258..f7e87f38fe 100644 --- a/src/UI/BaseUIElement.ts +++ b/src/UI/BaseUIElement.ts @@ -13,16 +13,6 @@ export default abstract class BaseUIElement { protected readonly clss: Set = new Set() protected style: string private _onClick: () => void | Promise - - public onClick(f: () => void) { - this._onClick = f - this.SetClass("cursor-pointer") - if (this._constructedHtmlElement !== undefined) { - this._constructedHtmlElement.onclick = f - } - return this - } - AttachTo(divId: string) { const element = document.getElementById(divId) if (element === null) { @@ -75,11 +65,6 @@ export default abstract class BaseUIElement { } return this } - - public HasClass(clss: string): boolean { - return this.clss.has(clss) - } - public SetStyle(style: string): this { this.style = style if (this._constructedHtmlElement !== undefined) { diff --git a/src/UI/BigComponents/Histogram.svelte b/src/UI/BigComponents/Histogram.svelte new file mode 100644 index 0000000000..a6263b339d --- /dev/null +++ b/src/UI/BigComponents/Histogram.svelte @@ -0,0 +1,35 @@ + + +{#if $config} + +{/if} diff --git a/src/UI/BigComponents/Histogram.ts b/src/UI/BigComponents/Histogram.ts deleted file mode 100644 index bc439ba8e5..0000000000 --- a/src/UI/BigComponents/Histogram.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { VariableUiElement } from "../Base/VariableUIElement" -import { Store, UIEventSource } from "../../Logic/UIEventSource" -import Table from "../Base/Table" -import Combine from "../Base/Combine" -import { FixedUiElement } from "../Base/FixedUiElement" -import { Utils } from "../../Utils" -import BaseUIElement from "../BaseUIElement" -import SvelteUIElement from "../Base/SvelteUIElement" -import Circle from "../../assets/svg/Circle.svelte" -import ChevronUp from "@babeard/svelte-heroicons/solid/ChevronUp" -import ChevronDown from "@babeard/svelte-heroicons/solid/ChevronDown" - -export default class Histogram extends VariableUiElement { - private static defaultPalette = [ - "#ff5858", - "#ffad48", - "#ffff59", - "#56bd56", - "#63a9ff", - "#9d62d9", - "#fa61fa", - ] - - constructor( - values: Store, - title: string | BaseUIElement, - countTitle: string | BaseUIElement, - options?: { - assignColor?: (t0: string) => string - sortMode?: "name" | "name-rev" | "count" | "count-rev" - } - ) { - const sortMode = new UIEventSource<"name" | "name-rev" | "count" | "count-rev">( - options?.sortMode ?? "name" - ) - const sortName = new VariableUiElement( - sortMode.map((m) => { - switch (m) { - case "name": - return new SvelteUIElement(ChevronUp) - case "name-rev": - return new SvelteUIElement(ChevronDown) - default: - return new SvelteUIElement(Circle) - } - }) - ) - const titleHeader = new Combine([sortName.SetClass("w-4 mr-2"), title]) - .SetClass("flex") - .onClick(() => { - if (sortMode.data === "name") { - sortMode.setData("name-rev") - } else { - sortMode.setData("name") - } - }) - - const sortCount = new VariableUiElement( - sortMode.map((m) => { - switch (m) { - case "count": - return new SvelteUIElement(ChevronUp) - case "count-rev": - return new SvelteUIElement(ChevronDown) - default: - return new SvelteUIElement(Circle) - } - }) - ) - - const countHeader = new Combine([sortCount.SetClass("w-4 mr-2"), countTitle]) - .SetClass("flex") - .onClick(() => { - if (sortMode.data === "count-rev") { - sortMode.setData("count") - } else { - sortMode.setData("count-rev") - } - }) - - const header = [titleHeader, countHeader] - - super( - values.map( - (values) => { - if (values === undefined) { - return undefined - } - - values = Utils.NoNull(values) - - const counts = new Map() - for (const value of values) { - const c = counts.get(value) ?? 0 - counts.set(value, c + 1) - } - - const keys = Array.from(counts.keys()) - - switch (sortMode.data) { - case "name": - keys.sort() - break - case "name-rev": - keys.sort().reverse(/*Copy of array, inplace reverse if fine*/) - break - case "count": - keys.sort((k0, k1) => counts.get(k0) - counts.get(k1)) - break - case "count-rev": - keys.sort((k0, k1) => counts.get(k1) - counts.get(k0)) - break - } - - const max = Math.max(...Array.from(counts.values())) - - const fallbackColor = (keyValue: string) => { - const index = keys.indexOf(keyValue) - return Histogram.defaultPalette[index % Histogram.defaultPalette.length] - } - let actualAssignColor = undefined - if (options?.assignColor === undefined) { - actualAssignColor = fallbackColor - } else { - actualAssignColor = (keyValue: string) => { - return options.assignColor(keyValue) ?? fallbackColor(keyValue) - } - } - - return new Table( - header, - keys.map((key) => [ - key, - new Combine([ - new Combine([ - new FixedUiElement("" + counts.get(key)).SetClass( - "font-bold rounded-full block" - ), - ]) - .SetClass("flex justify-center rounded border border-black") - .SetStyle( - `background: ${actualAssignColor(key)}; width: ${ - (100 * counts.get(key)) / max - }%` - ), - ]).SetClass("block w-full"), - ]), - { - contentStyle: keys.map(() => ["width: 20%"]), - } - ).SetClass("w-full zebra-table") - }, - [sortMode] - ) - ) - } -} diff --git a/src/UI/Popup/HistogramViz.ts b/src/UI/Popup/HistogramViz.ts index 2acd9883e8..901225a781 100644 --- a/src/UI/Popup/HistogramViz.ts +++ b/src/UI/Popup/HistogramViz.ts @@ -1,7 +1,8 @@ import { Store, UIEventSource } from "../../Logic/UIEventSource" import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" -import Histogram from "../BigComponents/Histogram" import { Feature } from "geojson" +import SvelteUIElement from "../Base/SvelteUIElement" +import Histogram from "../BigComponents/Histogram.svelte" export class HistogramViz extends SpecialVisualization { funcName = "histogram" @@ -15,21 +16,8 @@ export class HistogramViz extends SpecialVisualization { name: "key", doc: "The key to be read and to generate a histogram from", required: true, - }, - { - name: "title", - doc: "This text will be placed above the texts (in the first column of the visulasition)", - defaultValue: "", - }, - { - name: "countHeader", - doc: "This text will be placed above the bars", - defaultValue: "", - }, - { - name: "colors*", - doc: "(Matches all resting arguments - optional) Matches a regex onto a color value, e.g. `3[a-zA-Z+-]*:#33cc33`", - }, + } + ] structuredExamples(): { feature: Feature; args: string[] }[] { @@ -53,27 +41,7 @@ export class HistogramViz extends SpecialVisualization { tagSource: UIEventSource>, args: string[] ) { - let assignColors = undefined - if (args.length >= 3) { - const colors = [...args] - colors.splice(0, 3) - const mapping = colors.map((c) => { - const splitted = c.split(":") - const value = splitted.pop() - const regex = splitted.join(":") - return { regex: "^" + regex + "$", color: value } - }) - assignColors = (key) => { - for (const kv of mapping) { - if (key.match(kv.regex) !== null) { - return kv.color - } - } - return undefined - } - } - - const listSource: Store = tagSource.map((tags) => { + const values: Store = tagSource.map((tags) => { const value = tags[args[0]] try { if (value === "" || value === undefined) { @@ -88,8 +56,6 @@ export class HistogramViz extends SpecialVisualization { return undefined } }) - return new Histogram(listSource, args[1], args[2], { - assignColor: assignColors, - }) + return new SvelteUIElement(Histogram, { values }) } } diff --git a/src/UI/i18n/Translation.ts b/src/UI/i18n/Translation.ts index 694fabca86..22d39c35aa 100644 --- a/src/UI/i18n/Translation.ts +++ b/src/UI/i18n/Translation.ts @@ -295,7 +295,7 @@ export class Translation extends BaseUIElement { tr["_context"] = this.context } - return new Translation(tr, copyContext ? this.context : null) + return new Translation(tr, copyContext ? this.context : undefined) } /**