aboutsummaryrefslogtreecommitdiffstats
path: root/app/main.js
diff options
context:
space:
mode:
authorPetri Hienonen <petri.hienonen@gmail.com>2025-11-09 22:59:02 +0200
committerPetri Hienonen <petri.hienonen@gmail.com>2025-11-11 15:35:03 +0200
commit909773f9d253c61183cc1f9f6193656957946be5 (patch)
tree136075e1946accedda0530dd25940b8931408c5a /app/main.js
parentbe7ec90b500ac68e053f2b58feb085247ef95817 (diff)
downloadhousing-909773f9d253c61183cc1f9f6193656957946be5.tar.zst
Add statistical areas
Diffstat (limited to '')
-rw-r--r--app/main.js377
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"),
+ );
}
}