Spaces:
Sleeping
Sleeping
| """Reusable UI components for Solar Intelligence dashboard.""" | |
| from __future__ import annotations | |
| import logging | |
| import panel as pn | |
| import param | |
| logger = logging.getLogger(__name__) | |
| class LocationPicker(param.Parameterized): | |
| """Combined location input widget with geocoding support.""" | |
| city = param.String(default="", doc="City name for geocoding") | |
| latitude = param.Number(default=0.0, bounds=(-90, 90)) | |
| longitude = param.Number(default=0.0, bounds=(-180, 180)) | |
| location_name = param.String(default="") | |
| country_code = param.String(default="", doc="ISO country code from geocoding") | |
| def __init__(self, **params): | |
| super().__init__(**params) | |
| self._city_input = pn.widgets.TextInput( | |
| name="City Name", value=self.city, | |
| placeholder="e.g. New York, London, Tokyo", | |
| description="Type any city name and click Search", | |
| ) | |
| self._lat_input = pn.widgets.FloatInput( | |
| name="Latitude", value=self.latitude, step=0.1, start=-90, end=90, | |
| description="North (+) / South (-), e.g. 28.6 for Delhi", | |
| ) | |
| self._lon_input = pn.widgets.FloatInput( | |
| name="Longitude", value=self.longitude, step=0.1, start=-180, end=180, | |
| description="East (+) / West (-), e.g. 77.2 for Delhi", | |
| ) | |
| self._search_btn = pn.widgets.Button( | |
| name="Search Location", button_type="primary", | |
| ) | |
| self._status = pn.pane.Markdown("") | |
| self._search_btn.on_click(self._on_search) | |
| self._lat_input.param.watch(self._on_coord_change, "value") | |
| self._lon_input.param.watch(self._on_coord_change, "value") | |
| def _on_search(self, event): | |
| city = self._city_input.value.strip() | |
| if not city: | |
| self._status.object = "*Enter a city name*" | |
| return | |
| try: | |
| from geopy.geocoders import Nominatim | |
| geolocator = Nominatim(user_agent="solar-intelligence-platform") | |
| location = geolocator.geocode(city, timeout=10, addressdetails=True) | |
| if location is None: | |
| self._status.object = f"*Could not find '{city}'*" | |
| return | |
| lat = round(location.latitude, 4) | |
| lon = round(location.longitude, 4) | |
| self._lat_input.value = lat | |
| self._lon_input.value = lon | |
| self.latitude = lat | |
| self.longitude = lon | |
| self.location_name = city | |
| # Extract country code from geocoding result | |
| address = location.raw.get("address", {}) | |
| cc = address.get("country_code", "").upper() | |
| self.country_code = cc | |
| country_name = address.get("country", "") | |
| if country_name: | |
| self._status.object = f"Found: {city}, {country_name} ({lat}, {lon})" | |
| else: | |
| self._status.object = f"Found: {city} ({lat}, {lon})" | |
| except Exception as e: | |
| self._status.object = f"*Error: {e}*" | |
| def _on_coord_change(self, event): | |
| self.latitude = self._lat_input.value | |
| self.longitude = self._lon_input.value | |
| def panel(self) -> pn.Column: | |
| """Return the location picker widget column.""" | |
| return pn.Column( | |
| "### Where are your solar panels?", | |
| self._city_input, | |
| self._search_btn, | |
| self._lat_input, | |
| self._lon_input, | |
| self._status, | |
| width=260, | |
| ) | |
| class PanelConfigurator(param.Parameterized): | |
| """Solar panel specification input widgets.""" | |
| panel_efficiency = param.Number(default=0.20, bounds=(0.05, 0.40)) | |
| panel_area = param.Number(default=1.7, bounds=(0.1, 10.0)) | |
| num_panels = param.Integer(default=10, bounds=(1, 1000)) | |
| system_losses = param.Number(default=0.14, bounds=(0.0, 0.5)) | |
| tilt_angle = param.Number(default=30, bounds=(0, 90)) | |
| direction = param.Selector( | |
| default="South", | |
| objects=["North", "North-East", "East", "South-East", | |
| "South", "South-West", "West", "North-West"], | |
| ) | |
| def panel(self) -> pn.Column: | |
| """Return the panel configuration widget column.""" | |
| return pn.Column( | |
| "### Your Solar Panels", | |
| pn.widgets.FloatSlider.from_param( | |
| self.param.panel_efficiency, name="Panel Efficiency", format="0.0%", | |
| ), | |
| pn.widgets.FloatInput.from_param( | |
| self.param.panel_area, name="Panel Size (m²)", | |
| ), | |
| pn.widgets.IntSlider.from_param( | |
| self.param.num_panels, name="How Many Panels?", | |
| ), | |
| pn.widgets.FloatSlider.from_param( | |
| self.param.system_losses, name="System Losses", format="0.0%", | |
| ), | |
| pn.widgets.FloatSlider.from_param( | |
| self.param.tilt_angle, name="Roof Tilt (°)", | |
| ), | |
| pn.widgets.Select.from_param( | |
| self.param.direction, name="Panels Facing", | |
| ), | |
| width=260, | |
| ) | |
| class FinancialConfigurator(param.Parameterized): | |
| """Financial parameter input widgets with multi-currency support.""" | |
| currency = param.Selector( | |
| default="INR", | |
| objects=["INR", "USD", "EUR", "GBP", "CNY", "JPY", "AUD", "BRL", | |
| "ZAR", "CAD", "KRW", "AED", "MXN", "SGD"], | |
| doc="Currency for financial calculations", | |
| ) | |
| system_cost = param.Number(default=500000, bounds=(1000, 50_000_000)) | |
| electricity_rate = param.Number(default=8.0, bounds=(0.01, 100.0)) | |
| incentive_percent = param.Number(default=0.40, bounds=(0, 1.0)) | |
| def __init__(self, **params): | |
| super().__init__(**params) | |
| from solar_intelligence.config import CURRENCIES, CURRENCY_DEFAULTS, COUNTRY_PROFILES | |
| self._currencies = CURRENCIES | |
| self._currency_defaults = CURRENCY_DEFAULTS | |
| self._country_profiles = COUNTRY_PROFILES | |
| # Apply defaults for initial currency | |
| self._apply_currency_defaults(self.currency) | |
| def _apply_currency_defaults(self, currency_code: str): | |
| """Apply default financial values for selected currency.""" | |
| defaults = self._currency_defaults.get(currency_code, {}) | |
| if defaults: | |
| self.system_cost = defaults["system_cost"] | |
| self.electricity_rate = defaults["electricity_rate"] | |
| self.incentive_percent = defaults["incentive_percent"] | |
| def apply_country(self, country_code: str): | |
| """Auto-set currency and financial defaults from a country code.""" | |
| profile = self._country_profiles.get(country_code, {}) | |
| if profile: | |
| cur = profile.get("default_currency", self.currency) | |
| if cur in [o for o in self.param.currency.objects]: | |
| self.currency = cur | |
| self._apply_currency_defaults(cur) | |
| # Override with country-specific values if available | |
| if "electricity_rate" in profile: | |
| self.electricity_rate = profile["electricity_rate"] | |
| if "subsidy_percent" in profile: | |
| self.incentive_percent = profile["subsidy_percent"] | |
| def currency_symbol(self) -> str: | |
| """Get the symbol for current currency.""" | |
| return self._currencies.get(self.currency, {}).get("symbol", "$") | |
| def panel(self) -> pn.Column: | |
| """Return the financial parameters widget column.""" | |
| sym = self.currency_symbol | |
| currency_select = pn.widgets.Select.from_param( | |
| self.param.currency, name="Currency", | |
| ) | |
| cost_input = pn.widgets.FloatInput.from_param( | |
| self.param.system_cost, name=f"System Cost ({sym})", | |
| ) | |
| rate_input = pn.widgets.FloatInput.from_param( | |
| self.param.electricity_rate, name=f"Electricity Rate ({sym}/kWh)", | |
| ) | |
| incentive_input = pn.widgets.FloatSlider.from_param( | |
| self.param.incentive_percent, name="Subsidy/Incentive", format="0.0%", | |
| ) | |
| # Watch currency changes to update defaults | |
| def on_currency_change(event): | |
| self._apply_currency_defaults(event.new) | |
| sym_new = self._currencies.get(event.new, {}).get("symbol", "$") | |
| cost_input.name = f"System Cost ({sym_new})" | |
| rate_input.name = f"Electricity Rate ({sym_new}/kWh)" | |
| currency_select.param.watch(on_currency_change, "value") | |
| return pn.Column( | |
| "### Costs & Savings", | |
| currency_select, | |
| cost_input, | |
| rate_input, | |
| incentive_input, | |
| width=260, | |
| ) | |
| class KPICard: | |
| """Styled metric display card.""" | |
| def create(title: str, value: str, subtitle: str = "", color: str = "#FFB900") -> pn.Column: | |
| """Create a styled KPI metric card.""" | |
| return pn.Column( | |
| pn.pane.HTML(f""" | |
| <div style="background: linear-gradient(135deg, {color}22, {color}11); | |
| border-left: 4px solid {color}; border-radius: 8px; | |
| padding: 16px; min-width: 180px;"> | |
| <div style="font-size: 12px; color: #666; text-transform: uppercase; | |
| letter-spacing: 1px;">{title}</div> | |
| <div style="font-size: 28px; font-weight: 700; color: #333; | |
| margin: 4px 0;">{value}</div> | |
| <div style="font-size: 11px; color: #888;">{subtitle}</div> | |
| </div> | |
| """), | |
| width=220, | |
| ) | |