Skip to content

Building a Weather-Based Activities Advisor for the UK

Next.jsTypeScriptWeather APIsData AnalysisUK WeatherActivity Planning

Building a Weather-Based Activities Advisor for the UK

Planning outdoor activities in the UK can be challenging due to the country's notoriously unpredictable weather. To solve this problem, I built a comprehensive weather-based activities advisor that helps users find the best times and locations for outdoor activities across the UK.

The platform consists of three integrated tools: an Activities Advisor, Seasonality Explorer, and Activity Dashboard that together provide intelligent weather-based activity recommendations.

Project Overview

The Activities Advisor addresses a common problem: knowing when and where to do outdoor activities in the UK's variable climate. The system combines real-time weather data, historical climate analysis, and activity-specific scoring to provide actionable recommendations.

Core Features

  • Multi-Activity Support: 18+ activities from picnics to stargazing
  • UK-Wide Coverage: 50+ major UK cities with precise coordinates
  • Live Leaderboard: Real-time ranking of cities by activity suitability
  • Seasonality Analysis: Historical weather patterns using ERA5 reanalysis data
  • Value Optimization: "Fun per £" calculations including travel costs
  • Interactive Dashboard: Multi-visualization dashboard with heatmaps and radar charts

Technical Architecture

The platform is built using modern web technologies optimized for data-heavy applications:

// Core tech stack
- Next.js 15 with App Router
- TypeScript for type safety
- Open-Meteo API for weather data
- Tailwind CSS with custom design system
- Custom SVG visualizations
- Server-side data processing

Data Sources and APIs

The system integrates multiple data sources:

// Weather data from Open-Meteo API
const weatherEndpoint = 'https://api.open-meteo.com/v1/forecast';
const params = new URLSearchParams({
  latitude: String(city.lat),
  longitude: String(city.lon),
  hourly: 'temperature_2m,precipitation,wind_speed_10m,cloud_cover',
  timezone: 'Europe/London',
  forecast_days: '7'
});

// Historical climate data using ERA5 reanalysis
const climatologyEndpoint = 'https://archive-api.open-meteo.com/v1/era5';

Activity Scoring System

The heart of the platform is a sophisticated scoring algorithm that evaluates weather conditions for specific activities:

Activity Configuration

interface Activity {
  id: string;
  name: string;
  minTempC: number;
  maxTempC: number;
  idealTempC: number;
  maxRainMmPerHr: number;
  maxWindKph: number;
  cloudPreference: 'sunny' | 'clear-night' | 'any';
  needsDaylight: boolean;
  baseCostPerPerson: number;
  defaultDurationHours: number;
}

// Example: BBQ configuration
{
  id: 'bbq',
  name: 'BBQ',
  minTempC: 12,
  maxTempC: 35,
  idealTempC: 22,
  maxRainMmPerHr: 0.5,
  maxWindKph: 25,
  cloudPreference: 'sunny',
  needsDaylight: true,
  baseCostPerPerson: 15,
  defaultDurationHours: 3
}

Weighted Scoring Algorithm

export const DEFAULT_WEIGHTS = {
  temperature: 0.35,
  rain: 0.25,
  wind: 0.15,
  cloud: 0.15,
  daylight: 0.10
};

function temperatureScoreC(activity: Activity, tempC: number): number {
  const { minTempC, maxTempC, idealTempC } = activity;
  if (tempC <= minTempC - 5 || tempC >= maxTempC + 5) return 0;
  
  const span = Math.max(2, Math.min(10, (maxTempC - minTempC) / 6));
  const g = gaussian(tempC, idealTempC, span);
  
  // Reduce score outside preferred bounds
  const inRangeFactor = tempC < minTempC || tempC > maxTempC ? 0.6 : 1;
  return clamp01(g) * 100 * inRangeFactor;
}

