Free Weather Data with National Weather Service API
About This Project
This is part of my ongoing exploration of browser-native development with Scittle. In my previous articles on Building Browser-Native Presentations and Python + ClojureScript Integration, I’ve been sharing how to create interactive, educational content without build tools.
Today, I want to show you something practical: building weather applications using the National Weather Service API. What makes this special? The NWS API is:
- Completely free - No API keys, no registration, no rate limits
- Official and accurate - Data directly …
Free Weather Data with National Weather Service API
About This Project
This is part of my ongoing exploration of browser-native development with Scittle. In my previous articles on Building Browser-Native Presentations and Python + ClojureScript Integration, I’ve been sharing how to create interactive, educational content without build tools.
Today, I want to show you something practical: building weather applications using the National Weather Service API. What makes this special? The NWS API is:
- Completely free - No API keys, no registration, no rate limits
- Official and accurate - Data directly from the U.S. government
- Comprehensive - Current conditions, forecasts, alerts, and more
- Well-documented - Clear endpoints and data structures
But here’s the real magic: we’ll build everything with Scittle - meaning zero build tools, zero configuration, just ClojureScript running directly in your browser. And as a bonus, all our functions will use keyword arguments for clarity and ease of use.
Whether you’re learning ClojureScript, exploring APIs, or building weather apps, this guide will show you how to do it the simple way. Let’s dive in!
Why the National Weather Service API?
Before we dive into code, let’s understand why the NWS API is such a great choice:
Free and Accessible
Most weather APIs require: - Signing up for an account - Managing API keys - Dealing with rate limits - Paying for more requests
The NWS API requires none of that. Just make HTTP requests and get data. Perfect for learning, prototyping, or building personal projects.
Official Government Data
The data comes directly from NOAA (National Oceanic and Atmospheric Administration), the same source that powers weather forecasts across the United States. You’re getting:
- Real-time observations from weather stations
- Professional meteorological forecasts
- Severe weather alerts and warnings
- Historical weather data
Rich Feature Set
The API provides:
- Points API - Convert coordinates to forecast zones
- Forecast API - 7-day forecasts with detailed periods
- Hourly Forecasts - Hour-by-hour predictions
- Current Observations - Real-time weather station data
- Weather Alerts - Watches, warnings, and advisories
- Grid Data - Raw forecast model output
Educational Value
The API’s design teaches important concepts:
- RESTful API architecture
- Coordinate-based services
- Asynchronous data fetching
- Error handling in real-world scenarios
Understanding the API Architecture
The NWS API uses a two-step process to get weather data:
graph LR A[Latitude/Longitude] –> B[Points API] B –> C[Forecast URLs] C –> D[Forecast Data] C –> E[Hourly Data] C –> F[Grid Data] C –> G[Stations] G –> H[Current Observations]
Step 1: Get the Points
First, you query the /points/{lat},{lon} endpoint with your coordinates. This returns URLs for various forecast products specific to that location.
Step 2: Fetch the Data
Use the returned URLs to fetch the specific weather data you need: forecasts, hourly predictions, current conditions, etc.
This design is smart because: - Forecasts are generated for grid points, not exact coordinates - It allows the API to scale efficiently - You get all relevant endpoints in one initial request
Why Keyword Arguments?
Throughout this article, you’ll notice all our functions use keyword arguments instead of positional arguments. Here’s why:
(kind/code
";; Traditional positional arguments
(fetch-points 40.7128 -74.0060
(fn [result]
(if (:success result)
(handle-success (:data result))
(handle-error (:error result)))))
;; Keyword arguments style
(fetch-points
{:lat 40.7128
:lon -74.0060
:on-success handle-success
:on-error handle-error})")
;; Traditional positional arguments
(fetch-points 40.7128 -74.0060
(fn [result]
(if (:success result)
(handle-success (:data result))
(handle-error (:error result)))))
;; Keyword arguments style
(fetch-points
{:lat 40.7128
:lon -74.0060
:on-success handle-success
:on-error handle-error})
Benefits of keyword arguments:
- Self-documenting - Clear what each value represents
- Flexible - Order doesn’t matter
- Optional parameters - Easy to add defaults
- Extensible - Add new options without breaking existing code
- Beginner-friendly - Easier to understand and use
This is especially valuable when teaching or sharing code examples.
Setting Up: What We Need
For this tutorial, you need absolutely nothing installed locally! We’ll use:
- Scittle - ClojureScript interpreter (from CDN)
- Reagent - React wrapper for ClojureScript (from CDN)
- NWS API - Free weather data (no key needed)
- Your browser - That’s it!
All demos will be embedded right in this article. You can: - Run them immediately - View the source code - Copy and adapt for your own projects
Coming Up
In the following sections, we’ll build progressively complex examples:
- Simple Weather Lookup - Basic API call and display
- Current Conditions - Detailed weather information
- 7-Day Forecast - Visual forecast cards
- Hourly Forecast - Time-based predictions
- Weather Alerts - Severe weather warnings
- Complete Dashboard - Full-featured weather app
Each example will be fully functional and ready to use. Let’s get started!
Ready to build? Let’s start with the core API layer in the next section.
The API Layer
Before we build demos, let’s look at our API functions. We’ve created a complete API layer that uses keyword arguments for all functions.
(ns scittle.weather.weather-api
"National Weather Service API integration with keyword arguments.
All functions use keyword argument maps for clarity and flexibility.
No API key required - completely free!
Main API Reference: https://www.weather.gov/documentation/services-web-api"
(:require [clojure.string :as str]))
;; ============================================================================
;; Constants
;; ============================================================================
(def weather-api-base-url
"Base URL for NWS API"
"https://api.weather.gov")
;; ============================================================================
;; Core Helper Functions
;; ============================================================================
(defn fetch-json
"Fetch JSON data from a URL with error handling.
Uses browser's native fetch API - no external dependencies.
Args (keyword map):
:url - URL to fetch (string, required)
:on-success - Success callback receiving parsed data (fn, required)
:on-error - Error callback receiving error message (fn, optional)
The success callback receives the parsed JSON as a Clojure map.
The error callback receives an error message string.
Example:
(fetch-json
{:url \"https://api.weather.gov/points/40,-74\"
:on-success #(js/console.log \"Data:\" %)
:on-error #(js/console.error \"Failed:\" %)})"
[{:keys [url on-success on-error]
:or {on-error #(js/console.error "Fetch error:" %)}}]
(-> (js/fetch url)
(.then (fn [response]
(if (.-ok response)
(.then (.json response)
(fn [data]
(on-success (js->clj data :keywordize-keys true))))
(on-error (str "HTTP Error: " (.-status response))))))
(.catch (fn [error]
(on-error (.-message error))))))
;; ============================================================================
;; Points API - Get forecast endpoints for coordinates
;; ============================================================================
(defn fetch-points
"Get NWS grid points and forecast URLs for given coordinates.
This is typically the first API call - it returns URLs for all weather
products available at the given location.
Args (keyword map):
:lat - Latitude (number, required, range: -90 to 90)
:lon - Longitude (number, required, range: -180 to 180)
:on-success - Success callback receiving location data (fn, required)
:on-error - Error callback receiving error message (fn, optional)
Success callback receives a map with:
:forecast - URL for 7-day forecast
:forecastHourly - URL for hourly forecast
:forecastGridData - URL for raw grid data
:observationStations - URL for nearby weather stations
:forecastOffice - NWS office identifier
:gridId - Grid identifier
:gridX, :gridY - Grid coordinates
:city, :state - Location name
Example:
(fetch-points
{:lat 40.7128
:lon -74.0060
:on-success (fn [data]
(js/console.log \"City:\" (:city data))
(fetch-forecast {:url (:forecast data) ...}))
:on-error (fn [error]
(js/console.error \"Error:\" error))})"
[{:keys [lat lon on-success on-error]
:or {on-error #(js/console.error "Points API error:" %)}}]
(let [url (str weather-api-base-url "/points/" lat "," lon)]
(fetch-json
{:url url
:on-success (fn [result]
(let [properties (get-in result [:properties])]
(on-success
{:forecast (:forecast properties)
:forecastHourly (:forecastHourly properties)
:forecastGridData (:forecastGridData properties)
:observationStations (:observationStations properties)
:forecastOffice (:forecastOffice properties)
:gridId (:gridId properties)
:gridX (:gridX properties)
:gridY (:gridY properties)
:city (:city (get-in result [:properties :relativeLocation :properties]))
:state (:state (get-in result [:properties :relativeLocation :properties]))})))
:on-error on-error})))
;; ============================================================================
;; Forecast API - Get 7-day forecast
;; ============================================================================
(defn fetch-forecast
"Fetch 7-day forecast from a forecast URL.
Returns periods (typically 14 periods: day/night for 7 days).
Args (keyword map):
:url - Forecast URL from points API (string, required)
:on-success - Success callback receiving forecast periods (fn, required)
:on-error - Error callback receiving error message (fn, optional)
Success callback receives a vector of period maps, each containing:
:number - Period number
:name - Period name (e.g., \"Tonight\", \"Friday\")
:temperature - Temperature value
:temperatureUnit - Unit (F or C)
:windSpeed - Wind speed string
:windDirection - Wind direction
:icon - Weather icon URL
:shortForecast - Brief description
:detailedForecast - Detailed description
Example:
(fetch-forecast
{:url forecast-url
:on-success (fn [periods]
(doseq [period (take 3 periods)]
(js/console.log (:name period) \"-\" (:shortForecast period))))
:on-error (fn [error]
(js/console.error \"Forecast error:\" error))})"
[{:keys [url on-success on-error]
:or {on-error #(js/console.error "Forecast API error:" %)}}]
(fetch-json
{:url url
:on-success (fn [result]
(on-success (get-in result [:properties :periods])))
:on-error on-error}))
;; ============================================================================
;; Hourly Forecast API
;; ============================================================================
(defn fetch-hourly-forecast
"Fetch hourly forecast from a forecast URL.
Args (keyword map):
:url - Hourly forecast URL from points API (string, required)
:on-success - Success callback receiving hourly periods (fn, required)
:on-error - Error callback receiving error message (fn, optional)
Success callback receives a vector of hourly period maps.
Each period has the same structure as the regular forecast.
Example:
(fetch-hourly-forecast
{:url hourly-url
:on-success (fn [periods]
(js/console.log \"Next 12 hours:\")
(doseq [period (take 12 periods)]
(js/console.log (:startTime period) \"-\" (:temperature period) \"°F\")))
:on-error (fn [error]
(js/console.error \"Hourly forecast error:\" error))})"
[{:keys [url on-success on-error]
:or {on-error #(js/console.error "Hourly forecast API error:" %)}}]
(fetch-json
{:url url
:on-success (fn [result]
(on-success (get-in result [:properties :periods])))
:on-error on-error}))
;; ============================================================================
;; Observation Stations API
;; ============================================================================
(defn fetch-observation-stations
"Get list of observation stations near a location.
Args (keyword map):
:url - Observation stations URL from points API (string, required)
:on-success - Success callback receiving station list (fn, required)
:on-error - Error callback receiving error message (fn, optional)
Success callback receives a vector of station maps, each containing:
:stationIdentifier - Station ID (e.g., \"KJFK\")
:name - Station name
:elevation - Elevation data
Example:
(fetch-observation-stations
{:url stations-url
:on-success (fn [stations]
(let [first-station (first stations)]
(js/console.log \"Nearest station:\" (:name first-station))
(fetch-current-observations
{:station-id (:stationIdentifier first-station)
:on-success ...})))
:on-error (fn [error]
(js/console.error \"Stations error:\" error))})"
[{:keys [url on-success on-error]
:or {on-error #(js/console.error "Observation stations API error:" %)}}]
(fetch-json
{:url url
:on-success (fn [result]
(on-success
(map #(get-in % [:properties])
(get-in result [:features]))))
:on-error on-error}))
;; ============================================================================
;; Current Observations API
;; ============================================================================
(defn fetch-current-observations
"Get current weather observations from a station.
Args (keyword map):
:station-id - Station identifier (string, required, e.g., \"KJFK\")
:on-success - Success callback receiving observation data (fn, required)
:on-error - Error callback receiving error message (fn, optional)
Success callback receives a map with current conditions:
:temperature - Current temperature with :value and :unitCode
:dewpoint - Dewpoint temperature
:windDirection - Wind direction in degrees
:windSpeed - Wind speed
:barometricPressure - Pressure
:relativeHumidity - Humidity percentage
:visibility - Visibility distance
:textDescription - Weather description
:timestamp - Observation time
Example:
(fetch-current-observations
{:station-id \"KJFK\"
:on-success (fn [obs]
(js/console.log \"Temperature:\"
(get-in obs [:temperature :value]) \"°C\")
(js/console.log \"Conditions:\" (:textDescription obs)))
:on-error (fn [error]
(js/console.error \"Observations error:\" error))})"
[{:keys [station-id on-success on-error]
:or {on-error #(js/console.error "Current observations API error:" %)}}]
(let [url (str weather-api-base-url "/stations/" station-id "/observations/latest")]
(fetch-json
{:url url
:on-success (fn [result]
(on-success (get-in result [:properties])))
:on-error on-error})))
;; ============================================================================
;; Alerts API - Weather alerts
;; ============================================================================
(defn fetch-alerts-for-point
"Fetch active weather alerts for a specific location.
Args (keyword map):
:lat - Latitude (number, required)
:lon - Longitude (number, required)
:on-success - Success callback receiving alerts list (fn, required)
:on-error - Error callback receiving error message (fn, optional)
Success callback receives a vector of alert maps, each containing:
:event - Alert type (e.g., \"Tornado Warning\")
:headline - Brief headline
:description - Full alert description
:severity - Severity level (Extreme/Severe/Moderate/Minor)
:urgency - Urgency level
:certainty - Certainty level
:onset - Start time
:ends - End time
Example:
(fetch-alerts-for-point
{:lat 40.7128
:lon -74.0060
:on-success (fn [alerts]
(if (empty? alerts)
(js/console.log \"No active alerts\")
(doseq [alert alerts]
(js/console.log (:severity alert) \"-\" (:event alert)))))
:on-error (fn [error]
(js/console.error \"Alerts error:\" error))})"
[{:keys [lat lon on-success on-error]
:or {on-error #(js/console.error "Alerts API error:" %)}}]
(let [url (str weather-api-base-url "/alerts/active?point=" lat "," lon)]
(fetch-json
{:url url
:on-success (fn [result]
(on-success
(map #(get-in % [:properties])
(get-in result [:features]))))
:on-error on-error})))
;; ============================================================================
;; Complete Weather Data - Convenience function
;; ============================================================================
(defn fetch-complete-weather
"Fetch comprehensive weather data for given coordinates.
This is a convenience function that:
1. Fetches points data
2. Fetches 7-day forecast
3. Fetches hourly forecast
4. Fetches observation stations
5. Fetches current observations from nearest station
6. Fetches active alerts
All data is collected and returned in a single callback.
Args (keyword map):
:lat - Latitude (number, required)
:lon - Longitude (number, required)
:on-success - Success callback receiving complete weather data (fn, required)
:on-error - Error callback receiving error message (fn, optional)
Success callback receives a map with:
:points - Location and grid information
:forecast - 7-day forecast periods
:hourly - Hourly forecast periods
:stations - Nearby weather stations
:current - Current observations
:alerts - Active weather alerts
Example:
(fetch-complete-weather
{:lat 40.7128
:lon -74.0060
:on-success (fn [weather]
(js/console.log \"Location:\"
(get-in weather [:points :city]))
(js/console.log \"Current temp:\"
(get-in weather [:current :temperature :value]))
(js/console.log \"Forecast periods:\"
(count (:forecast weather))))
:on-error (fn [error]
(js/console.error \"Complete weather error:\" error))})"
[{:keys [lat lon on-success on-error]
:or {on-error #(js/console.error "Complete weather error:" %)}}]
(let [results (atom {})]
;; First get the points data
(fetch-points
{:lat lat
:lon lon
:on-success
(fn [points-data]
(swap! results assoc :points points-data)
;; Fetch 7-day forecast
(when (:forecast points-data)
(fetch-forecast
{:url (:forecast points-data)
:on-success (fn [forecast-data]
(swap! results assoc :forecast forecast-data))}))
;; Fetch hourly forecast
(when (:forecastHourly points-data)
(fetch-hourly-forecast
{:url (:forecastHourly points-data)
:on-success (fn [hourly-data]
(swap! results assoc :hourly hourly-data))}))
;; Fetch observation stations and current observations
(when (:observationStations points-data)
(fetch-observation-stations
{:url (:observationStations points-data)
:on-success
(fn [stations-data]
(swap! results assoc :stations stations-data)
;; Get current observations from first station
(when-let [station-id (:stationIdentifier (first stations-data))]
(fetch-current-observations
{:station-id station-id
:on-success (fn [obs-data]
(swap! results assoc :current obs-data)
;; Return all collected data
(on-success @results))})))}))
;; Fetch alerts for this point
(fetch-alerts-for-point
{:lat lat
:lon lon
:on-success (fn [alerts-data]
(swap! results assoc :alerts alerts-data))}))
:on-error on-error})))
;; ============================================================================
;; Utility Functions
;; ============================================================================
(defn get-weather-icon
"Map NWS icon URLs to emoji representations.
Args:
icon-url - NWS icon URL (string)
Returns:
Emoji string representing the weather condition.
Example:
(get-weather-icon \"https://api.weather.gov/icons/land/day/rain\")
;; => \"🌧️\""
[icon-url]
(when icon-url
(let [icon-name (-> icon-url
(str/split #"/")
last
(str/split #"\?")
first
(str/replace #"\..*$" ""))]
(cond
(str/includes? icon-name "skc") "☀️" ; Clear
(str/includes? icon-name "few") "🌤️" ; Few clouds
(str/includes? icon-name "sct") "⛅" ; Scattered clouds
(str/includes? icon-name "bkn") "🌥️" ; Broken clouds
(str/includes? icon-name "ovc") "☁️" ; Overcast
(str/includes? icon-name "rain") "🌧️" ; Rain
(str/includes? icon-name "snow") "❄️" ; Snow
(str/includes? icon-name "tsra") "⛈️" ; Thunderstorm
(str/includes? icon-name "fog") "🌫️" ; Fog
(str/includes? icon-name "wind") "💨" ; Windy
(str/includes? icon-name "hot") "🌡️" ; Hot
(str/includes? icon-name "cold") "🥶" ; Cold
:else "🌡️")))) ; Default
This API layer provides all the functions we need:
fetch-points- Convert coordinates to NWS grid pointsfetch-forecast- Get 7-day forecastfetch-hourly-forecast- Get hourly predictionsfetch-current-observations- Real-time weather datafetch-alerts-for-point- Active weather alertsfetch-complete-weather- Convenience function for all data
Notice how every function follows the same pattern: a single keyword argument map with :on-success and :on-error callbacks. This makes the code self-documenting and easy to use.
Demo 1: Simple Weather Lookup
Let’s start with the simplest possible weather app. This demo:
- Accepts latitude and longitude input
- Fetches weather data using the NWS API
- Displays location, temperature, and forecast
- Includes quick-access buttons for major cities
- Shows loading and error states
Key features:
- Uses keyword arguments:
{:lat 40.7128 :lon -74.0060 :on-success ... :on-error ...} - Native browser fetch (no external libraries)
- Simple, clean Reagent components
- Inline styles (no CSS files needed)
(ns scittle.weather.simple-lookup
"Simple weather lookup demo - minimal example showing basic API usage.
This is the simplest possible weather app:
- Two input fields for coordinates
- One button to fetch weather
- Display location and temperature
Demonstrates:
- Basic API call with keyword arguments
- Loading state
- Error handling
- Minimal Reagent UI"
(:require [reagent.core :as r]
[reagent.dom :as rdom]
[clojure.string :as str]))
;; ============================================================================
;; Inline API Functions (simplified for this demo)
;; ============================================================================
(defn fetch-json
"Fetch JSON from URL with error handling."
[{:keys [url on-success on-error]}]
(-> (js/fetch url)
(.then (fn [response]
(if (.-ok response)
(.then (.json response)
(fn [data]
(on-success (js->clj data :keywordize-keys true))))
(on-error (str "HTTP Error: " (.-status response))))))
(.catch (fn [error]
(on-error (.-message error))))))
(defn fetch-weather-data
"Fetch basic weather data for coordinates."
[{:keys [lat lon on-success on-error]}]
(let [points-url (str "https://api.weather.gov/points/" lat "," lon)]
(fetch-json
{:url points-url
:on-success
(fn [points-result]
;; Got points, now get forecast
(let [properties (get-in points-result [:properties])
forecast-url (:forecast properties)
city (:city (get-in points-result [:properties :relativeLocation :properties]))
state (:state (get-in points-result [:properties :relativeLocation :properties]))]
;; Fetch the forecast
(fetch-json
{:url forecast-url
:on-success
(fn [forecast-result]
(let [periods (get-in forecast-result [:properties :periods])
first-period (first periods)]
(on-success
{:city city
:state state
:temperature (:temperature first-period)
:temperatureUnit (:temperatureUnit first-period)
:shortForecast (:shortForecast first-period)
:detailedForecast (:detailedForecast first-period)})))
:on-error on-error})))
:on-error on-error})))
;; ============================================================================
;; Styles
;; ============================================================================
(def card-style
{:background "#ffffff"
:border "1px solid #e0e0e0"
:border-radius "8px"
:padding "20px"
:box-shadow "0 2px 4px rgba(0,0,0,0.1)"
:margin-bottom "20px"})
(def input-style
{:width "100%"
:padding "10px"
:border "1px solid #ddd"
:border-radius "4px"
:font-size "14px"
:margin-bottom "10px"})
(def button-style
{:background "#2196f3"
:color "white"
:border "none"
:padding "12px 24px"
:border-radius "4px"
:cursor "pointer"
:font-size "16px"
:width "100%"
:transition "background 0.3s"})
(def button-hover-style
(merge button-style
{:background "#1976d2"}))
(def button-disabled-style
(merge button-style
{:background "#ccc"
:cursor "not-allowed"}))
;; ============================================================================
;; Components
;; ============================================================================
(defn loading-spinner
"Simple loading indicator."
[]
[:div {:style {:text-align "center"
:padding "40px"}}
[:div {:style {:display "inline-block"
:width "40px"
:height "40px"
:border "4px solid #f3f3f3"
:border-top "4px solid #2196f3"
:border-radius "50%"
:animation "spin 1s linear infinite"}}]
[:style "@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }"]
[:p {:style {:margin-top "15px"
:color "#666"}}
"Fetching weather data..."]])
(defn error-display
"Display error message."
[{:keys [error on-retry]}]
[:div {:style (merge card-style
{:background "#ffebee"
:border "1px solid #ef5350"})}
[:h4 {:style {:margin-top 0
:color "#c62828"}}
"⚠️ Error"]
[:p {:style {:color "#666"}}
error]
(when on-retry
[:button {:on-click on-retry
:style {:background "#f44336"
:color "white"
:border "none"
:padding "8px 16px"
:border-radius "4px"
:cursor "pointer"
:margin-top "10px"}}
"Try Again"])])
(defn weather-result
"Display weather results."
[{:keys [data]}]
[:div {:style card-style}
[:h2 {:style {:margin-top 0
:color "#2196f3"}}
"📍 " (:city data) ", " (:state data)]
[:div {:style {:text-align "center"
:margin "30px 0"}}
[:div {:style {:font-size "48px"
:font-weight "bold"
:color "#333"}}
(:temperature data) "°" (:temperatureUnit data)]
[:div {:style {:font-size "18px"
:color "#666"
:margin-top "10px"}}
(:shortForecast data)]]
[:div {:style {:background "#f5f5f5"
:padding "15px"
:border-radius "4px"
:margin-top "20px"}}
[:p {:style {:margin 0
:line-height 1.6
:color "#555"}}
(:detailedForecast data)]]])
(defn input-form
"Input form for coordinates."
[{:keys [lat lon on-lat-change on-lon-change on-submit loading? disabled?]}]
[:div {:style card-style}
[:h3 {:style {:margin-top 0}}
"🌍 Enter Coordinates"]
[:p {:style {:color "#666"
:font-size "14px"
:margin-bottom "15px"}}
"Enter latitude and longitude to get weather data"]
[:div
[:label {:style {:display "block"
:margin-bottom "5px"
:color "#555"
:font-weight "500"}}
"Latitude"]
[:input {:type "number"
:step "0.0001"
:placeholder "e.g., 40.7128"
:value @lat
:on-change #(on-lat-change (.. % -target -value))
:disabled loading?
:style (merge input-style
(when loading? {:opacity 0.6}))}]]
[:div
[:label {:style {:display "block"
:margin-bottom "5px"
:color "#555"
:font-weight "500"}}
"Longitude"]
[:input {:type "number"
:step "0.0001"
:placeholder "e.g., -74.0060"
:value @lon
:on-change #(on-lon-change (.. % -target -value))
:disabled loading?
:style (merge input-style
(when loading? {:opacity 0.6}))}]]
[:button {:on-click on-submit
:disabled (or loading? disabled?)
:style (cond
loading? button-disabled-style
disabled? button-disabled-style
:else button-style)}
(if loading?
"Loading..."
"Get Weather")]])
(defn quick-locations
"Quick access buttons for major cities."
[{:keys [on-select loading?]}]
[:div {:style {:margin-top "20px"}}
[:p {:style {:color "#666"
:font-size "14px"
:margin-bottom "10px"}}
"Or try these cities:"]
[:div {:style {:display "flex"
:flex-wrap "wrap"
:gap "8px"}}
(for [[city lat lon] [["Charlotte, NC" 35.2271 -80.8431]
["Miami, FL" 25.7617 -80.1918]
["Denver, CO" 39.7392 -104.9903]
["New York, NY" 40.7128 -74.0060]
["Los Angeles, CA" 34.0522 -118.2437]
["Chicago, IL" 41.8781 -87.6298]]]
^{:key city}
[:button {:on-click #(on-select lat lon)
:disabled loading?
:style {:padding "6px 12px"
:background (if loading? "#ccc" "transparent")
:color (if loading? "#999" "#2196f3")
:border "1px solid #2196f3"
:border-radius "20px"
:cursor (if loading? "not-allowed" "pointer")
:font-size "13px"
:transition "all 0.2s"}}
city])]])
;; ============================================================================
;; Main Component
;; ============================================================================
(defn main-component
"Main weather lookup component."
[]
(let [lat (r/atom "35.2271")
lon (r/atom "-80.8431")
loading? (r/atom false)
error (r/atom nil)
weather-data (r/atom nil)
fetch-weather (fn [latitude longitude]
(reset! loading? true)
(reset! error nil)
(reset! weather-data nil)
(fetch-weather-data
{:lat latitude
:lon longitude
:on-success (fn [data]
(reset! loading? false)
(reset! weather-data data))
:on-error (fn [err]
(reset! loading? false)
(reset! error err))}))]
(fn []
[:div {:style {:max-width "600px"
:margin "0 auto"
:padding "20px"
:font-family "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"}}
[:h1 {:style {:text-align "center"
:color "#333"
:margin-bottom "30px"}}
"☀️ Simple Weather Lookup"]
;; Input Form
[input-form
{:lat lat
:lon lon
:on-lat-change #(reset! lat %)
:on-lon-change #(reset! lon %)
:on-submit #(when (and (not (str/blank? @lat))
(not (str/blank? @lon)))
(fetch-weather @lat @lon))
:loading? @loading?
:disabled? (or (str/blank? @lat)
(str/blank? @lon))}]
;; Quick Locations
[quick-locations
{:on-select (fn [latitude longitude]
(reset! lat (str latitude))
(reset! lon (str longitude))
(fetch-weather latitude longitude))
:loading? @loading?}]
;; Loading State
(when @loading?
[loading-spinner])
;; Error Display
(when @error
[error-display
{:error @error
:on-retry #(when (and (not (str/blank? @lat))
(not (str/blank? @lon)))
(fetch-weather @lat @lon))}])
;; Weather Results
(when @weather-data
[weather-result {:data @weather-data}])
;; Instructions
(when (and (not @loading?)
(not @error)
(not @weather-data))
[:div {:style {:text-align "center"
:margin-top "40px"
:color "#999"
:font-size "14px"}}
[:p "Enter coordinates above or click a city to get started"]
[:p {:style {:margin-top "10px"}}
"Uses the free NWS API - no API key required!"]])])))
;; ============================================================================
;; Mount
;; ============================================================================
(defn ^:export init []
(rdom/render [main-component]
(js/document.getElementById "simple-lookup-demo")))
;; Auto-initialize when script loads
(init)
Try It Live
Click a city button or enter coordinates to see it in action!
(kind/hiccup
[:div#simple-lookup-demo {:style {:min-height "500px"}}
[:script {:type "application/x-scittle"
:src "simple_lookup.cljs"}]])
Demo 2: Current Weather Conditions
Now let’s build something more detailed. This demo shows comprehensive current conditions with all available metrics from the weather station.
What makes this demo powerful:
- Temperature unit conversion - Toggle between Fahrenheit, Celsius, and Kelvin
- Complete metrics grid - Humidity, wind, pressure, visibility, dewpoint
- Conditional data - Heat index and wind chill when applicable
- Station information - See which weather station is reporting
- Large display format - Beautiful, scannable layout
Technical features:
- Two-step API process: coordinates → station → observations
- Helper functions for unit conversion (C→F, C→K)
- Wind direction formatting (degrees → compass direction)
- Distance conversion (meters → miles)
- Responsive grid layout
The data flow:
graph LR A[Coordinates] –> B[Get Station] B –> C[Station ID] C –> D[Latest Observations] D –> E[Display All Metrics]
(ns scittle.weather.current-conditions
"Display detailed current weather conditions with all available metrics.
Demonstrates unit conversion, comprehensive data display, and state management."
(:require [reagent.core :as r]
[reagent.dom :as rdom]
[clojure.string :as str]))
;; ============================================================================
;; API Functions (Inline from weather_api.cljs)
;; ============================================================================
(defn fetch-json
"Fetch JSON from URL with error handling."
[{:keys [url on-success on-error]
:or {on-error #(js/console.error "Fetch error:" %)}}]
(-> (js/fetch url)
(.then (fn [response]
(if (.-ok response)
(.json response)
(throw (js/Error. (str "HTTP " (.-status response)))))))
(.then (fn [data]
(on-success (js->clj data :keywordize-keys true))))
(.catch on-error)))
(defn get-weather-station
"Get nearest weather station for coordinates."
[{:keys [lat lon on-success on-error]}]
(fetch-json
{:url (str "https://api.weather.gov/points/" lat "," lon)
:on-success (fn [data]
(let [station-url (get-in data [:properties :observationStations])]
(fetch-json
{:url station-url
:on-success (fn [stations]
(let [station-id (-> stations
:features
first
:properties
:stationIdentifier)]
(on-success station-id)))
:on-error on-error})))
:on-error on-error}))
(defn get-latest-observations
"Get latest observations from station."
[{:keys [station-id on-success on-error]}]
(fetch-json
{:url (str "https://api.weather.gov/stations/" station-id "/observations/latest")
:on-success on-success
:on-error on-error}))
;; ============================================================================
;; Temperature Conversion Utilities
;; ============================================================================
(defn celsius-to-fahrenheit [c]
"Convert Celsius to Fahrenheit"
(when c (+ (* c 1.8) 32)))
(defn celsius-to-kelvin [c]
"Convert Celsius to Kelvin"
(when c (+ c 273.15)))
(defn format-temp
"Format temperature based on unit (F, C, or K)"
[celsius unit]
(when celsius
(case unit
"F" (str (Math/round (celsius-to-fahrenheit celsius)) "°F")
"C" (str (Math/round celsius) "°C")
"K" (str (Math/round (celsius-to-kelvin celsius)) "K")
(str (Math/round celsius) "°C"))))
;; ============================================================================
;; Wind & Distance Utilities
;; ============================================================================
(defn format-wind-direction
"Convert degrees to compass direction"
[degrees]
(when degrees
(let [directions ["N" "NNE" "NE" "ENE" "E" "ESE" "SE" "SSE"
"S" "SSW" "SW" "WSW" "W" "WNW" "NW" "NNW"]
index (mod (Math/round (/ degrees 22.5)) 16)]
(nth directions index))))
(defn meters-to-miles [m]
"Convert meters to miles"
(when m (* m 0.000621371)))
(defn mps-to-mph [mps]
"Convert meters per second to miles per hour"
(when mps (* mps 2.237)))
;; ============================================================================
;; Styles
;; ============================================================================
(def container-style
{:max-width "800px"
:margin "0 auto"
:padding "20px"
:font-family "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"})
(def card-style
{:background "#ffffff"
:border "1px solid #e0e0e0"
:border-radius "12px"
:padding "24px"
:margin-bottom "20px"
:box-shadow "0 2px 8px rgba(0,0,0,0.1)"})
(def header-style
{:text-align "center"
:margin-bottom "30px"})
(def title-style
{:font-size "28px"
:font-weight "600"
:color "#1a1a1a"
:margin "0 0 10px 0"})
(def subtitle-style
{:font-size "14px"
:color "#666"
:margin 0})
(def input-group-style
{:display "flex"
:gap "10px"
:margin-bottom "20px"
:flex-wrap "wrap"})
(def input-style
{:padding "10px 15px"
:border "1px solid #ddd"
:border-radius "6px"
:font-size "14px"
:flex "1"
:min-width "120px"})
(def button-style
{:padding "10px 20px"
:background "#3b82f6"
:color "white"
:border "none"
:border-radius "6px"
:cursor "pointer"
:font-size "14px"
:font-weight "500"
:transition "background 0.2s"})
(def button-hover-style
(merge button-style {:background "#2563eb"}))
(def quick-buttons-style
{:display "flex"
:gap "8px"
:flex-wrap "wrap"
:margin-bottom "20px"})
(def quick-button-style
{:padding "8px 16px"
:background "#f3f4f6"
:border "1px solid #e5e7eb"
:border-radius "6px"
:cursor "pointer"
:font-size "13px"
:transition "all 0.2s"})
(def temp-display-style
{:text-align "center"
:padding "30px 0"
:border-bottom "1px solid #e0e0e0"})
(def large-temp-style
{:font-size "72px"
:font-weight "300"
:color "#1a1a1a"
:margin "0"
:line-height "1"})
(def condition-text-style
{:font-size "20px"
:color "#4b5563"
:margin "10px 0 20px 0"})
(def unit-toggle-style
{:display "flex"
:justify-content "center"
:gap "8px"
:margin-top "15px"})
(def unit-button-style
{:padding "6px 16px"
:border "1px solid #ddd"
:border-radius "6px"
:background "#fff"
:cursor "pointer"
:font-size "14px"
:transition "all 0.2s"})
(def unit-button-active-style
(merge unit-button-style
{:background "#3b82f6"
:color "white"
:border-color "#3b82f6"}))
(def metrics-grid-style
{:display "grid"
:grid-template-columns "repeat(auto-fit, minmax(200px, 1fr))"
:gap "20px"
:padding "20px 0"})
(def metric-card-style
{:padding "15px"
:background "#f9fafb"
:border-radius "8px"
:border "1px solid #e5e7eb"})
(def metric-label-style
{:font-size "13px"
:color "#6b7280"
:margin "0 0 5px 0"
:font-weight "500"
:text-transform "uppercase"
:letter-spacing "0.5px"})
(def metric-value-style
{:font-size "24px"
:color "#1a1a1a"
:margin "0"
:font-weight "500"})
(def station-info-style
{:margin-top "20px"
:padding-top "20px"
:border-top "1px solid #e0e0e0"
:font-size "13px"
:color "#6b7280"
:text-align "center"})
(def loading-style
{:text-align "center"
:padding "40px"
:color "#6b7280"})
(def error-style
{:background "#fef2f2"
:border "1px solid #fecaca"
:color "#dc2626"
:padding "12px"
:border-radius "6px"
:margin "10px 0"})
;; ============================================================================
;; Quick City Locations
;; ============================================================================
(def quick-cities
[{:name "Charlotte, NC" :lat 35.2271 :lon -80.8431}
{:name "Miami, FL" :lat 25.7617 :lon -80.1918}
{:name "Denver, CO" :lat 39.7392 :lon -104.9903}
{:name "Seattle, WA" :lat 47.6062 :lon -122.3321}
{:name "New York, NY" :lat 40.7128 :lon -74.0060}
{:name "Los Angeles, CA" :lat 34.0522 :lon -118.2437}
{:name "Chicago, IL" :lat 41.8781 :lon -87.6298}])
;; ============================================================================
;; Components
;; ============================================================================
(defn loading-spinner []
[:div {:style loading-style}
[:div {:style {:font-size "40px" :margin-bottom "10px"}} "⏳"]
[:div "Loading current conditions..."]])
(defn error-message [{:keys [message]}]
[:div {:style error-style}
[:strong "Error: "] message])
(defn unit-toggle-buttons [{:keys [current-unit on-change]}]
[:div {:style unit-toggle-style}
(for [unit ["F" "C" "K"]]
^{:key unit}
[:button
{:style (if (= unit current-unit)
unit-button-active-style
unit-button-style)
:on-click #(on-change unit)}
unit])])
(defn metric-card [{:keys [label value]}]
[:div {:style metric-card-style}
[:div {:style metric-label-style} label]
[:div {:style metric-value-style} (or value "—")]])
(defn temperature-display [{:keys [temp-c unit condition]}]
[:div {:style temp-display-style}
[:div {:style large-temp-style}
(format-temp temp-c unit)]
[:div {:style condition-text-style}
(or condition "No data")]])
(defn metrics-grid [{:keys [observations unit]}]
(let [props (:properties observations)
temp-c (:value (:temperature props))
dewpoint-c (:value (:dewpoint props))
wind-speed-mps (:value (:windSpeed props))
wind-dir (:value (:windDirection props))
humidity (:value (:relativeHumidity props))
pressure (:value (:barometricPressure props))
visibility-m (:value (:visibility props))
heat-index-c (:value (:heatIndex props))
wind-chill-c (:value (:windChill props))]
[:div {:style metrics-grid-style}
[metric-card
{:label "Humidity"
:value (when humidity (str (Math/round humidity) "%"))}]
[metric-card
{:label "Wind"
:value (when wind-speed-mps
(str (Math/round (mps-to-mph wind-speed-mps)) " mph "
(when wind-dir
(str "from " (format-wind-direction wind-dir)))))}]
[metric-card
{:label "Pressure"
:value (when pressure
(str (Math/round (/ pressure 100)) " mb"))}]
[metric-card
{:label "Visibility"
:value (when visibility-m
(let [miles (meters-to-miles visibility-m)]
(str (Math/round miles) " mi")))}]
[metric-card
{:label "Dewpoint"
:value (format-temp dewpoint-c unit)}]
(when heat-index-c
[metric-card
{:label "Heat Index"
:value (format-temp heat-index-c unit)}])
(when wind-chill-c
[metric-card
{:label "Wind Chill"
:value (format-temp wind-chill-c unit)}])]))
(defn station-info [{:keys [observations]}]
(let [props (:properties observations)
station (:station props)
timestamp (:timestamp props)
station-id (last (str/split station #"/"))]
[:div {:style station-info-style}
[:div "Station: " station-id]
[:div "Last Updated: "
(when timestamp
(.toLocaleString (js/Date. timestamp)))]]))
(defn weather-display [{:keys [observations unit on-unit-change]}]
(let [props (:properties observations)
temp-c (:value (:temperature props))
condition (:textDescription props)]
[:div {:style card-style}
[temperature-display
{:temp-c temp-c
:unit unit
:condition condition}]
[unit-toggle-buttons
{:current-unit unit
:on-change on-unit-change}]
[metrics-grid
{:observations observations
:unit unit}]
[station-info
{:observations observations}]]))
(defn location-input [{:keys [lat lon on-lat-change on-lon-change on-fetch]}]
[:div
[:div {:style input-group-style}
[:input {:type "number"
:placeholder "Latitude"
:value lat
:on-change #(on-lat-change (.. % -target -value))
:step "0.0001"
:style input-style}]
[:input {:type "number"
:placeholder "Longitude"
:value lon
:on-change #(on-lon-change (.. % -target -value))
:step "0.0001"
:style input-style}]
[:button {:on-click on-fetch
:style button-style
:on-mouse-over #(set! (.. % -target -style -background) "#2563eb")
:on-mouse-out #(set! (.. % -target -style -background) "#3b82f6")}
"Get Conditions"]]])
(defn quick-city-buttons [{:keys [cities on-select]}]
[:div {:style quick-buttons-style}
(for [city cities]
^{:key (:name city)}
[:button
{:style quick-button-style
:on-click #(on-select city)
:on-mouse-over #(set! (.. % -target -style -background) "#e5e7eb")
:on-mouse-out #(set! (.. % -target -style -background) "#f3f4f6")}
(:name city)])])
;; ============================================================================
;; Main Component
;; ============================================================================
(defn main-component []
(let [lat (r/atom "35.2271")
lon (r/atom "-80.8431")
observations (r/atom nil)
loading? (r/atom false)
error (r/atom nil)
unit (r/atom "F")
fetch-conditions
(fn []
(reset! loading? true)
(reset! error nil)
(reset! observations nil)
(get-weather-station
{:lat @lat
:lon @lon
:on-success
(fn [station-id]
(get-latest-observations
{:station-id station-id
:on-success
(fn [obs-data]
(reset! observations obs-data)
(reset! loading? false))
:on-error
(fn [err]
(reset! error (str "Failed to fetch observations: " err))
(reset! loading? false))}))
:on-error
(fn [err]
(reset! error (str "Failed to find weather station: " err))
(reset! loading? false))}))
select-city
(fn [city]
(reset! lat (str (:lat city)))
(reset! lon (str (:lon city)))
(fetch-conditions))]
(fn []
[:div {:style container-style}
[:div {:style header-style}
[:h1 {:style title-style} "☀️ Current Weather Conditions"]
[:p {:style subtitle-style}
"Detailed weather metrics from NOAA National Weather Service"]]
[:div {:style card-style}
[location-input
{:lat @lat
:lon @lon
:on-lat-change #(reset! lat %)
:on-lon-change #(reset! lon %)
:on-fetch fetch-conditions}]
[quick-city-buttons
{:cities quick-cities
:on-select select-city}]]
(cond
@loading? [loading-spinner]
@error [error-message {:message @error}]
@observations [weather-display
{:observations @observations
:unit @unit
:on-unit-change #(reset! unit %)}])])))
;; ============================================================================
;; Mount
;; ============================================================================
(defn ^:export init []
(when-let [el (js/document.getElementById "current-conditions-demo")]
(rdom/render [main-component] el)))
(init)
Try It Live
Click a city or enter coordinates, then use the F/C/K buttons to change units!
(kind/hiccup
[:div#current-conditions-demo {:style {:min-height "700px"}}
[:script {:type "application/x-scittle"
:src "current_conditions.cljs"}]])
Demo 3: 7-Day Forecast Viewer
Now we’re getting visual! This demo displays weather forecasts as beautiful cards in a responsive grid layout.
What makes this demo special:
- Visual weather cards - Each period gets its own card with emoji weather icons
- Smart icon mapping - Automatically selects emojis based on forecast text
- Flexible viewing - Toggle between 7 days or all 14 periods (day + night)
- Hover effects - Cards lift and shadow when you hover
- Responsive grid - Automatically adjusts to screen size
- Rich information - Temperature, conditions, wind, precipitation chance
Technical highlights:
- Weather icon mapping with regex pattern matching
- CSS Grid with
auto-fillfor responsive layout - Form-2 Reagent components for hover state
- Conditional rendering (precipitation only when > 0%)
- Toggle controls for view modes
What you’ll see on each card:
- Period name (Tonight, Friday, Saturday, etc.)
- Weather emoji (⛈️, 🌧️, ☀️, ⛅, etc.)
- Temperature (with F/C/K conversion)
- Short forecast text
- Wind information (speed and direction)
- Precipitation probability (when applicable)
(ns scittle.weather.forecast-viewer
"7-day weather forecast with visual cards and period toggles.
Demonstrates grid layouts, emoji icons, and responsive design."
(:require [reagent.core :as r]
[reagent.dom :as rdom]
[clojure.string :as str]))
;; ============================================================================
;; API Functions (Inline)
;; ============================================================================
(defn fetch-json
"Fetch JSON from URL with error handling."
[{:keys [url on-success on-error]
:or {on-error #(js/console.error "Fetch error:" %)}}]
(-> (js/fetch url)
(.then (fn [response]
(if (.-ok response)
(.json response)
(throw (js/Error. (str "HTTP " (.-status response)))))))
(.then (fn [data]
(on-success (js->clj data :keywordize-keys true))))
(.catch on-error)))
(defn get-forecast-url
"Get forecast URL for coordinates."
[{:keys [lat lon on-success on-error]}]
(fetch-json
{:url (str "https://api.weather.gov/points/" lat "," lon)
:on-success (fn [data]
(let [forecast-url (get-in data [:properties :forecast])]
(on-success forecast-url)))
:on-error on-error}))
(defn get-forecast-data
"Get forecast data from forecast URL."
[{:keys [url on-success on-error]}]
(fetch-json
{:url url
:on-success on-success
:on-error on-error}))
;; ============================================================================
;; Weather Icon Mapping
;; ============================================================================
(defn get-weather-icon
"Map weather conditions to emoji icons."
[short-forecast]
(let [forecast-lower (str/lower-case (or short-forecast ""))]
(cond
(re-find #"thunder|tstorm" forecast-lower) "⛈️"
(re-find #"rain|shower" forecast-lower) "🌧️"
(re-find #"snow|flurr" forecast-lower) "❄️"
(re-find #"sleet|ice" forecast-lower) "🌨️"
(re-find #"fog|mist" forecast-lower) "🌫️"
(re-find #"cloud" forecast-lower) "☁️"
(re-find #"partly|mostly" forecast-lower) "⛅"
(re-find #"clear|sunny" forecast-lower) "☀️"
(re-find #"wind" forecast-lower) "💨"
:else "🌤️")))
;; ============================================================================
;; Temperature Conversion
;; ============================================================================
(defn fahrenheit-to-celsius [f]
"Convert Fahrenheit to Celsius"
(when f (* (- f 32) 0.5556)))
(defn fahrenheit-to-kelvin [f]
"Convert Fahrenheit to Kelvin"
(when f (+ (fahrenheit-to-celsius f) 273.15)))
(defn format-temp
"Format temperature based on unit (F, C, or K)"
[fahrenheit unit]
(when fahrenheit
(case unit
"F" (str fahrenheit "°F")
"C" (str (Math/round (fahrenheit-to-celsius fahrenheit)) "°C")
"K" (str (Math/round (fahrenheit-to-kelvin fahrenheit)) "K")
(str fahrenheit "°F"))))
;; ============================================================================
;; Styles
;; ============================================================================
(def container-style
{:max-width "1200px"
:margin "0 auto"
:padding "20px"
:font-family "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"})
(def card-style
{:background "#ffffff"
:border "1px solid #e0e0e0"
:border-radius "12px"
:padding "24px"
:margin-bottom "20px"
:box-shadow "0 2px 8px rgba(0,0,0,0.1)"})
(def header-style
{:text-align "center"
:margin-bottom "30px"})
(def title-style
{:font-size "28px"
:font-weight "600"
:color "#1a1a1a"
:margin "0 0 10px 0"})
(def subtitle-style
{:font-size "14px"
:color "#666"
:margin 0})
(def input-group-style
{:display "flex"
:gap "10px"
:margin-bottom "20px"
:flex-wrap "wrap"})
(def input-style
{:padding "10px 15px"
:border "1px solid #ddd"
:border-radius "6px"
:font-size "14px"
:flex "1"
:min-width "120px"})
(def button-style
{:padding "10px 20px"
:background "#3b82f6"
:color "white"
:border "none"
:border-radius "6px"
:cursor "pointer"
:font-size "14px"
:font-weight "500"
:transition "background 0.2s"})
(def quick-buttons-style
{:display "flex"
:gap "8px"
:flex-wrap "wrap"
:margin-bottom "20px"})
(def quick-button-style
{:padding "8px 16px"
:background "#f3f4f6"
:border "1px