Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ dependencies = [
"fiona>=1.9.5",
# Additional Geospatial Tools
"epinterface>=1.1.1",
"scythe-engine>=0.0.43",
"scythe-engine>=0.1.0",
"ladybug-core>=0.44.29",
"pydantic>=2.11,<3",
"boto3>=1.40.49",
Expand All @@ -43,6 +43,7 @@ visualization = [
"bokeh>=3.8.0",
"rasterio>=1.3.9",
"folium>=0.15.0",
"playwright>=1.40.0",
]

cli = [
Expand Down Expand Up @@ -79,6 +80,7 @@ globi = "globi.tools.cli.main:cli"

[tool.uv.sources]
# scythe-engine = {git = "https://github.com/szvsw/scythe", branch = "feature/allow-optional-filerefs"}
# scythe-engine = {git = "https://github.com/szvsw/scythe", branch = "feature/update-hatchet"}
# scythe-engine = {path = "../scythe", editable = true}


Expand Down
7 changes: 6 additions & 1 deletion src/globi/tools/visualization/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@
UseCaseConfig,
UseCaseType,
)
from globi.tools.visualization.pages import render_raw_data_page, render_use_cases_page
from globi.tools.visualization.views import (
render_overview_page,
render_raw_data_page,
render_use_cases_page,
)

__all__ = [
"Building3DConfig",
Expand All @@ -33,6 +37,7 @@
"ScenarioComparisonConfig",
"UseCaseConfig",
"UseCaseType",
"render_overview_page",
"render_raw_data_page",
"render_use_cases_page",
]
125 changes: 125 additions & 0 deletions src/globi/tools/visualization/data_sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING

Expand All @@ -23,6 +24,130 @@
from mypy_boto3_s3 import S3Client


@dataclass
class S3ExperimentInfo:
"""Information about an experiment available in S3."""

run_name: str
versions: list[str]
latest_version: str | None = None

@property
def display_name(self) -> str:
"""Display name for the experiment."""
return self.run_name

def __str__(self) -> str:
"""String representation."""
version_str = self.latest_version or "no versions"
return f"{self.run_name} ({version_str})"


def _get_s3_prefixes(
s3_client: S3Client,
bucket: str,
prefix: str,
) -> list[str]:
"""List all common prefixes under a given S3 prefix."""
paginator = s3_client.get_paginator("list_objects_v2")
prefixes: list[str] = []
for page in paginator.paginate(
Bucket=bucket,
Prefix=prefix,
Delimiter="/",
PaginationConfig={"PageSize": 1000},
):
for common_prefix in page.get("CommonPrefixes", []):
p = common_prefix.get("Prefix", "")
if p:
prefixes.append(p)
return prefixes


def _extract_experiment_names(prefixes: list[str], base_prefix: str) -> list[str]:
"""Extract experiment names from S3 prefixes."""
names = []
for p in prefixes:
name = p[len(base_prefix) :].rstrip("/")
if name:
names.append(name)
return names


def _extract_versions(prefixes: list[str], exp_prefix: str) -> list[str]:
"""Extract version strings from S3 prefixes."""
versions = []
for p in prefixes:
version = p[len(exp_prefix) :].rstrip("/")
if version.startswith("v"):
versions.append(version)
return versions


def list_s3_experiments(
bucket: str | None = None,
prefix: str | None = None,
s3_client: S3Client | None = None,
) -> list[S3ExperimentInfo]:
"""List all available experiments in S3.

Args:
bucket: S3 bucket name. If None, uses ScytheStorageSettings.
prefix: S3 prefix. If None, uses ScytheStorageSettings.BUCKET_PREFIX.
s3_client: Optional S3 client. If None, creates a new one.

Returns:
List of S3ExperimentInfo objects with experiment names and versions.
"""
import boto3
from scythe.settings import ScytheStorageSettings

settings = ScytheStorageSettings()
bucket = bucket or settings.BUCKET
prefix = prefix or settings.BUCKET_PREFIX

if s3_client is None:
s3_client = boto3.client("s3")

if not prefix.endswith("/"):
prefix = prefix + "/"

exp_prefixes = _get_s3_prefixes(s3_client, bucket, prefix)
exp_names = _extract_experiment_names(exp_prefixes, prefix)

result = []
for exp_name in sorted(exp_names):
exp_prefix = f"{prefix}{exp_name}/"
version_prefixes = _get_s3_prefixes(s3_client, bucket, exp_prefix)
versions = _extract_versions(version_prefixes, exp_prefix)
sorted_versions = _sort_versions(versions)
latest = sorted_versions[-1] if sorted_versions else None
result.append(
S3ExperimentInfo(
run_name=exp_name,
versions=sorted_versions,
latest_version=latest,
)
)
return result


def _sort_versions(versions: list[str]) -> list[str]:
"""Sort semantic versions in ascending order."""

def parse_version(v: str) -> tuple[int, int, int]:
if v.startswith("v"):
v = v[1:]
parts = v.replace("-", ".").split(".")
return (
int(parts[0]) if len(parts) > 0 else 0,
int(parts[1]) if len(parts) > 1 else 0,
int(parts[2]) if len(parts) > 2 else 0,
)

return sorted(versions, key=parse_version)
Comment on lines +46 to +148
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason this is not using the equivalent code from Scythe?



class DataSource(ABC):
"""Abstract base class for data sources."""

Expand Down
32 changes: 32 additions & 0 deletions src/globi/tools/visualization/export.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Export helpers for chart download (e.g. HTML to PNG)."""

from __future__ import annotations

import tempfile
from pathlib import Path

from playwright.sync_api import sync_playwright


def render_html_to_png(html: str, width: int = 800, height: int = 500) -> bytes | None:
"""Render chart HTML to PNG bytes using playwright if available."""
with tempfile.NamedTemporaryFile(
mode="w", suffix=".html", delete=False, encoding="utf-8"
) as f:
f.write(html)
path = Path(f.name)

try:
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page(viewport={"width": width, "height": height})
page.goto(f"file://{path.resolve()}")
page.wait_for_timeout(500)
png_bytes = page.screenshot(type="png", full_page=True)
browser.close()
except Exception:
return None
else:
return png_bytes
finally:
path.unlink(missing_ok=True)
91 changes: 45 additions & 46 deletions src/globi/tools/visualization/main.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,50 @@
"""Entry point for the GLOBI visualization app."""
"""Entry point for the GLOBI visualization app. Uses st.navigation so the first page displays as "Overview"."""

from __future__ import annotations

from pathlib import Path

import streamlit as st

from globi.tools.visualization.data_sources import DataSource
from globi.tools.visualization.models import LocalDataSourceConfig, S3DataSourceConfig
from globi.tools.visualization.pages import render_raw_data_page, render_use_cases_page


def main() -> None:
"""Entry point for the visualization app."""
st.set_page_config(page_title="GLOBI Visualization", layout="wide")
st.title("GLOBI Visualization")

with st.sidebar:
source_type = st.radio("Data Source", options=["Local", "S3"], index=0)

if source_type == "Local":
base_dir = st.text_input("Output Directory", value="outputs")
config = LocalDataSourceConfig(base_dir=Path(base_dir))
else:
run_name = st.text_input("S3 Run Name", value="")
version = st.text_input("Version (optional)", value="")
if not run_name:
st.warning("Enter a run name to load from S3.")
return
config = S3DataSourceConfig(
run_name=run_name,
version=version if version else None,
)

data_source = DataSource.from_config(config)

page = st.sidebar.selectbox(
"Page",
options=["Raw Data Visualization", "Use Cases"],
index=0,
)

if page == "Raw Data Visualization":
render_raw_data_page(data_source)
elif page == "Use Cases":
render_use_cases_page(data_source)


if __name__ == "__main__":
main()
from globi.tools.visualization.sidebar import render_data_source_sidebar
from globi.tools.visualization.views import (
render_overview_page,
render_raw_data_page,
render_use_cases_page,
)

st.set_page_config(page_title="GLOBI Visualization", layout="wide")
st.title("GLOBI Visualization")

data_source = render_data_source_sidebar()
st.session_state["data_source"] = data_source


def _overview() -> None:
render_overview_page()


def _raw_data() -> None:
ds = st.session_state.get("data_source")
if ds is not None:
render_raw_data_page(ds)
else:
st.info(
"Configure a data source in the sidebar (e.g. Local with an output directory) to view raw data."
)


def _use_cases() -> None:
ds = st.session_state.get("data_source")
if ds is not None:
render_use_cases_page(ds)
else:
st.info(
"Configure a data source in the sidebar (e.g. Local with an output directory) to run use cases."
)


pg = st.navigation([
st.Page(_overview, title="Overview"),
st.Page(_raw_data, title="Raw Data Visualization"),
st.Page(_use_cases, title="Use Cases"),
])
pg.run()
6 changes: 0 additions & 6 deletions src/globi/tools/visualization/pages/__init__.py

This file was deleted.

Loading