This scoring system accounts for:

  • Temperature curves: Gaussian distributions around ideal temperatures
  • Rain tolerance: Exponential decay beyond activity thresholds
  • Wind sensitivity: Quadratic penalty for high winds
  • Cloud preferences: Different activities prefer different sky conditions
  • Daylight requirements: Essential for photography, optional for others

Component Architecture

1. Activities Leaderboard

The main leaderboard provides real-time city rankings:

export default function ActivitiesPage() {
  const [selectedActivity, setSelectedActivity] = useState('bbq');
  const [metric, setMetric] = useState<'score' | 'value'>('score');
  const [groupSize, setGroupSize] = useState(2);
  
  // Real-time scoring for all UK cities
  const cityScores = useMemo(() => {
    return cities.map(city => {
      const score = computeScore(activity, weather, costProfile);
      return {
        city: city.name,
        score: score.score,
        funPerCurrency: score.funPerCurrency,
        breakdown: score.breakdown
      };
    });
  }, [selectedActivity, metric, groupSize]);

  return (
    <div className="space-y-6">
      <ActivitySelector />
      <CityLeaderboard cities={sortedCities} />
      <WeatherDetails />
    </div>
  );
}

2. Seasonality Explorer

Historical climate analysis using ERA5 reanalysis data:

async function loadSeasonality() {
  const qp = new URLSearchParams({
    lat: String(selectedCity.lat),
    lon: String(selectedCity.lon),
    activityId,
    year: String(year),
    groupSize: String(groupSize)
  });
  
  const response = await fetch(`/api/climatology?${qp.toString()}`);
  const data = await response.json();
  
  // Process monthly averages
  setMonthly(data.monthly.map(month => ({
    avgBestScore: month.avgBestScore,
    avgBestFpc: month.avgBestFpc,
    stdBestScore: month.stdBestScore,
    days: month.days
  })));
}

The seasonality component reveals patterns like:

  • May-June: Optimal for most outdoor activities (longer daylight, mild temps)
  • July-August: Warmest but more variable wind/rain
  • September: Good shoulder season with stable weather
  • Winter months: Limited daylight affects most activities

3. Multi-Visualization Dashboard

An integrated dashboard combining multiple visualization types:

// Dashboard combines:
- Top cities cards with live scores
- Comparative bar charts across activities  
- Factor breakdown radar charts
- 48-hour weather heatmaps
- Real-time weather updates

export function ActivityDashboard() {
  return (
    <div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
      <TopCitiesCards activities={activities} />
      <ComparativeBarChart cities={topCities} />
      <FactorRadarChart breakdown={weatherBreakdown} />
      <WeatherHeatmap hours={next48Hours} />
    </div>
  );
}

API Design and Data Processing

City Forecast API

// /api/city-forecast
export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const lat = searchParams.get('lat');
  const lon = searchParams.get('lon');
  const activityId = searchParams.get('activityId');
  
  // Fetch weather data
  const weatherData = await fetchWeatherData(lat, lon);
  const activity = getActivityById(activityId);
  
  // Score each hour
  const hours = weatherData.hourly.time.map((time, i) => {
    const weather = {
      tempC: weatherData.hourly.temperature_2m[i],
      precipitationMmPerHr: weatherData.hourly.precipitation[i],
      windKph: weatherData.hourly.wind_speed_10m[i],
      cloudCoverPercent: weatherData.hourly.cloud_cover[i]
    };
    
    const score = computeScore(activity, weather);
    
    return {
      dateTimeIso: time,
      score: score.score,
      fpc: score.funPerCurrency,
      breakdown: score.breakdown,
      weather
    };
  });
  
  return NextResponse.json({ hours });
}

Climatology API for Historical Analysis

