Interactive weather dashboard with data visualization and location-based forecasts
A comprehensive weather dashboard that provides current conditions, 7-day forecasts, and interactive maps. Features location-based weather data, historical trends, and customizable widgets for different weather metrics.
This weather dashboard transforms complex meteorological data into an intuitive, visually appealing interface. Built as a Progressive Web App (PWA), it provides users with comprehensive weather information through interactive charts, maps, and customizable widgets.
Built with Vue.js 3 and the Composition API for reactive data management:
// Weather data composable
export const useWeatherData = () => {
const weatherData = ref<WeatherData | null>(null);
const loading = ref(false);
const error = ref<string | null>(null);
const fetchWeather = async (lat: number, lon: number) => {
loading.value = true;
error.value = null;
try {
const response = await weatherAPI.getCurrentWeather(lat, lon);
weatherData.value = response.data;
} catch (err) {
error.value = "Failed to fetch weather data";
} finally {
loading.value = false;
}
};
return {
weatherData: readonly(weatherData),
loading: readonly(loading),
error: readonly(error),
fetchWeather,
};
};
Custom charts built with D3.js for maximum flexibility and performance:
// Temperature trend chart
const createTemperatureChart = (data: TemperatureData[]) => {
const svg = d3.select("#temperature-chart");
const margin = { top: 20, right: 30, bottom: 40, left: 50 };
const width = 800 - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom;
const xScale = d3
.scaleTime()
.domain(d3.extent(data, (d) => d.timestamp))
.range([0, width]);
const yScale = d3
.scaleLinear()
.domain(d3.extent(data, (d) => d.temperature))
.range([height, 0]);
const line = d3
.line<TemperatureData>()
.x((d) => xScale(d.timestamp))
.y((d) => yScale(d.temperature))
.curve(d3.curveMonotoneX);
svg
.append("path")
.datum(data)
.attr("class", "temperature-line")
.attr("d", line);
};
Service worker for offline functionality and caching:
// Service worker for caching weather data
self.addEventListener("fetch", (event) => {
if (event.request.url.includes("/api/weather")) {
event.respondWith(
caches.open("weather-cache-v1").then((cache) => {
return cache.match(event.request).then((response) => {
if (response) {
// Serve from cache, but update in background
fetch(event.request).then((fetchResponse) => {
cache.put(event.request, fetchResponse.clone());
});
return response;
}
// Fetch and cache new data
return fetch(event.request).then((fetchResponse) => {
cache.put(event.request, fetchResponse.clone());
return fetchResponse;
});
});
})
);
}
});
Integrated multiple APIs for comprehensive data coverage:
class WeatherAggregator {
async getComprehensiveWeather(lat: number, lon: number) {
const [current, forecast, alerts] = await Promise.allSettled([
this.openWeatherAPI.getCurrent(lat, lon),
this.weatherAPI.getForecast(lat, lon),
this.noaaAPI.getAlerts(lat, lon),
]);
return {
current: current.status === "fulfilled" ? current.value : null,
forecast: forecast.status === "fulfilled" ? forecast.value : null,
alerts: alerts.status === "fulfilled" ? alerts.value : [],
};
}
}
Problem: Different weather APIs have varying data formats and reliability.
Solution: Created a unified data adapter layer:
Problem: Keeping weather data current without overwhelming API rate limits.
Solution: Implemented smart caching and update strategies:
Problem: Rendering historical weather data caused performance issues.
Solution: Optimized data visualization:
<template>
<div class="weather-widget" :class="{ loading: loading }">
<div v-if="weatherData" class="weather-content">
<h3>{{ weatherData.location.name }}</h3>
<div class="temperature">
{{ Math.round(weatherData.current.temp) }}°{{ unit }}
</div>
<div class="condition">
<img
:src="weatherData.current.icon"
:alt="weatherData.current.description"
/>
<span>{{ weatherData.current.description }}</span>
</div>
</div>
<div v-else-if="error" class="error-state">
<p>{{ error }}</p>
<button @click="retry">Retry</button>
</div>
<div v-else class="loading-skeleton">
<!-- Loading skeleton -->
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
import { useWeatherData } from "@/composables/useWeatherData";
import { useGeolocation } from "@/composables/useGeolocation";
const { weatherData, loading, error, fetchWeather } = useWeatherData();
const { coordinates } = useGeolocation();
const unit = computed(() =>
weatherData.value?.units === "metric" ? "C" : "F"
);
const retry = () => {
if (coordinates.value) {
fetchWeather(coordinates.value.lat, coordinates.value.lon);
}
};
</script>
// Weather map with overlay controls
export class WeatherMap {
private map: mapboxgl.Map;
private weatherLayer: mapboxgl.Layer | null = null;
constructor(container: string) {
this.map = new mapboxgl.Map({
container,
style: "mapbox://styles/mapbox/satellite-v9",
center: [-74.5, 40],
zoom: 9,
});
}
addWeatherLayer(type: "precipitation" | "temperature" | "wind") {
if (this.weatherLayer) {
this.map.removeLayer(this.weatherLayer.id);
}
this.weatherLayer = {
id: `weather-${type}`,
type: "raster",
source: {
type: "raster",
tiles: [`https://api.weather.com/v1/maps/${type}/{z}/{x}/{y}.png`],
tileSize: 256,
},
paint: {
"raster-opacity": 0.7,
},
};
this.map.addLayer(this.weatherLayer);
}
}