From bcc8236dddbda84f3f28c869db703be299877fbd Mon Sep 17 00:00:00 2001 From: sameeranageshwar Date: Sun, 12 Apr 2026 01:09:03 -0400 Subject: [PATCH] frontend graphs --- website/bun.lock | 11 + website/package.json | 5 +- .../src/lib/components/AlertsDropdown.svelte | 53 +- .../src/lib/components/InteractiveMap.svelte | 682 +++++++++++------- .../src/lib/components/LocationPopup.svelte | 190 +++++ website/src/lib/states/alerts.svelte.ts | 66 ++ website/src/lib/states/map.svelte.ts | 23 +- website/src/routes/history/+page.svelte | 4 +- website/src/routes/layout.css | 2 + 9 files changed, 755 insertions(+), 281 deletions(-) create mode 100644 website/src/lib/components/LocationPopup.svelte create mode 100644 website/src/lib/states/alerts.svelte.ts diff --git a/website/bun.lock b/website/bun.lock index d26e7d3..b8979a7 100644 --- a/website/bun.lock +++ b/website/bun.lock @@ -6,6 +6,9 @@ "name": "bitcamp-2026", "dependencies": { "@auth0/auth0-spa-js": "^2.19.0", + "chart.js": "^4.4.0", + "chartjs-adapter-date-fns": "^3.0.0", + "date-fns": "^2.30.0", "maplibre-gl": "^5.0.1", }, "devDependencies": { @@ -238,6 +241,8 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="], + "@mapbox/jsonlint-lines-primitives": ["@mapbox/jsonlint-lines-primitives@2.0.2", "", {}, "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ=="], "@mapbox/point-geometry": ["@mapbox/point-geometry@1.1.0", "", {}, "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ=="], @@ -472,6 +477,10 @@ "caniuse-lite": ["caniuse-lite@1.0.30001787", "", {}, "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg=="], + "chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="], + + "chartjs-adapter-date-fns": ["chartjs-adapter-date-fns@3.0.0", "", { "peerDependencies": { "chart.js": ">=2.8.0", "date-fns": ">=2.0.0" } }, "sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg=="], + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], @@ -500,6 +509,8 @@ "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], + "date-fns": ["date-fns@2.30.0", "", { "dependencies": { "@babel/runtime": "^7.21.0" } }, "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], diff --git a/website/package.json b/website/package.json index 9c9f6e6..d515dd3 100644 --- a/website/package.json +++ b/website/package.json @@ -14,7 +14,10 @@ "type": "module", "dependencies": { "@auth0/auth0-spa-js": "^2.19.0", - "maplibre-gl": "^5.0.1" + "maplibre-gl": "^5.0.1", + "chart.js": "^4.4.0", + "chartjs-adapter-date-fns": "^3.0.0", + "date-fns": "^2.30.0" }, "devDependencies": { "@iconify/svelte": "^5.2.1", diff --git a/website/src/lib/components/AlertsDropdown.svelte b/website/src/lib/components/AlertsDropdown.svelte index d6ec120..a80f73c 100644 --- a/website/src/lib/components/AlertsDropdown.svelte +++ b/website/src/lib/components/AlertsDropdown.svelte @@ -1,12 +1,9 @@
@@ -14,7 +11,7 @@ @@ -24,22 +21,29 @@

Active Noise Alerts

- {mockAlerts.length} New + {alertsState.activeAlerts.length} New
- {#each mockAlerts as alert} - + +
{/each} - {#if mockAlerts.length === 0} + {#if alertsState.activeAlerts.length === 0}

Campus is quiet right now.

@@ -49,3 +53,26 @@
{/if}
+ + + +
+ {#each alertsState.activeAlerts.slice(0, 3) as alert (alert.id)} +
+ +
+
+

NOISE SPIKE: {alert.level} dB

+ +
+

{alert.loc}

+

{alert.time}

+
+
+ {/each} +
diff --git a/website/src/lib/components/InteractiveMap.svelte b/website/src/lib/components/InteractiveMap.svelte index 01d9047..b4dfd9c 100644 --- a/website/src/lib/components/InteractiveMap.svelte +++ b/website/src/lib/components/InteractiveMap.svelte @@ -1,177 +1,281 @@
-
- {#if !themeState.isLight} -
- {/if} +
- diff --git a/website/src/lib/components/LocationPopup.svelte b/website/src/lib/components/LocationPopup.svelte new file mode 100644 index 0000000..b8b5971 --- /dev/null +++ b/website/src/lib/components/LocationPopup.svelte @@ -0,0 +1,190 @@ + + +{#if mapState.selectedLocation} +
+ +
+ +

{mapState.selectedLocation.name}

+
+
+ {statusLabel} + ยท {current2hAvg} dB Avg (Last 2h) +
+
+ + +
+ +
+

Usually quiet this time of day

+

Historical data shows this location typically averages less noise around this time.

+
+
+ + +
+

Past 24 Hours

+
+ +
+
+
+{/if} diff --git a/website/src/lib/states/alerts.svelte.ts b/website/src/lib/states/alerts.svelte.ts new file mode 100644 index 0000000..459fb6a --- /dev/null +++ b/website/src/lib/states/alerts.svelte.ts @@ -0,0 +1,66 @@ +import { UMD_LOCATIONS } from "./map.svelte"; + +export interface Alert { + id: string; + locId: string; + loc: string; + level: number; + time: string; + timestamp: number; +} + +class AlertsState { + activeAlerts = $state([]); + alertHistory = new Map(); + + processLiveReadings(latestReadings: any[]) { + const now = Date.now(); + + for (const room of latestReadings) { + // Only fire if the reading is >= 65dB (Disruptive or Harmful) + if (room.db >= 65) { + const roomDate = new Date(room.date.endsWith('Z') ? room.date : room.date + 'Z').getTime(); + + // Ensure the reading is recent (within 5 minutes, 300000ms), to prevent alerting on stale data on initial load + if (now - roomDate < 300000) { + const lastAlert = this.alertHistory.get(room.room_id) || 0; + + // Cooldown: Don't alert for the same location within 3 minutes (180000ms) + if (now - lastAlert > 180000) { + const locData = UMD_LOCATIONS.find(l => l.id === room.room_id); + const locName = locData ? locData.name : room.room_id; + + const d = new Date(roomDate); + const timeStr = d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', timeZone: 'America/New_York' }) + ' EST'; + + this.activeAlerts.unshift({ + id: Math.random().toString(36).substring(2, 9), + locId: room.room_id, + loc: locName, + level: Number(room.db.toFixed(1)), + time: timeStr, + timestamp: now + }); + + // Keep a max of 10 alerts logic + if (this.activeAlerts.length > 20) { + this.activeAlerts.pop(); + } + + this.alertHistory.set(room.room_id, now); + } + } + } + } + } + + dismissAlert(id: string) { + this.activeAlerts = this.activeAlerts.filter(a => a.id !== id); + } + + clearAll() { + this.activeAlerts = []; + } +} + +export const alertsState = new AlertsState(); diff --git a/website/src/lib/states/map.svelte.ts b/website/src/lib/states/map.svelte.ts index 962e568..e332713 100644 --- a/website/src/lib/states/map.svelte.ts +++ b/website/src/lib/states/map.svelte.ts @@ -19,12 +19,17 @@ export const UMD_LOCATIONS: StudyLocation[] = [ { id: 'stamp', name: 'Stamp Student Union', hasFloors: true, floors: ['Basement', 'Floor 1', 'Floor 2'], lng: -76.94473083972326, lat: 38.988130238874874 } ]; -export const DEFAULT_VIEW = { lng: -76.9425, lat: 38.9859, zoom: 16.5 }; +export const DEFAULT_VIEW = { lng: -76.94259561477574, lat: 38.98813763708658, zoom: 15.5 }; class MapState { // The target coordinates the map should fly to targetFlyTo = $state<{ lng: number; lat: number; zoom: number; timestamp: number } | null>(null); + selectedLocation = $state(null); + historyData = $state([]); + historyLoading = $state(false); + historyPlaybackTime = $state(Date.now()); // The current time scrubber for 24h + flyTo(lng: number, lat: number, zoom: number = 18) { this.targetFlyTo = { lng, lat, zoom, timestamp: Date.now() }; // timestamp ensures reactivity even if same coords } @@ -32,6 +37,22 @@ class MapState { flyHome() { this.flyTo(DEFAULT_VIEW.lng, DEFAULT_VIEW.lat, DEFAULT_VIEW.zoom); } + + async fetchHistoryData() { + this.historyLoading = true; + try { + // Fetch from FastAPI backend + const res = await fetch('http://127.0.0.1:8000/api/study-rooms/history'); + if (res.ok) { + const json = await res.json(); + this.historyData = json.data; + } + } catch (e) { + console.error("Failed to fetch history data", e); + } finally { + this.historyLoading = false; + } + } } export const mapState = new MapState(); diff --git a/website/src/routes/history/+page.svelte b/website/src/routes/history/+page.svelte index 0725674..7119287 100644 --- a/website/src/routes/history/+page.svelte +++ b/website/src/routes/history/+page.svelte @@ -66,10 +66,10 @@
-
+

- + HISTORICAL DATA

diff --git a/website/src/routes/layout.css b/website/src/routes/layout.css index 4ebf2ce..88b2893 100644 --- a/website/src/routes/layout.css +++ b/website/src/routes/layout.css @@ -22,9 +22,11 @@ --shadow-glow-primary: 0 0 15px rgba(243, 139, 168, 0.4); --shadow-glow-alert: 0 0 15px rgba(243, 139, 168, 0.4); --shadow-glow-blue: 0 0 15px rgba(243, 139, 168, 0.4); + --map-icon-filter: invert(1) contrast(100) opacity(0.9); } .light-theme { + --map-icon-filter: invert(0) contrast(150) opacity(0.9); --color-crust: #dce0e8; --color-mantle: #e6e9ef; --color-base: #eff1f5;