// /api/climatology  
export async function GET(request: NextRequest) {
  // Fetch ERA5 historical data for past year
  const historicalData = await fetchERA5Data(lat, lon, year);
  
  // Group by month and calculate statistics
  const monthly = Array.from({ length: 12 }, (_, month) => {
    const monthData = filterByMonth(historicalData, month);
    const dailyBestScores = calculateDailyBestScores(monthData, activity);
    
    return {
      avgBestScore: mean(dailyBestScores),
      stdBestScore: standardDeviation(dailyBestScores),
      avgBestFpc: calculateValueScores(dailyBestScores, activity),
      days: monthData.length
    };
  });
  
  return NextResponse.json({ monthly });
}

Development Challenges and Solutions

1. Weather Data Accuracy and Coverage

Challenge: Getting reliable, high-resolution weather data for 50+ UK cities.

Solution: Integrated Open-Meteo API which provides:

  • 1-hour resolution forecasts
  • Multiple weather parameters
  • Historical ERA5 reanalysis data
  • Free tier with generous limits
  • European weather model optimization

2. Activity-Specific Scoring Complexity

Challenge: Each activity has different weather preferences requiring nuanced scoring.

Solution: Developed a modular scoring system:

export function computeScore(activity: Activity, weather: WeatherSnapshot): ScoreResult {
  const t = temperatureScoreC(activity, weather.tempC);
  const r = rainScoreMmPerHr(activity, weather.precipitationMmPerHr);
  const w = windScoreKph(activity, weather.windKph);
  const c = cloudScore(activity, weather.cloudCoverPercent);
  const d = daylightScore(activity.needsDaylight, weather.isDaylightAtTime);

  const score = Math.round(
    DEFAULT_WEIGHTS.temperature * t +
    DEFAULT_WEIGHTS.rain * r +
    DEFAULT_WEIGHTS.wind * w +
    DEFAULT_WEIGHTS.cloud * c +
    DEFAULT_WEIGHTS.daylight * d
  );

  return { score, breakdown: { t, r, w, c, d }, funPerCurrency };
}

3. Performance with Large Datasets

Challenge: Processing weather data for 50+ cities and 18+ activities in real-time.

Solution: Implemented strategic optimizations:

  • Server-side data processing to reduce client load
  • Memoized scoring calculations
  • Efficient data structures for weather lookups
  • Progressive loading for dashboard components

4. Responsive Design for Data-Heavy UI

Challenge: Making complex data tables and charts work on mobile devices.

Solution: Created responsive data visualization patterns:

// Mobile-first responsive design
<div className="overflow-x-auto">
  <div className="grid grid-cols-7 gap-2 min-w-[700px]">
    {days.map(day => (
      <div key={day.date} className="flex flex-col">
        <div className="text-sm font-medium mb-2">
          {formatDay(day.date)}
        </div>
        <div className="grid grid-rows-24 gap-1">
          {day.hours.map(hour => (
            <WeatherCell key={hour.time} hour={hour} />
          ))}
        </div>
      </div>
    ))}
  </div>
</div>

Value Optimization Features

Cost-Benefit Analysis

The platform calculates "fun per £" by considering:

  • Base activity costs (equipment, food, etc.)
  • Travel distance and fuel costs
  • Group size optimization
  • Duration weighting
function calculateFunPerCurrency(score: number, activity: Activity, cost: CostProfile): number {
  const groupSize = cost.groupSize ?? 2;
  const baseCost = activity.baseCostPerPerson * groupSize;
  const travelCost = (cost.costPerKm ?? 0.45) * (cost.travelDistanceKm ?? 0);
  const totalCost = baseCost + travelCost + (cost.extraCost ?? 0);
  
  return totalCost > 0 
    ? (score * activity.defaultDurationHours) / totalCost 
    : score * activity.defaultDurationHours;
}

Smart Recommendations

The system provides contextual advice:

  • Best 2-hour windows: Optimal timing within forecast period
  • Weekend planning: Identifies Saturday/Sunday opportunities
  • Alternative suggestions: Backup plans when weather is marginal
  • Seasonal insights: Monthly patterns with explanatory context

Data Visualization and UX

Custom Weather Heatmaps

