diff --git a/examples/geo/.gitignore b/examples/geo/.gitignore new file mode 100644 index 00000000..a4b7a7a6 --- /dev/null +++ b/examples/geo/.gitignore @@ -0,0 +1,2 @@ +secrets.toml +.preswald_deploy \ No newline at end of file diff --git a/examples/geo/README.md b/examples/geo/README.md new file mode 100644 index 00000000..16258336 --- /dev/null +++ b/examples/geo/README.md @@ -0,0 +1,63 @@ +# πŸ›οΈ Monument Intelligence Dashboard + +An interactive, AI-powered dashboard built with [Preswald](https://github.com/StructuredLabs/preswald) that explores iconic monuments across the world. +Analyze historical sites by region, visitors, time period, and moreβ€”all through stunning visualizations and natural language chat. + +--- + +## 🌍 Features + +- πŸ“ Geospatial map of monuments with scalable marker sizes based on yearly visitors +- πŸ“Š Interactive filters for time period and visitor counts +- πŸ“ˆ Graphs for trends by country and build century +- 🧠 Built-in chatbot for natural language queries +- 🎨 Sidebar branding, responsive layout, and clean UI + +--- + +## πŸ—‚ Dataset + +The app uses a curated GeoJSON dataset with 45+ monuments including attributes like: + +- Name & Location +- Year Built +- Estimated Visitors per Year +- Latitude/Longitude coordinates + +File: `data/monuments_geo.geojson` + +--- + +## πŸš€ Run Locally + +Make sure you have Python β‰₯3.8 and Preswald installed. + +```bash +pip install preswald +``` + +Then run the app. + +```bash +preswald run +``` + +## πŸ“ Folder Structure + +monument-dashboard/ +β”œβ”€β”€ hello.py +β”œβ”€β”€ preswald.toml +β”œβ”€β”€ pyproject.toml +β”œβ”€β”€ README.md +β”œβ”€β”€ .gitignore +β”œβ”€β”€ data/ +β”‚ └── monuments_geo.geojson +β”œβ”€β”€ images/ +β”‚ β”œβ”€β”€ logo.png +β”‚ └── favicon.ico +└── dist/ # (created after export) + +## πŸ’‘ Credits + +Built using [Preswald](https://github.com/StructuredLabs/preswald) +Inspired by global cultural heritage and open data initiatives 🌐 \ No newline at end of file diff --git a/examples/geo/data/monuments_geo.geojson b/examples/geo/data/monuments_geo.geojson new file mode 100644 index 00000000..b7070ca4 --- /dev/null +++ b/examples/geo/data/monuments_geo.geojson @@ -0,0 +1,500 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "id": "m001", + "name": "Statue of Liberty", + "location": "New York, USA", + "year_built": 1886, + "visitors_per_year": 4300000 + }, + "geometry": { "type": "Point", "coordinates": [-74.0445, 40.6892] } + }, + { + "type": "Feature", + "properties": { + "id": "m002", + "name": "Eiffel Tower", + "location": "Paris, France", + "year_built": 1889, + "visitors_per_year": 7000000 + }, + "geometry": { "type": "Point", "coordinates": [2.2945, 48.8584] } + }, + { + "type": "Feature", + "properties": { + "id": "m003", + "name": "Taj Mahal", + "location": "Agra, India", + "year_built": 1643, + "visitors_per_year": 8000000 + }, + "geometry": { "type": "Point", "coordinates": [78.0421, 27.1751] } + }, + { + "type": "Feature", + "properties": { + "id": "m004", + "name": "Colosseum", + "location": "Rome, Italy", + "year_built": 80, + "visitors_per_year": 7500000 + }, + "geometry": { "type": "Point", "coordinates": [12.4922, 41.8902] } + }, + { + "type": "Feature", + "properties": { + "id": "m005", + "name": "Big Ben", + "location": "London, UK", + "year_built": 1859, + "visitors_per_year": 3000000 + }, + "geometry": { "type": "Point", "coordinates": [-0.1246, 51.5007] } + }, + { + "type": "Feature", + "properties": { + "id": "m006", + "name": "Christ the Redeemer", + "location": "Rio de Janeiro, Brazil", + "year_built": 1931, + "visitors_per_year": 2000000 + }, + "geometry": { "type": "Point", "coordinates": [-43.2105, -22.9519] } + }, + { + "type": "Feature", + "properties": { + "id": "m007", + "name": "Great Wall of China", + "location": "Beijing, China", + "year_built": -700, + "visitors_per_year": 10000000 + }, + "geometry": { "type": "Point", "coordinates": [116.5704, 40.4319] } + }, + { + "type": "Feature", + "properties": { + "id": "m008", + "name": "Sydney Opera House", + "location": "Sydney, Australia", + "year_built": 1973, + "visitors_per_year": 8200000 + }, + "geometry": { "type": "Point", "coordinates": [151.2153, -33.8568] } + }, + { + "type": "Feature", + "properties": { + "id": "m009", + "name": "Petra", + "location": "Ma'an, Jordan", + "year_built": -300, + "visitors_per_year": 1000000 + }, + "geometry": { "type": "Point", "coordinates": [35.4444, 30.3285] } + }, + { + "type": "Feature", + "properties": { + "id": "m010", + "name": "Angkor Wat", + "location": "Siem Reap, Cambodia", + "year_built": 1150, + "visitors_per_year": 2500000 + }, + "geometry": { "type": "Point", "coordinates": [103.866986, 13.412469] } + }, + { + "type": "Feature", + "properties": { + "id": "m011", + "name": "Mount Rushmore", + "location": "South Dakota, USA", + "year_built": 1941, + "visitors_per_year": 2000000 + }, + "geometry": { "type": "Point", "coordinates": [-103.4591, 43.8791] } + }, + { + "type": "Feature", + "properties": { + "id": "m012", + "name": "Acropolis of Athens", + "location": "Athens, Greece", + "year_built": -447, + "visitors_per_year": 2000000 + }, + "geometry": { "type": "Point", "coordinates": [23.7266, 37.9715] } + }, + { + "type": "Feature", + "properties": { + "id": "m013", + "name": "Chichen Itza", + "location": "YucatΓ‘n, Mexico", + "year_built": 600, + "visitors_per_year": 2700000 + }, + "geometry": { "type": "Point", "coordinates": [-88.5678, 20.6843] } + }, + { + "type": "Feature", + "properties": { + "id": "m014", + "name": "Machu Picchu", + "location": "Cusco Region, Peru", + "year_built": 1450, + "visitors_per_year": 1500000 + }, + "geometry": { "type": "Point", "coordinates": [-72.5450, -13.1631] } + }, + { + "type": "Feature", + "properties": { + "id": "m015", + "name": "Burj Khalifa", + "location": "Dubai, UAE", + "year_built": 2010, + "visitors_per_year": 6500000 + }, + "geometry": { "type": "Point", "coordinates": [55.2744, 25.1972] } + }, + { + "type": "Feature", + "properties": { + "id": "m016", + "name": "Golden Gate Bridge", + "location": "San Francisco, USA", + "year_built": 1937, + "visitors_per_year": 10000000 + }, + "geometry": { "type": "Point", "coordinates": [-122.4783, 37.8199] } + }, + { + "type": "Feature", + "properties": { + "id": "m017", + "name": "Hagia Sophia", + "location": "Istanbul, Turkey", + "year_built": 537, + "visitors_per_year": 3700000 + }, + "geometry": { "type": "Point", "coordinates": [28.9799, 41.0086] } + }, + { + "type": "Feature", + "properties": { + "id": "m018", + "name": "Brandenburg Gate", + "location": "Berlin, Germany", + "year_built": 1791, + "visitors_per_year": 1200000 + }, + "geometry": { "type": "Point", "coordinates": [13.3777, 52.5163] } + }, + { + "type": "Feature", + "properties": { + "id": "m019", + "name": "Alhambra", + "location": "Granada, Spain", + "year_built": 1333, + "visitors_per_year": 2500000 + }, + "geometry": { "type": "Point", "coordinates": [-3.5881, 37.1761] } + }, + { + "type": "Feature", + "properties": { + "id": "m020", + "name": "Neuschwanstein Castle", + "location": "Bavaria, Germany", + "year_built": 1886, + "visitors_per_year": 1400000 + }, + "geometry": { "type": "Point", "coordinates": [10.7498, 47.5576] } + }, + { + "type": "Feature", + "properties": { + "id": "m021", + "name": "Sagrada Familia", + "location": "Barcelona, Spain", + "year_built": 1882, + "visitors_per_year": 4500000 + }, + "geometry": { "type": "Point", "coordinates": [2.1744, 41.4036] } + }, + { + "type": "Feature", + "properties": { + "id": "m022", + "name": "Tower of London", + "location": "London, UK", + "year_built": 1078, + "visitors_per_year": 3000000 + }, + "geometry": { "type": "Point", "coordinates": [-0.0761, 51.5081] } + }, + { + "type": "Feature", + "properties": { + "id": "m023", + "name": "Versailles Palace", + "location": "Versailles, France", + "year_built": 1682, + "visitors_per_year": 7800000 + }, + "geometry": { "type": "Point", "coordinates": [2.1204, 48.8049] } + }, + { + "type": "Feature", + "properties": { + "id": "m024", + "name": "Edinburgh Castle", + "location": "Edinburgh, Scotland", + "year_built": 1103, + "visitors_per_year": 2200000 + }, + "geometry": { "type": "Point", "coordinates": [-3.2005, 55.9486] } + }, + { + "type": "Feature", + "properties": { + "id": "m025", + "name": "Forbidden City", + "location": "Beijing, China", + "year_built": 1420, + "visitors_per_year": 17000000 + }, + "geometry": { "type": "Point", "coordinates": [116.3975, 39.9163] } + }, + { + "type": "Feature", + "properties": { + "id": "m013", + "name": "Red Fort", + "location": "Delhi, India", + "year_built": 1648, + "visitors_per_year": 3000000 + }, + "geometry": { "type": "Point", "coordinates": [77.2410, 28.6562] } + }, + { + "type": "Feature", + "properties": { + "id": "m014", + "name": "Qutub Minar", + "location": "Delhi, India", + "year_built": 1192, + "visitors_per_year": 2700000 + }, + "geometry": { "type": "Point", "coordinates": [77.1855, 28.5244] } + }, + { + "type": "Feature", + "properties": { + "id": "m015", + "name": "Gateway of India", + "location": "Mumbai, India", + "year_built": 1924, + "visitors_per_year": 5000000 + }, + "geometry": { "type": "Point", "coordinates": [72.8347, 18.9218] } + }, + { + "type": "Feature", + "properties": { + "id": "m016", + "name": "Charminar", + "location": "Hyderabad, India", + "year_built": 1591, + "visitors_per_year": 2500000 + }, + "geometry": { "type": "Point", "coordinates": [78.4747, 17.3616] } + }, + { + "type": "Feature", + "properties": { + "id": "m017", + "name": "Sanchi Stupa", + "location": "Madhya Pradesh, India", + "year_built": -300, + "visitors_per_year": 1500000 + }, + "geometry": { "type": "Point", "coordinates": [77.7390, 23.4805] } + }, + { + "type": "Feature", + "properties": { + "id": "m018", + "name": "Hawa Mahal", + "location": "Jaipur, India", + "year_built": 1799, + "visitors_per_year": 1800000 + }, + "geometry": { "type": "Point", "coordinates": [75.8274, 26.9239] } + }, + { + "type": "Feature", + "properties": { + "id": "m019", + "name": "Meenakshi Temple", + "location": "Madurai, India", + "year_built": 1600, + "visitors_per_year": 7000000 + }, + "geometry": { "type": "Point", "coordinates": [78.1195, 9.9195] } + }, + { + "type": "Feature", + "properties": { + "id": "m020", + "name": "Ajanta Caves", + "location": "Maharashtra, India", + "year_built": -200, + "visitors_per_year": 1100000 + }, + "geometry": { "type": "Point", "coordinates": [75.7000, 20.5525] } + }, + { + "type": "Feature", + "properties": { + "id": "m021", + "name": "Golconda Fort", + "location": "Hyderabad, India", + "year_built": 1518, + "visitors_per_year": 1900000 + }, + "geometry": { "type": "Point", "coordinates": [78.4011, 17.3833] } + }, + { + "type": "Feature", + "properties": { + "id": "m022", + "name": "Sun Temple", + "location": "Konark, India", + "year_built": 1250, + "visitors_per_year": 1600000 + }, + "geometry": { "type": "Point", "coordinates": [86.0952, 19.8876] } + }, + { + "type": "Feature", + "properties": { + "id": "m023", + "name": "Victoria Memorial", + "location": "Kolkata, India", + "year_built": 1921, + "visitors_per_year": 3000000 + }, + "geometry": { "type": "Point", "coordinates": [88.3426, 22.5448] } + }, + { + "type": "Feature", + "properties": { + "id": "m024", + "name": "Lotus Temple", + "location": "Delhi, India", + "year_built": 1986, + "visitors_per_year": 8000000 + }, + "geometry": { "type": "Point", "coordinates": [77.2588, 28.5535] } + }, + { + "type": "Feature", + "properties": { + "id": "m025", + "name": "Forbidden City", + "location": "Beijing, China", + "year_built": 1420, + "visitors_per_year": 17000000 + }, + "geometry": { "type": "Point", "coordinates": [116.3975, 39.9163] } + }, + { + "type": "Feature", + "properties": { + "id": "m026", + "name": "Alhambra", + "location": "Granada, Spain", + "year_built": 1333, + "visitors_per_year": 2600000 + }, + "geometry": { "type": "Point", "coordinates": [-3.5881, 37.1760] } + }, + { + "type": "Feature", + "properties": { + "id": "m027", + "name": "Stonehenge", + "location": "Wiltshire, UK", + "year_built": -2500, + "visitors_per_year": 1200000 + }, + "geometry": { "type": "Point", "coordinates": [-1.8262, 51.1789] } + }, + { + "type": "Feature", + "properties": { + "id": "m028", + "name": "Machu Picchu", + "location": "Cusco Region, Peru", + "year_built": 1450, + "visitors_per_year": 1500000 + }, + "geometry": { "type": "Point", "coordinates": [-72.5450, -13.1631] } + }, + { + "type": "Feature", + "properties": { + "id": "m029", + "name": "Christ Church Cathedral", + "location": "Dublin, Ireland", + "year_built": 1030, + "visitors_per_year": 300000 + }, + "geometry": { "type": "Point", "coordinates": [-6.2711, 53.3430] } + }, + { + "type": "Feature", + "properties": { + "id": "m030", + "name": "Neuschwanstein Castle", + "location": "Bavaria, Germany", + "year_built": 1886, + "visitors_per_year": 1400000 + }, + "geometry": { "type": "Point", "coordinates": [10.7498, 47.5576] } + }, + { + "type": "Feature", + "properties": { + "id": "m031", + "name": "Ephesus", + "location": "SelΓ§uk, Turkey", + "year_built": -1000, + "visitors_per_year": 2000000 + }, + "geometry": { "type": "Point", "coordinates": [27.3400, 37.9390] } + }, + { + "type": "Feature", + "properties": { + "id": "m032", + "name": "Burj Khalifa", + "location": "Dubai, UAE", + "year_built": 2010, + "visitors_per_year": 6000000 + }, + "geometry": { "type": "Point", "coordinates": [55.2744, 25.1972] } + } + ] +} \ No newline at end of file diff --git a/examples/geo/hello.py b/examples/geo/hello.py new file mode 100644 index 00000000..b379858b --- /dev/null +++ b/examples/geo/hello.py @@ -0,0 +1,119 @@ +import pandas as pd +import plotly.express as px +from preswald import ( + get_df, + plotly, + table, + text, + slider, + selectbox, + sidebar, + checkbox, + topbar, + alert, + image, + progress, + chat, + separator, +) + +# Add sidebar and topbar for structure +sidebar(name="πŸ—ΊοΈ Global Monuments Explorer", logo="images/logo.png") +topbar() + +# App Header +text(""" +# πŸ›οΈ Monument Intelligence Dashboard +Explore historic monuments around the world with dynamic filters, geographic visualizations, and chat-powered analytics. +""") +separator() + +# Load data +df = get_df("monuments_geo") + +if df is not None: + df.columns = df.columns.str.lower() + + # Flatten geometry + df["latitude"] = df["geometry"].apply(lambda g: g.get("coordinates", [None, None])[1]) + df["longitude"] = df["geometry"].apply(lambda g: g.get("coordinates", [None, None])[0]) + df["country"] = df["location"].apply(lambda loc: loc.split(",")[-1].strip()) + + # Drop rows with missing lat/lon + df = df.dropna(subset=["latitude", "longitude"]) + + # Summary Stats + text(f"### πŸ—‚ Total Monuments: **{len(df)}**", size=0.5) + text(f"### 🌍 Countries Covered: **{df['country'].nunique()}**", size=0.5) + separator() + + # Filters + min_visitors = slider("🎟️ Minimum Visitors Per Year", min_val=0, max_val=20000000, default=1000000, step=100000) + text("") + year_cutoff = slider("πŸ—οΈ Built After Year", min_val=-700, max_val=2025, default=1800) + text("") + historic_filter = checkbox("πŸ•°οΈ Show Ancient Monuments (Before 1000 AD)", default=True) + + # Apply filters + filtered = df[df["visitors_per_year"] >= min_visitors] + filtered = filtered[filtered["year_built"] >= year_cutoff] + if not historic_filter: + filtered = filtered[filtered["year_built"] >= 1000] + + shown_pct = round(len(filtered) / len(df) * 100, 1) + progress("πŸ“Š Visible Monuments (%)", value=shown_pct) + alert(f"{len(filtered)} monuments match your filters.", level="info") + + # Table + text("## πŸ“‹ Filtered Monument List") + table(filtered[["name", "location", "year_built", "visitors_per_year"]].sort_values("visitors_per_year", ascending=False)) + + # Map + separator() + text("## 🌐 Global Monument Map") + + # Normalize size for better visibility + filtered["scaled_size"] = pd.qcut(filtered["visitors_per_year"], q=5, labels=[5, 10, 15, 20, 30]).astype(int) + + fig_map = px.scatter_geo( + filtered, + lat="latitude", + lon="longitude", + size="scaled_size", + color="country", + hover_name="name", + title="Visitor Hotspots Around the Globe", + projection="natural earth", + ) + + fig_map.update_traces(marker=dict(opacity=0.8, line=dict(width=1, color='white'))) + fig_map.update_layout(geo=dict(showland=True, landcolor="LightGray")) + plotly(fig_map) + + # Country Ranking + separator() + text("## 🏳️ Top Countries by Monument Count") + top_countries = filtered["country"].value_counts().nlargest(8).reset_index() + top_countries.columns = ["Country", "Count"] + fig_bar = px.bar(top_countries, x="Country", y="Count", title="Top Countries by Monument Count", text_auto=True, color="Country") + plotly(fig_bar) + + # Visitors Over Time Chart + separator() + text("## πŸ“ˆ Average Visitors by Build Century") + filtered["century"] = (filtered["year_built"] // 100 * 100).astype(int) + century_df = filtered.groupby("century")["visitors_per_year"].mean().reset_index() + fig_line = px.line(century_df, x="century", y="visitors_per_year", title="Average Visitors Per Century", markers=True) + plotly(fig_line) + + # Optional image section + if checkbox("πŸ–ΌοΈ Show App Logo"): + image(src="images/logo.png", alt="Monument Logo", className="rounded-xl shadow-xl") + + # Interactive Chat + separator() + text("## πŸ€– Ask Questions About the Data") + chat("monuments_geo") + +else: + alert("🚫 No data available to display.", level="warning") diff --git a/examples/geo/images/favicon.ico b/examples/geo/images/favicon.ico new file mode 100644 index 00000000..5a787152 Binary files /dev/null and b/examples/geo/images/favicon.ico differ diff --git a/examples/geo/images/logo.png b/examples/geo/images/logo.png new file mode 100644 index 00000000..8fcd47ec Binary files /dev/null and b/examples/geo/images/logo.png differ diff --git a/examples/geo/preswald.toml b/examples/geo/preswald.toml new file mode 100644 index 00000000..c028a655 --- /dev/null +++ b/examples/geo/preswald.toml @@ -0,0 +1,17 @@ +[project] +title = "Monuments App" +version = "0.1.0" +port = 8501 +entrypoint = "hello.py" +slug = "monuments" + +[branding] +name = "Monuments App" +logo = "images/logo.png" +favicon = "images/favicon.ico" +primaryColor = "#395886" + +[data.monuments_geo] +type = "geojson" +path = "data/monuments_geo.geojson" +flatten_geometry = true \ No newline at end of file diff --git a/examples/geo/pyproject.toml b/examples/geo/pyproject.toml new file mode 100644 index 00000000..1a1e0521 --- /dev/null +++ b/examples/geo/pyproject.toml @@ -0,0 +1,24 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "monuments" +version = "0.1.0" +description = "A Preswald app showcasing world monuments" +requires-python = ">=3.8" +dependencies = [ + "preswald>=0.1.33", + "pandas", + "plotly", + "geopandas" +] + +[tool.black] +line-length = 88 + +[tool.isort] +profile = "black" + +[tool.hatch.build.targets.wheel] +packages = ["."] \ No newline at end of file diff --git a/preswald/engine/managers/data.py b/preswald/engine/managers/data.py index 87a57d63..f7f47a37 100644 --- a/preswald/engine/managers/data.py +++ b/preswald/engine/managers/data.py @@ -11,6 +11,7 @@ import toml from requests.auth import HTTPBasicAuth +import geopandas as gpd logger = logging.getLogger(__name__) @@ -430,6 +431,15 @@ def connect(self): # noqa: C901 if source_type == "csv": cfg = CSVConfig(path=source_config["path"]) self.sources[name] = CSVSource(name, cfg, self.duckdb_conn) + + elif source_type in ["geojson", "shapefile"]: + df = load_geospatial_source(source_config) + table_name = f"geo_{uuid.uuid4().hex[:8]}" + self.duckdb_conn.register(table_name, df) + self.sources[name] = DataSource(name, self.duckdb_conn) + self.sources[name]._table_name = table_name + self.sources[name].to_df = lambda: df + self.sources[name].query = lambda sql: self.duckdb_conn.execute(sql.replace(name, table_name)).df() elif source_type == "json": cfg = JSONConfig( @@ -618,3 +628,11 @@ def _load_json_source(config: dict[str, Any]) -> pd.DataFrame: raise ValueError( f"Error converting JSON data from file '{path}' to DataFrame: {e}" ) from e + +def load_geospatial_source(config: dict[str, Any]) -> pd.DataFrame: + df = gpd.read_file(config["path"]) + + if config.get("flatten_geometry", True): + df["geometry"] = df["geometry"].apply(lambda g: g.__geo_interface__) + + return df \ No newline at end of file