diff options
Diffstat (limited to '')
| -rw-r--r-- | app/main.js | 377 |
1 files changed, 89 insertions, 288 deletions
diff --git a/app/main.js b/app/main.js index 1273a11..3995a07 100644 --- a/app/main.js +++ b/app/main.js @@ -1,12 +1,15 @@ -// main.js -import { Dom, DomOptions, Modal, Widgets } from "dom"; -import { ColorParameter, MapEl } from "map"; +// main.js - Updated with Sidebar class + +import { Modal, Sidebar } from "components"; +import { Dom, DomOptions } from "dom"; +import { AreaColorParameter, ColorParameter, MapEl } from "map"; import { DataProvider, District, Filters, House, ScoringEngine, + StatisticalArea, TrainStation, TrainTracks, Weights, @@ -19,6 +22,8 @@ export class App { #trainTracks = []; /** @type {TrainStation[]} */ #trainStations = []; + /** @type {StatisticalArea[]} */ + #statAreas = []; /** @type {House[]} */ #filtered = []; /** @type {Filters} */ @@ -31,14 +36,16 @@ export class App { #map; /** @type {HTMLElement} */ #stats; - /** @type {HTMLElement} */ - #controls; + /** @type {Sidebar} */ + #sidebar; /** @type {Modal|null} */ #modal = null; /** @type {boolean} */ #persistent = false; /** @type {string} */ #colorParameter = ColorParameter.price; + /** @type {string} */ + #areaColorParameter = AreaColorParameter.unemploymentRate; constructor() { // Set up main layout container @@ -50,29 +57,14 @@ export class App { margin: "0", }); - this.#controls = App.buildControls( + // Create sidebar instance + this.#sidebar = new Sidebar( this.#filters, this.#weights, - () => { - this.#filtered = this.#houses.filter((h) => h.matchesFilters(this.#filters)); - if (this.#map) { - const filteredIds = this.#filtered.map((h) => h.id); - this.#map.updateHouseVisibility(filteredIds); - } - this.#updateStats(); - }, - (key, value) => { - if (key in this.#weights) { - this.#weights[/** @type {keyof Weights} */ (key)] = value; - } - App.#recalculateScores(this.#houses, this.#weights); - this.#map?.setColorParameter(this.#colorParameter); - this.#updateStats(); - }, - (param) => { - this.#colorParameter = param; - this.#map?.setColorParameter(this.#colorParameter); - }, + () => this.#onFilterChange(), + (key, value) => this.#onWeightChange(key, value), + (param) => this.#onColorChange(param), + (param) => this.#onAreaColorChange(param), ); this.#map = new MapEl({ @@ -125,7 +117,7 @@ export class App { Dom.div( new DomOptions({ children: [ - this.#controls, + this.#sidebar.render(), Dom.div( new DomOptions({ children: [this.#map.svg, this.#stats], @@ -134,7 +126,7 @@ export class App { display: "flex", flex: "1", flexDirection: "column", - minWidth: "0", // Prevents flex overflow + minWidth: "0", }, }), ), @@ -152,246 +144,47 @@ export class App { } /** - * Build controls container - * @param {Filters} filters - * @param {Weights} weights - * @param {() => void} onFilterChange - * @param {(key: string, value: number) => void} onWeightChange - * @param {(param: string) => void} onColorChange - * @returns {HTMLElement} + * Handle filter changes */ - static buildControls(filters, weights, onFilterChange, onWeightChange, onColorChange) { - const controls = Dom.div( - new DomOptions({ - children: [ - Dom.div( - new DomOptions({ - children: [ - Dom.heading( - 3, - "Map Colors", - new DomOptions({ - styles: { - color: "#333", - fontSize: "1.1rem", - margin: "0 0 1rem 0", - }, - }), - ), - Dom.div( - new DomOptions({ - children: [ - Dom.label( - "color-parameter", - "Color houses by", - new DomOptions({ - styles: { - fontSize: "0.85rem", - fontWeight: "bold", - marginBottom: "0.25rem", - }, - }), - ), - Dom.select( - (e) => { - const target = /** @type {HTMLSelectElement} */ (e.target); - onColorChange(target.value); - }, - new DomOptions({ - children: [ - Dom.option(ColorParameter.price, "Price"), - Dom.option(ColorParameter.score, "Score"), - Dom.option(ColorParameter.year, "Construction Year"), - Dom.option(ColorParameter.area, "Living Area"), - ], - id: "color-parameter", - styles: { - border: "1px solid #ddd", - borderRadius: "4px", - fontSize: "0.9rem", - padding: "0.5rem", - }, - }), - ), - ], - styles: { display: "flex", flexDirection: "column" }, - }), - ), - ], - styles: { - borderBottom: "1px solid #eee", - paddingBottom: "1rem", - }, - }), - ), - ], - id: "color-section", - styles: { - background: "#fff", - borderRight: "1px solid #ddd", - display: "flex", - flexDirection: "column", - flexShrink: "0", - gap: "1rem", - overflowY: "auto", - padding: "1rem", - width: "300px", - }, - }), - ); - - controls.append( - Dom.div( - new DomOptions({ - children: [ - Dom.heading( - 3, - "Filters", - new DomOptions({ - styles: { - color: "#333", - fontSize: "1.1rem", - margin: "0 0 1rem 0", - }, - }), - ), - - Dom.div( - new DomOptions({ - children: [ - Widgets.numberFilter("min-price", "Min price (€)", (v) => { - filters.minPrice = v ?? 0; - onFilterChange(); - }), + #onFilterChange() { + this.#filtered = this.#houses.filter((h) => h.matchesFilters(this.#filters)); + if (this.#map) { + const filteredIds = this.#filtered.map((h) => h.id); + this.#map.updateHouseVisibility(filteredIds); + } + this.#updateStats(); + } - Widgets.numberFilter("max-price", "Max price (€)", (v) => { - filters.maxPrice = v ?? Number.POSITIVE_INFINITY; - onFilterChange(); - }), - ], - id: "price-row", - styles: { - display: "flex", - gap: "0.5rem", - }, - }), - ), - Widgets.numberFilter("min-year", "Min year", (v) => { - filters.minYear = v ?? 0; - onFilterChange(); - }), + /** + * Handle weight changes + * @param {string} key + * @param {number} value + */ + #onWeightChange(key, value) { + if (key in this.#weights) { + this.#weights[/** @type {keyof Weights} */ (key)] = value; + } + App.#recalculateScores(this.#houses, this.#weights); + this.#map?.setColorParameter(this.#colorParameter); + this.#updateStats(); + } - Widgets.numberFilter("min-area", "Min area (m²)", (v) => { - filters.minArea = v ?? 0; - onFilterChange(); - }), - Dom.div( - new DomOptions({ - children: [ - Dom.label( - "district-select", - "Districts", - new DomOptions({ - styles: { fontSize: "0.85rem", fontWeight: "bold", marginBottom: "0.25rem" }, - }), - ), - Dom.select( - (e) => { - const target = /** @type {HTMLSelectElement} */ (e.target); - const selectedOptions = Array.from(target.selectedOptions).map( - (opt) => opt.value, - ); - filters.districts = selectedOptions; - onFilterChange(); - }, - new DomOptions({ - attributes: { multiple: "true" }, - children: [], - id: "district-select", - styles: { - border: "1px solid #ddd", - borderRadius: "4px", - minHeight: "120px", - padding: "0.5rem", - }, - }), - ), - ], - id: "district-multi-select", - styles: { display: "flex", flexDirection: "column" }, - }), - ), - ], - id: "filter-section", - styles: { - borderBottom: "1px solid #eee", - paddingBottom: "1rem", - }, - }), - ), - Dom.div( - new DomOptions({ - children: [ - Dom.heading( - 3, - "Weights", - new DomOptions({ - styles: { - color: "#333", - fontSize: "1.1rem", - margin: "0 0 1rem 0", - }, - }), - ), - Widgets.slider("w-price", "Price weight", "price", weights.price, onWeightChange), - Widgets.slider( - "w-market", - "Market distance", - "distanceMarket", - weights.distanceMarket, - onWeightChange, - ), - Widgets.slider( - "w-school", - "School distance", - "distanceSchool", - weights.distanceSchool, - onWeightChange, - ), - Widgets.slider("w-crime", "Crime rate", "crimeRate", weights.crimeRate, onWeightChange), - Widgets.slider("w-safety", "Safety index", "safety", weights.safety, onWeightChange), - Widgets.slider( - "w-students", - "S2 students", - "s2Students", - weights.s2Students, - onWeightChange, - ), - Widgets.slider( - "w-railway", - "Railway distance", - "distanceRailway", - weights.distanceRailway, - onWeightChange, - ), - Widgets.slider( - "w-year", - "Construction year", - "constructionYear", - weights.constructionYear, - onWeightChange, - ), - ], - id: "weights-section", - styles: { - borderBottom: "1px solid #eee", - paddingBottom: "1rem", - }, - }), - ), - ); + /** + * Handle color parameter changes + * @param {string} param + */ + #onColorChange(param) { + this.#colorParameter = param; + this.#map?.setColorParameter(this.#colorParameter); + } - return controls; + /** + * Handle area color parameter changes + * @param {string} param + */ + #onAreaColorChange(param) { + this.#areaColorParameter = param; + this.#map?.setAreaColorParameter(this.#areaColorParameter); } /** @@ -441,13 +234,14 @@ export class App { document.body.appendChild(this.#modal.render()); this.#modal.show(); } + /** * Load data and initialize application * @param {HTMLElement} loading */ async #initialize(loading) { try { - const [districts, houses, trainStations, trainTracks, coastLine, mainRoads] = + const [districts, houses, trainStations, trainTracks, coastLine, mainRoads, statAreas] = await Promise.all([ DataProvider.getDistricts(), DataProvider.getHouses(), @@ -455,11 +249,13 @@ export class App { DataProvider.getTrainTracks(), DataProvider.getCoastline(), DataProvider.getMainRoads(), + DataProvider.getStatisticalAreas(), ]); this.#districts = districts; this.#houses = houses; this.#trainStations = trainStations; this.#trainTracks = trainTracks; + this.#statAreas = statAreas; this.#filtered = houses.slice(); @@ -470,15 +266,16 @@ export class App { trainTracks, trainStations, houses, + statAreas, this.#colorParameter, ); - // Populate district multi-select - const districtOptions = App.#renderDistrictOptions(this.#districts, this.#houses); - const districtSelect = this.#controls.querySelector("#district-select"); - if (districtSelect) { - districtSelect.append(...districtOptions); - } + // Set default area coloring to unemployment rate + this.#map.setAreaColorParameter(this.#areaColorParameter); + + // Update sidebar with districts and area color parameter + this.#sidebar.updateDistricts(this.#districts, this.#houses); + this.#sidebar.setAreaColorParameter(this.#areaColorParameter); this.#updateStats(); } finally { @@ -487,18 +284,6 @@ export class App { } /** - * Render district options for multi-select - * @param {District[]} _districts - * @param {House[]} houses - * @returns {HTMLOptionElement[]} - */ - static #renderDistrictOptions(_districts, houses) { - // Get unique districts from houses (they might have districts not in the district polygons) - const houseDistricts = [...new Set(houses.map((h) => h.district).filter((d) => d))].sort(); - return houseDistricts.map((districtName) => Dom.option(districtName, districtName)); - } - - /** * Recalculate scores statically * @param {House[]} houses * @param {Weights} weights @@ -509,16 +294,32 @@ export class App { } } + /** + * Update statistics display using DOM methods + */ #updateStats() { const count = this.#filtered.length; const avg = count ? Math.round(this.#filtered.reduce((s, h) => s + h.scores.current, 0) / count) : 0; - this.#stats.innerHTML = ` - <strong>${count}</strong> houses shown - • Average score: <strong>${avg}</strong> - • Use weights sliders to adjust scoring - `; + + // Clear existing content + this.#stats.innerHTML = ""; + + // Create elements using DOM methods + const countStrong = document.createElement("strong"); + countStrong.textContent = count.toString(); + + const avgStrong = document.createElement("strong"); + avgStrong.textContent = avg.toString(); + + // Append all elements + this.#stats.append( + countStrong, + document.createTextNode(" houses shown • Average score: "), + avgStrong, + document.createTextNode(" • Use weights sliders to adjust scoring"), + ); } } |