function WeatherHeatmap({ hours }: { hours: Hour[] }) {
  const days = groupByDay(hours);
  
  return (
    <div className="grid grid-cols-7 gap-2">
      {days.map(day => (
        <div key={day.date} className="space-y-1">
          <div className="text-xs text-center font-medium">
            {format(new Date(day.date), 'EEE d')}
          </div>
          {day.hours.map(hour => (
            <div
              key={hour.dateTimeIso}
              className="h-6 rounded text-xs flex items-center justify-center cursor-pointer"
              style={{ 
                backgroundColor: scoreToColor(hour.score),
                color: hour.score > 50 ? 'white' : 'black'
              }}
              title={`${format(new Date(hour.dateTimeIso), 'HH:mm')}: ${hour.score}/100`}
            >
              {format(new Date(hour.dateTimeIso), 'H')}
            </div>
          ))}
        </div>
      ))}
    </div>
  );
}

Interactive City Selection

// Dynamic city comparison with instant updates
const CityDetailPage = ({ params }: { params: { city: string; activity: string } }) => {
  const citySlug = decodeURIComponent(params.city);
  const city = useMemo(() => findCityBySlug(cities, citySlug), [citySlug]);
  
  useEffect(() => {
    if (!city) return;
    loadForecast();
  }, [city?.lat, city?.lon, params.activity]);
  
  // Real-time data loading with loading states
  async function loadForecast() {
    setLoading(true);
    const response = await fetch(`/api/city-forecast?${searchParams}`);
    const data = await response.json();
    setHours(data.hours || []);
    setLoading(false);
  }
};

Results and User Impact

The Activities Advisor has proven valuable for UK outdoor activity planning:

Key Metrics

  • 18 activities covering major outdoor pursuits
  • 50+ UK cities with precise coordinate-based weather data
  • 7-day forecasts with hourly granularity
  • Historical analysis using decades of ERA5 climate data
  • Value optimization considering travel costs and group sizes

User Benefits

  • Timing optimization: Find the best 2-hour windows for activities
  • Location comparison: Compare cities side-by-side for weekend trips
  • Seasonal planning: Understand monthly patterns for long-term planning
  • Cost awareness: Factor in travel costs when choosing destinations
  • Risk reduction: Avoid planning outdoor activities during poor weather

Future Enhancements

V2.0 Planned Features

  • Weather alerts: Push notifications for optimal conditions
  • User preferences: Personalized activity recommendations
  • Social features: Share plans and recommendations
  • Extended forecasts: 14-day outlook for longer planning
  • Activity clustering: Group activities by location and timing

Advanced Analytics

  • Machine learning predictions: Improve accuracy using historical patterns
  • Microclimate modeling: Account for local geography effects
  • Activity correlation: Suggest complementary activities
  • Crowd avoidance: Factor in popular times and locations

Lessons Learned

  1. Weather data is complex: Different activities need different weather parameters and thresholds.

  2. User experience matters for data: Complex weather data needs intuitive visualization to be actionable.

  3. UK weather is genuinely challenging: Even sophisticated algorithms can't always find perfect conditions.

  4. Historical context helps: Seasonality analysis provides valuable long-term perspective.

  5. Value optimization changes behavior: Including costs makes users reconsider travel distances.

Conclusion

The Weather-Based Activities Advisor demonstrates how weather data can be transformed into actionable insights for outdoor activity planning. By combining real-time forecasts, historical climate analysis, and activity-specific scoring, the platform helps users make informed decisions about when and where to pursue outdoor activities in the UK's challenging climate.

The most satisfying aspect was seeing how the integrated approach—combining live leaderboards, seasonality analysis, and value optimization—provides a comprehensive solution to a common problem. As weather prediction continues to improve and user needs evolve, I'm excited to enhance the platform with more advanced features and broader coverage.


Note: Weather forecasts are inherently uncertain. This tool provides data-driven recommendations but cannot guarantee perfect weather conditions. Always check current conditions before heading outdoors and have backup plans for variable UK weather.