Enhance report generation by adding peak lightning activity analysis and key findings section. Introduce new functions for summarizing storm activity and update report strings for improved clarity. Refactor analysis radius logic to ensure accurate boundary calculations based on distance rings.
This commit is contained in:
parent
6d6e2e4d7c
commit
29622b48fb
@ -28,6 +28,29 @@ def filter_lightning_by_distance(lightning_df, centroid_lat, centroid_lng):
|
||||
mask = dists <= max_distance
|
||||
return lightning_df.loc[mask].copy()
|
||||
|
||||
def get_peak_activity(lightning_df, centroid_lat, centroid_lng):
|
||||
"""
|
||||
Return the single busiest minute within the analysis radius.
|
||||
|
||||
Returns a dict {'when': 'DD-MM-YYYY HH:MM', 'count': int} or None when there
|
||||
is no activity to summarize.
|
||||
"""
|
||||
filtered_df = filter_lightning_by_distance(lightning_df, centroid_lat, centroid_lng)
|
||||
if len(filtered_df) == 0:
|
||||
return None
|
||||
|
||||
f = filtered_df.copy()
|
||||
if f['local_time'].dtype == 'object':
|
||||
f['local_time'] = pd.to_datetime(f['local_time'])
|
||||
|
||||
f['minute_rounded'] = f['local_time'].dt.floor('min')
|
||||
counts = f.groupby('minute_rounded').size()
|
||||
if counts.empty:
|
||||
return None
|
||||
|
||||
peak_minute = counts.idxmax()
|
||||
return {'when': peak_minute.strftime('%d-%m-%Y %H:%M'), 'count': int(counts.max())}
|
||||
|
||||
def find_activity_periods(df, min_gap_minutes=30, min_events_per_period=10):
|
||||
"""
|
||||
Find concentrated activity periods based on time gaps.
|
||||
@ -198,7 +221,7 @@ def _build_histogram_figure_for_periods(periods_chunk, max_distance_km, lang=Non
|
||||
period_str = f"{start.strftime('%d-%m-%Y %H:%M')}-{end.strftime('%d-%m-%Y %H:%M')}"
|
||||
|
||||
# Use line breaks to fit within histogram width
|
||||
return f"Date: {date_str}<br>Period: {period_str}<br>Total lightnings: {event_count}"
|
||||
return s.hist_period_title.format(date=date_str, period=period_str, total=event_count)
|
||||
|
||||
fig = make_subplots(
|
||||
rows=n_rows,
|
||||
@ -226,7 +249,7 @@ def _build_histogram_figure_for_periods(periods_chunk, max_distance_km, lang=Non
|
||||
if p_type in minute_counts.columns:
|
||||
counts = minute_counts[p_type].values
|
||||
|
||||
p_type_name = "Cloud-to-Ground" if p_type == '0' else "Intercloud"
|
||||
p_type_name = s.hist_cg_legend if p_type == '0' else s.hist_ic_legend
|
||||
|
||||
fig.add_trace(
|
||||
go.Bar(
|
||||
|
||||
@ -18,7 +18,7 @@ from docx.oxml import OxmlElement
|
||||
from docx.oxml.ns import qn
|
||||
from docx.shared import Cm, Inches, Pt, RGBColor
|
||||
|
||||
from src.analysis.histogram import create_lightning_histogram_pages
|
||||
from src.analysis.histogram import create_lightning_histogram_pages, get_peak_activity
|
||||
from src.analysis.risk import calculate_turbine_risks
|
||||
from src.analysis.statistics import calculate_lightning_statistics
|
||||
from src.config import config
|
||||
@ -36,7 +36,7 @@ from src.utils import (
|
||||
now_in_timezone,
|
||||
get_analysis_radius_m,
|
||||
)
|
||||
from src.analysis.geospatial import haversine_distance
|
||||
from src.analysis.geospatial import haversine_distance, haversine_distance_vectorized
|
||||
from src.visualization.maps import plot_cloud_to_ground_coordinate_plane, plot_intercloud_coordinate_plane
|
||||
from src.visualization.storm_cells import (
|
||||
create_storm_cells_map,
|
||||
@ -169,6 +169,103 @@ def _add_bullets(doc: Document, items: Iterable[str], size_pt: int = 10) -> None
|
||||
r.font.size = Pt(size_pt)
|
||||
|
||||
|
||||
def _build_key_findings(
|
||||
lightning_df: pd.DataFrame,
|
||||
turbine_df: pd.DataFrame,
|
||||
stats: dict[str, Any],
|
||||
centroid_lat: float,
|
||||
centroid_lng: float,
|
||||
s,
|
||||
storm_cell_count: int = 0,
|
||||
storm_closest_to_centroid_km: float | None = None,
|
||||
) -> tuple[list[str], str]:
|
||||
radius_m = get_analysis_radius_m()
|
||||
if radius_m <= 0:
|
||||
radius_m = int(max(config.distance_rings)) if config.distance_rings else 0
|
||||
innermost_km = (float(min(config.distance_rings)) / 1000.0) if config.distance_rings else (radius_m / 1000.0)
|
||||
|
||||
turbine_count = len(turbine_df)
|
||||
total_events = int(stats.get("total_events", 0) or 0)
|
||||
|
||||
cg_total = 0
|
||||
ic_total = 0
|
||||
innermost_count = 0
|
||||
within_df = lightning_df.iloc[0:0]
|
||||
if len(lightning_df) > 0 and radius_m > 0:
|
||||
dists_m = haversine_distance_vectorized(
|
||||
centroid_lat,
|
||||
centroid_lng,
|
||||
lightning_df["lat"].values,
|
||||
lightning_df["lng"].values,
|
||||
)
|
||||
within_mask = dists_m <= radius_m
|
||||
within_df = lightning_df.loc[within_mask].copy()
|
||||
within_dists = dists_m[within_mask]
|
||||
ptype = within_df["p_type"].astype(str)
|
||||
cg_total = int((ptype == "0").sum())
|
||||
ic_total = int((ptype == "1").sum())
|
||||
innermost_count = int((within_dists <= (innermost_km * 1000.0)).sum())
|
||||
|
||||
strongest_ka: float | None = None
|
||||
nearest_km: float | None = None
|
||||
nearest_turbine: str | None = None
|
||||
if len(within_df) > 0 and "current" in within_df.columns:
|
||||
cg_df = within_df[within_df["p_type"].astype(str) == "0"]
|
||||
if len(cg_df) > 0:
|
||||
currents = pd.to_numeric(cg_df["current"], errors="coerce").abs()
|
||||
if bool(currents.notna().any()):
|
||||
strongest_ka = float(currents.max()) / 1000.0
|
||||
if turbine_count > 0 and {"lat", "lng"}.issubset(turbine_df.columns):
|
||||
cg_lat = cg_df["lat"].values
|
||||
cg_lng = cg_df["lng"].values
|
||||
best = float("inf")
|
||||
best_name: str | None = None
|
||||
for _, t in turbine_df.iterrows():
|
||||
d = haversine_distance_vectorized(float(t["lat"]), float(t["lng"]), cg_lat, cg_lng)
|
||||
m = float(d.min())
|
||||
if m < best:
|
||||
best = m
|
||||
best_name = str(t.get("name", "N/A"))
|
||||
if best != float("inf"):
|
||||
nearest_km = best / 1000.0
|
||||
nearest_turbine = best_name
|
||||
|
||||
high_plus_count = 0
|
||||
if "risk_log" in turbine_df.columns and turbine_count > 0:
|
||||
high_plus_count = int((pd.to_numeric(turbine_df["risk_log"], errors="coerce") >= 0.8).sum())
|
||||
|
||||
active_days = len(stats.get("daily_lightning_by_rings") or {})
|
||||
peak = get_peak_activity(lightning_df, centroid_lat, centroid_lng)
|
||||
|
||||
bullets: list[str] = [
|
||||
s.kf_total_events.format(total=total_events, cg=cg_total, ic=ic_total),
|
||||
s.kf_innermost.format(radius=innermost_km, count=innermost_count),
|
||||
]
|
||||
if peak:
|
||||
bullets.append(s.kf_peak_activity.format(when=peak["when"], count=peak["count"]))
|
||||
if strongest_ka is not None:
|
||||
bullets.append(s.kf_strongest_strike.format(ka=strongest_ka))
|
||||
if nearest_km is not None and nearest_turbine is not None:
|
||||
bullets.append(s.kf_nearest_strike.format(distance=nearest_km, name=nearest_turbine))
|
||||
if turbine_count > 0:
|
||||
bullets.append(s.kf_high_risk_turbines.format(count=high_plus_count, total=turbine_count))
|
||||
if active_days > 1:
|
||||
bullets.append(s.kf_active_days.format(days=active_days))
|
||||
if storm_cell_count > 0 and storm_closest_to_centroid_km is not None:
|
||||
bullets.append(
|
||||
s.kf_storm_distance.format(count=storm_cell_count, distance=storm_closest_to_centroid_km)
|
||||
)
|
||||
|
||||
if total_events == 0:
|
||||
recommendation = s.recommendation_no_activity
|
||||
elif high_plus_count > 0:
|
||||
recommendation = s.recommendation_high_plus.format(count=high_plus_count, total=turbine_count)
|
||||
else:
|
||||
recommendation = s.recommendation_routine
|
||||
|
||||
return bullets, recommendation
|
||||
|
||||
|
||||
def _lighten_hex_color(hex_color: str, factor: float = 0.6) -> str:
|
||||
s = hex_color.strip()
|
||||
if s.startswith("#"):
|
||||
@ -602,6 +699,26 @@ def create_docx_report(
|
||||
storm_closest_distance_km: float | None = None
|
||||
storm_over_threshold_km = 1.0
|
||||
|
||||
# Closest storm-cell centroid to the farm centroid (independent of turbines): lets the
|
||||
# report comment on detected storms even when no turbine was directly affected.
|
||||
storm_cell_count = len(storm_data) if storm_data else 0
|
||||
storm_closest_to_centroid_km: float | None = None
|
||||
if storm_data:
|
||||
min_centroid_dist = float("inf")
|
||||
for storm in storm_data:
|
||||
wkt_string = storm.get("cell_polygon_wkt", "") or ""
|
||||
if not wkt_string:
|
||||
continue
|
||||
centroid = calculate_storm_cell_centroid(wkt_string)
|
||||
if centroid is None:
|
||||
continue
|
||||
c_lat, c_lng = centroid
|
||||
dist_km = haversine_distance(centroid_lat, centroid_lng, c_lat, c_lng) / 1000.0
|
||||
if dist_km < min_centroid_dist:
|
||||
min_centroid_dist = dist_km
|
||||
if min_centroid_dist != float("inf"):
|
||||
storm_closest_to_centroid_km = min_centroid_dist
|
||||
|
||||
if storm_data and top_turbine_name is not None and "risk_log" in turbine_df.columns and len(turbine_df) > 0:
|
||||
# Use the coordinates of the highest-risk turbine for storm-distance calculations.
|
||||
if "lat" in turbine_df.columns and "lng" in turbine_df.columns and top_turbine_name is not None:
|
||||
@ -645,6 +762,9 @@ def create_docx_report(
|
||||
"storm_near_turbine_count": storm_near_turbine_count,
|
||||
"storm_closest_distance_km": round(float(storm_closest_distance_km), 1) if storm_closest_distance_km is not None else None,
|
||||
"storm_over_threshold_km": storm_over_threshold_km,
|
||||
"storm_detected": storm_cell_count > 0,
|
||||
"storm_cell_count": storm_cell_count,
|
||||
"storm_closest_to_centroid_km": round(float(storm_closest_to_centroid_km), 1) if storm_closest_to_centroid_km is not None else None,
|
||||
"turbine_risk_counts": turbine_risk_counts,
|
||||
"storm_summary": storm_summary,
|
||||
}
|
||||
@ -656,16 +776,31 @@ def create_docx_report(
|
||||
if commentary_text.startswith(prefix):
|
||||
commentary_text = commentary_text[len(prefix):].strip()
|
||||
break
|
||||
key_findings, recommendation_text = _build_key_findings(
|
||||
lightning_df,
|
||||
turbine_df,
|
||||
stats,
|
||||
centroid_lat,
|
||||
centroid_lng,
|
||||
s,
|
||||
storm_cell_count=storm_cell_count,
|
||||
storm_closest_to_centroid_km=storm_closest_to_centroid_km,
|
||||
)
|
||||
|
||||
# Keep the commentary close to the Turbine Information table (same page if possible).
|
||||
doc.add_paragraph("")
|
||||
doc.add_paragraph("")
|
||||
_add_title(doc, s.report_summary, size_pt=14, bold=True, align=WD_ALIGN_PARAGRAPH.LEFT)
|
||||
_add_paragraph(doc, s.key_findings_heading, size_pt=11, bold=True)
|
||||
_add_bullets(doc, key_findings, size_pt=10)
|
||||
# DOCX line spacing for the Gemini commentary paragraph.
|
||||
p = doc.add_paragraph()
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
r = p.add_run(commentary_text)
|
||||
r.font.size = Pt(10)
|
||||
p.paragraph_format.line_spacing = 1.5
|
||||
_add_paragraph(doc, s.recommendation_heading, size_pt=11, bold=True)
|
||||
_add_paragraph(doc, recommendation_text, size_pt=10)
|
||||
|
||||
doc.add_page_break()
|
||||
|
||||
@ -692,6 +827,7 @@ def create_docx_report(
|
||||
_add_title(doc, s.cloud_to_ground_current_vs_distance, size_pt=14)
|
||||
if start_date and end_date:
|
||||
_add_paragraph(doc, f"{start_date} - {end_date}", size_pt=10)
|
||||
_add_paragraph(doc, s.cg_current_vs_distance_desc, size_pt=10)
|
||||
cg_chart = _build_current_vs_distance_chart(
|
||||
lightning_df,
|
||||
pre["dists_km"],
|
||||
@ -717,6 +853,7 @@ def create_docx_report(
|
||||
_add_title(doc, s.intercloud_current_vs_distance, size_pt=14)
|
||||
if start_date and end_date:
|
||||
_add_paragraph(doc, f"{start_date} - {end_date}", size_pt=10)
|
||||
_add_paragraph(doc, s.ic_current_vs_distance_desc, size_pt=10)
|
||||
ic_chart = _build_current_vs_distance_chart(
|
||||
lightning_df,
|
||||
pre["dists_km"],
|
||||
@ -734,6 +871,7 @@ def create_docx_report(
|
||||
risk_table_data, risk_row_colors = build_risk_table_data(turbine_df, lang)
|
||||
if risk_table_data and len(risk_table_data) > 1:
|
||||
_add_title(doc, s.turbine_risk_assessment, size_pt=14)
|
||||
_add_paragraph(doc, s.turbine_risk_assessment_desc, size_pt=10)
|
||||
_add_table(doc, risk_table_data, row_colors=risk_row_colors)
|
||||
doc.add_page_break()
|
||||
|
||||
@ -797,8 +935,6 @@ def create_docx_report(
|
||||
size_pt=9,
|
||||
)
|
||||
|
||||
_add_paragraph(doc, s.ring_section_intro, size_pt=10)
|
||||
_add_paragraph(doc, s.ring_closer_risk.format(closest=closest_km), size_pt=10)
|
||||
ring_items: list[str] = []
|
||||
for ring_name, ring_data in sorted((stats.get("lightning_by_distance_rings") or {}).items()):
|
||||
if ring_data.get("total", 0) > 0:
|
||||
@ -854,9 +990,6 @@ def create_docx_report(
|
||||
size_pt=10,
|
||||
)
|
||||
|
||||
_add_paragraph(doc, s.avg_direction.format(value=summary.get("avg_direction", 0)), size_pt=10)
|
||||
_add_paragraph(doc, s.avg_speed.format(value=summary.get("avg_speed", 0)), size_pt=10)
|
||||
|
||||
daily_breakdown: dict[str, int] = summary.get("daily_breakdown", {}) or {}
|
||||
_add_paragraph(doc, s.daily_storm_breakdown, size_pt=10, bold=True)
|
||||
sorted_days = sorted(daily_breakdown.keys())
|
||||
|
||||
@ -104,6 +104,8 @@ def build_gemini_prompt(context: dict[str, Any], language: ReportLanguage | None
|
||||
storm_near_turbine_count = context.get("storm_near_turbine_count")
|
||||
storm_closest_distance_km = context.get("storm_closest_distance_km")
|
||||
storm_over_threshold_km = context.get("storm_over_threshold_km", 1.0)
|
||||
storm_detected = context.get("storm_detected")
|
||||
storm_closest_to_centroid_km = context.get("storm_closest_to_centroid_km")
|
||||
|
||||
ring_lines = _ring_lines(context, s)
|
||||
storm_lines = _storm_lines(context)
|
||||
@ -142,6 +144,8 @@ def build_gemini_prompt(context: dict[str, Any], language: ReportLanguage | None
|
||||
f"- storm_near_turbine_count: {storm_near_turbine_count}\n"
|
||||
f"- storm_closest_distance_km: {storm_closest_distance_km}\n"
|
||||
f"- storm_over_threshold_km: {storm_over_threshold_km}\n"
|
||||
f"- storm_detected: {storm_detected}\n"
|
||||
f"- storm_closest_to_centroid_km: {storm_closest_to_centroid_km}\n"
|
||||
+ (f"\n- storm_summary:\n{chr(10).join(storm_lines)}" if storm_lines else "\n- storm_summary: not available")
|
||||
+ "\n\n"
|
||||
"Paragraf gereksinimleri:\n"
|
||||
@ -150,7 +154,9 @@ def build_gemini_prompt(context: dict[str, Any], language: ReportLanguage | None
|
||||
"- is_single_turbine_report true ise: \"{top_turbine_name} türbini için log-risk skoru ... sınıfındadır\" gibi doğal bir ifade kullan.\n"
|
||||
"- is_single_turbine_report false ise: analiz alanında en yüksek riskli türbini ve risk sınıfını belirt.\n"
|
||||
"- top_turbine_name adını aynen kullan ve max_risk_definition ile ilişkilendir.\n"
|
||||
"- Fırtına hücresi etkileşimini uygun olduğunda belirt; veri yoksa kısaca belirt.\n"
|
||||
"- Fırtına hücresi etkileşimini yalnızca veri varken belirt.\n"
|
||||
"- Fırtına tespit edildiyse (storm_detected true) ancak hiçbir türbinin üzerine gelmediyse, en yakın fırtına hücresinin merkeze uzaklığını (storm_closest_to_centroid_km) belirt.\n"
|
||||
"- storm_detected false ise fırtınadan hiç bahsetme.\n"
|
||||
"- Sayıları yuvarla: yoğunluk 3 ondalık, log risk 2 ondalık, mesafeler 1 ondalık km, sayılar tam sayı.\n"
|
||||
"- Ton analitik, net ve alarmist olmayan olsun.\n"
|
||||
"\n"
|
||||
@ -189,6 +195,8 @@ def build_gemini_prompt(context: dict[str, Any], language: ReportLanguage | None
|
||||
f"- storm_near_turbine_count: {storm_near_turbine_count}\n"
|
||||
f"- storm_closest_distance_km: {storm_closest_distance_km}\n"
|
||||
f"- storm_over_threshold_km: {storm_over_threshold_km}\n"
|
||||
f"- storm_detected: {storm_detected}\n"
|
||||
f"- storm_closest_to_centroid_km: {storm_closest_to_centroid_km}\n"
|
||||
+ (f"\n- storm_summary:\n{chr(10).join(storm_lines)}" if storm_lines else "\n- storm_summary: not available")
|
||||
+ "\n\n"
|
||||
"Requirements for the paragraph:\n"
|
||||
@ -202,6 +210,8 @@ def build_gemini_prompt(context: dict[str, Any], language: ReportLanguage | None
|
||||
"- Mention storm-cell interaction with the turbine when storm information is available:\n"
|
||||
" - If storm_over_turbine is true: say that storm cells were very close to/over the turbine (based on centroid distance <= storm_over_threshold_km).\n"
|
||||
" - If storm_over_turbine is false and storm_closest_distance_km is provided: say the closest storm cell centroid came within storm_closest_distance_km km of the turbine.\n"
|
||||
" - If storms were detected (storm_detected true) but did not pass over any turbine, mention the closest storm cell's distance to the centroid (storm_closest_to_centroid_km).\n"
|
||||
" - If storm_detected is false, do not mention storms at all.\n"
|
||||
"- If storm_summary is available, mention total storm cells and at least one severity count.\n"
|
||||
"- Round numeric values as follows (use the rounded values you are given, avoid long decimals):\n"
|
||||
" - lightning density to 3 decimals (events/km²)\n"
|
||||
@ -231,6 +241,7 @@ def fallback_commentary(context: dict[str, Any], language: ReportLanguage | None
|
||||
storm_closest_distance_km = context.get("storm_closest_distance_km")
|
||||
storm_over_threshold_km = context.get("storm_over_threshold_km", 1.0)
|
||||
storm_near_turbine_count = context.get("storm_near_turbine_count")
|
||||
storm_closest_to_centroid_km = context.get("storm_closest_to_centroid_km")
|
||||
|
||||
outermost_ring = top_rings[-1] if top_rings else None
|
||||
best_ring = top_rings[0] if top_rings else None
|
||||
@ -243,13 +254,14 @@ def fallback_commentary(context: dict[str, Any], language: ReportLanguage | None
|
||||
if severity_counts:
|
||||
severity = max(severity_counts.items(), key=lambda kv: kv[1])[0]
|
||||
count = severity_counts.get(severity, 0)
|
||||
severity_label = s.storm_severity_names.get(str(severity).strip().lower(), str(severity).title())
|
||||
if s.gemini_write_turkish:
|
||||
storm_line = (
|
||||
f"Toplam {total_cells} fırtına hücresi kaydedilmiş; en fazla {count} hücre {severity} şiddetindedir."
|
||||
f"Toplam {total_cells} fırtına hücresi kaydedilmiş; en fazla {count} hücre {severity_label} şiddetindedir."
|
||||
)
|
||||
else:
|
||||
storm_line = (
|
||||
f"Storm data indicates {total_cells} storm cells, with the highest share in {severity} ({count} cells)."
|
||||
f"Storm data indicates {total_cells} storm cells, with the highest share in {severity_label} ({count} cells)."
|
||||
)
|
||||
elif s.gemini_write_turkish:
|
||||
storm_line = f"Toplam {total_cells} fırtına hücresi kaydedilmiştir."
|
||||
@ -289,13 +301,18 @@ def fallback_commentary(context: dict[str, Any], language: ReportLanguage | None
|
||||
f"Fırtına hücrelerinden {storm_near_turbine_count} tanesinin merkezi türbine "
|
||||
f"{storm_over_threshold_km:.1f} km'den yakın konumlanmıştır. "
|
||||
)
|
||||
elif isinstance(storm_closest_to_centroid_km, (int, float)):
|
||||
storm_interaction_sentence = (
|
||||
f"Tespit edilen fırtına hücreleri herhangi bir türbinin üzerine denk gelmemiş; "
|
||||
f"en yakın hücre merkeze {storm_closest_to_centroid_km:.1f} km mesafeye kadar yaklaşmıştır. "
|
||||
)
|
||||
elif isinstance(storm_closest_distance_km, (int, float)):
|
||||
storm_interaction_sentence = (
|
||||
f"En yakın fırtına hücresi merkezi türbine {storm_closest_distance_km:.1f} km mesafededir. "
|
||||
)
|
||||
else:
|
||||
storm_interaction_sentence = ""
|
||||
storm_severity_sentence = storm_line if storm_line else "Bu raporda fırtına hücresi verisi bulunmamaktadır."
|
||||
storm_severity_sentence = storm_line
|
||||
else:
|
||||
radius_txt = f"within {analysis_radius_km:.1f} km" if isinstance(analysis_radius_km, (int, float)) else ""
|
||||
events_txt = f"{total_events} total lightning events" if total_events is not None else "N/A"
|
||||
@ -317,13 +334,17 @@ def fallback_commentary(context: dict[str, Any], language: ReportLanguage | None
|
||||
storm_interaction_sentence = (
|
||||
f"Storm interaction: storm-cell centroids came within {storm_over_threshold_km:.1f} km of the turbine (count={storm_near_turbine_count}). "
|
||||
)
|
||||
elif isinstance(storm_closest_to_centroid_km, (int, float)):
|
||||
storm_interaction_sentence = (
|
||||
f"Detected storm cells did not pass directly over any turbine; the nearest cell came within {storm_closest_to_centroid_km:.1f} km of the centroid. "
|
||||
)
|
||||
elif isinstance(storm_closest_distance_km, (int, float)):
|
||||
storm_interaction_sentence = (
|
||||
f"Storm interaction: the closest storm-cell centroid came within {storm_closest_distance_km:.1f} km of the turbine. "
|
||||
)
|
||||
else:
|
||||
storm_interaction_sentence = ""
|
||||
storm_severity_sentence = storm_line if storm_line else "Storm severity distribution is not available for this report."
|
||||
storm_severity_sentence = storm_line
|
||||
|
||||
return (paragraph_intro + turbine_sentence + method_sentence + storm_interaction_sentence + storm_severity_sentence).strip()
|
||||
|
||||
|
||||
@ -37,13 +37,29 @@ class ReportStrings:
|
||||
turbine_information: str
|
||||
turbine_information_desc: str
|
||||
report_summary: str
|
||||
key_findings_heading: str
|
||||
kf_total_events: str
|
||||
kf_innermost: str
|
||||
kf_peak_activity: str
|
||||
kf_strongest_strike: str
|
||||
kf_nearest_strike: str
|
||||
kf_high_risk_turbines: str
|
||||
kf_active_days: str
|
||||
kf_storm_distance: str
|
||||
recommendation_heading: str
|
||||
recommendation_high_plus: str
|
||||
recommendation_routine: str
|
||||
recommendation_no_activity: str
|
||||
cloud_to_ground_lightnings: str
|
||||
cloud_to_ground_current_vs_distance: str
|
||||
cg_chart_title: str
|
||||
cg_current_vs_distance_desc: str
|
||||
intercloud_lightnings: str
|
||||
intercloud_current_vs_distance: str
|
||||
ic_chart_title: str
|
||||
ic_current_vs_distance_desc: str
|
||||
turbine_risk_assessment: str
|
||||
turbine_risk_assessment_desc: str
|
||||
lightning_breakdown_rings: str
|
||||
period_total_events: str
|
||||
area_covered: str
|
||||
@ -192,8 +208,13 @@ class ReportStrings:
|
||||
map_cg_lightning: str
|
||||
map_ic_lightning: str
|
||||
map_distance_ring: str
|
||||
map_severity_legend: str
|
||||
storm_severity_names: dict[str, str]
|
||||
hist_minutes_from_start: str
|
||||
hist_lightning_count: str
|
||||
hist_period_title: str
|
||||
hist_cg_legend: str
|
||||
hist_ic_legend: str
|
||||
storm_cells_day_title: str
|
||||
heatmap_title: str
|
||||
heatmap_subtitle: str
|
||||
@ -223,13 +244,29 @@ _STRINGS_EN = ReportStrings(
|
||||
turbine_information="Turbine Information",
|
||||
turbine_information_desc="This table contains detailed information about all turbines in the wind farm.",
|
||||
report_summary="Report Summary",
|
||||
key_findings_heading="Key Findings",
|
||||
kf_total_events="Total events: {total} ({cg} cloud-to-ground, {ic} intercloud)",
|
||||
kf_innermost="Events within {radius:.1f} km of the centroid: {count}",
|
||||
kf_peak_activity="Peak activity: {when} ({count} events in one minute)",
|
||||
kf_strongest_strike="Strongest cloud-to-ground strike: {ka:.1f} kA",
|
||||
kf_nearest_strike="Nearest cloud-to-ground strike to a turbine: {distance:.1f} km ({name})",
|
||||
kf_high_risk_turbines="Turbines at High risk or above: {count} of {total}",
|
||||
kf_active_days="Days with recorded activity: {days}",
|
||||
kf_storm_distance="Storm cells detected: {count} (closest {distance:.1f} km from the centroid)",
|
||||
recommendation_heading="Recommendation",
|
||||
recommendation_high_plus="{count} of {total} turbines fall in the High risk category or above. Inspection of the lightning protection systems on these turbines is recommended.",
|
||||
recommendation_routine="No turbines fall in the High risk category or above; routine monitoring is sufficient for this period.",
|
||||
recommendation_no_activity="No lightning activity was recorded within the analysis area during this period.",
|
||||
cloud_to_ground_lightnings="Cloud-to-Ground Lightnings",
|
||||
cloud_to_ground_current_vs_distance="Cloud-to-Ground — Current vs Distance",
|
||||
cg_chart_title="Cloud-to-Ground Lightning — Current vs Distance from Centroid",
|
||||
cg_chart_title="Cloud-to-Ground Lightning — Current and Distance to Centroid",
|
||||
cg_current_vs_distance_desc="The chart below shows the distance to the center point and the current magnitude of the cloud-to-ground events that occurred during the analyzed time period.",
|
||||
intercloud_lightnings="Intercloud Lightnings",
|
||||
intercloud_current_vs_distance="Intercloud — Current vs Distance",
|
||||
ic_chart_title="Intercloud Lightning — Current vs Distance from Centroid",
|
||||
ic_chart_title="Intercloud Lightning — Current and Distance to Centroid",
|
||||
ic_current_vs_distance_desc="The chart below shows the distance to the center point and the current magnitude of the intercloud events that occurred during the analyzed time period.",
|
||||
turbine_risk_assessment="Turbine Risk Assessment",
|
||||
turbine_risk_assessment_desc="You can find the explanation of how the risk assessment is performed in Appendices 1, 2 and 3.",
|
||||
lightning_breakdown_rings="Lightning Breakdown by Distance Rings",
|
||||
period_total_events="Period: {start} - {end} (Total: {total} lightning events)",
|
||||
area_covered="Area covered within {radius:.1f} km radius: {area:.1f} km²",
|
||||
@ -277,7 +314,7 @@ _STRINGS_EN = ReportStrings(
|
||||
alpha_desc="α (distance decay factor): controls how risk decays with distance (smaller = slower decay)",
|
||||
included_events="Included events: only cloud-to-ground lightning (p_type = 0)",
|
||||
per_strike_contribution="Per-strike contribution: increases with |I| and decreases exponentially with distance",
|
||||
turbine_risk_sum="Turbine's risk score: sum of per-strike contributions for all included strikes (typically within the outermost distance ring)",
|
||||
turbine_risk_sum="Turbine's risk score: the sum of per-strike contributions from all cloud-to-ground strikes within the outermost distance ring",
|
||||
log_transformed="For visualization and reporting, we use the log-transformed score",
|
||||
risk_interpretation="2. Risk Score Interpretation",
|
||||
understanding_risk="Understanding Risk Score Values:",
|
||||
@ -304,7 +341,7 @@ _STRINGS_EN = ReportStrings(
|
||||
centroid_bullet_1="Farm centroid is provided by the monitoring workflow and shared by every map and table in this report",
|
||||
centroid_bullet_2="Distance rings are drawn from the farm centroid; innermost rings represent the highest proximity risk",
|
||||
centroid_bullet_3="Lightning proximity for each strike is measured from the farm centroid using the Haversine formula",
|
||||
centroid_bullet_4="The monitoring boundary defines the outer analysis radius — strikes beyond it are excluded",
|
||||
centroid_bullet_4="The analysis is limited to the outermost distance ring; strikes beyond it are excluded from all statistics and charts",
|
||||
freq_lightning_algo="5. Frequent Lightning Activity Period Detection Algorithm",
|
||||
how_period_timespans="How Period Timespans Are Determined:",
|
||||
gap_based_algo="The algorithm uses a gap-based approach to identify concentrated lightning activity periods.",
|
||||
@ -371,15 +408,20 @@ _STRINGS_EN = ReportStrings(
|
||||
map_longitude="Longitude",
|
||||
map_latitude="Latitude",
|
||||
map_legend="Legend",
|
||||
map_cg_plane_title="Cloud-to-Ground Lightning - Coordinate Plane View - Central Turbine: {name}",
|
||||
map_ic_plane_title="Intercloud Lightning - Coordinate Plane View - Central Turbine: {name}",
|
||||
map_cg_plane_title="CG Event Distribution",
|
||||
map_ic_plane_title="IC Event Distribution",
|
||||
map_wind_turbines="Wind Turbines",
|
||||
map_storm_cells="Storm Cells",
|
||||
map_cg_lightning="Cloud-to-Ground Lightning",
|
||||
map_ic_lightning="Intercloud Lightning",
|
||||
map_distance_ring="{radius:.1f}km Distance Ring",
|
||||
map_severity_legend="{severity} Severity",
|
||||
storm_severity_names={"high": "High", "medium": "Medium", "low": "Low", "unknown": "Unknown"},
|
||||
hist_minutes_from_start="Minutes from start",
|
||||
hist_lightning_count="Lightning Count",
|
||||
hist_period_title="Date: {date}<br>Period: {period}<br>Total lightnings: {total}",
|
||||
hist_cg_legend="Cloud-to-Ground",
|
||||
hist_ic_legend="Intercloud",
|
||||
storm_cells_day_title="Storm Cells - {day}",
|
||||
heatmap_title="Risk Score Heatmap",
|
||||
heatmap_subtitle="Current Magnitude vs Distance (0.1-{max_km:.1f}km) - Higher values (red) = Higher risk",
|
||||
@ -418,21 +460,37 @@ _STRINGS_TR = ReportStrings(
|
||||
turbine_information="Türbin Bilgileri",
|
||||
turbine_information_desc="Bu tablo, rüzgar çiftliğindeki tüm türbinlere ait ayrıntılı bilgileri içerir.",
|
||||
report_summary="Rapor Özeti",
|
||||
key_findings_heading="Öne Çıkan Bulgular",
|
||||
kf_total_events="Toplam olay: {total} ({cg} yıldırım, {ic} şimşek)",
|
||||
kf_innermost="Merkeze {radius:.1f} km mesafe içinde: {count} olay",
|
||||
kf_peak_activity="En yoğun an: {when} (bir dakikada {count} olay)",
|
||||
kf_strongest_strike="En güçlü yıldırım darbesi: {ka:.1f} kA",
|
||||
kf_nearest_strike="Bir türbine en yakın yıldırım: {distance:.1f} km ({name})",
|
||||
kf_high_risk_turbines="Yüksek ve üzeri riskli türbin: {total} türbinden {count} adet",
|
||||
kf_active_days="Aktivite görülen gün sayısı: {days}",
|
||||
kf_storm_distance="Tespit edilen fırtına hücresi: {count} (merkeze en yakın {distance:.1f} km)",
|
||||
recommendation_heading="Öneri",
|
||||
recommendation_high_plus="{total} türbinden {count} adedi Yüksek risk ve üzeri sınıfta yer almaktadır. Bu türbinlerin yıldırımdan korunma sistemlerinin incelenmesi önerilir.",
|
||||
recommendation_routine="Yüksek risk ve üzeri sınıfta türbin bulunmamaktadır; bu dönem için rutin izleme yeterlidir.",
|
||||
recommendation_no_activity="Bu dönemde analiz alanında yıldırım-şimşek aktivitesi kaydedilmemiştir.",
|
||||
cloud_to_ground_lightnings="Yıldırım",
|
||||
cloud_to_ground_current_vs_distance="Yıldırım — Akım ve Mesafe",
|
||||
cg_chart_title="Şimşek — Merkezden Akım ve Mesafe",
|
||||
cg_chart_title="Yıldırım — Akım ve Merkeze olan Mesafe",
|
||||
cg_current_vs_distance_desc="Aşağıdaki grafik, incelenen zaman dilimi içinde gerçekleşen yıldırımların merkez noktaya mesafesini ve akım miktarını göstermektedir.",
|
||||
intercloud_lightnings="Şimşek",
|
||||
intercloud_current_vs_distance="Şimşek — Akım ve Mesafe",
|
||||
ic_chart_title="Şimşek — Merkezden Akım ve Mesafe",
|
||||
ic_chart_title="Şimşek — Akım ve Merkeze olan Mesafe",
|
||||
ic_current_vs_distance_desc="Aşağıdaki grafik, incelenen zaman dilimi içinde gerçekleşen şimşeklerin merkez noktaya mesafesini ve akım miktarını göstermektedir.",
|
||||
turbine_risk_assessment="Türbin Risk Değerlendirmesi",
|
||||
turbine_risk_assessment_desc="Risk değerlendirmesinin nasıl yapıldığına dair açıklamayı Ek. 1, 2 ve 3'te bulabilirsiniz.",
|
||||
lightning_breakdown_rings="Mesafe Halkalarına Göre Yıldırım Dağılımı",
|
||||
period_total_events="Dönem: {start} - {end} (Toplam: {total} yıldırım olayı)",
|
||||
period_total_events="Dönem: {start} - {end} (Toplam: {total} yıldırım-şimşek olayı)",
|
||||
area_covered="{radius:.1f} km yarıçap içinde kapsanan alan: {area:.1f} km²",
|
||||
total_lightnings_radius="{radius:.1f} km yarıçap içindeki toplam yıldırım: {total} olay",
|
||||
total_density="Toplam yıldırım yoğunluğu: {density:.3f} olay/km²",
|
||||
density_calc="(Hesaplama: {total} toplam yıldırım / {area:.1f} km² alan)",
|
||||
total_lightnings_radius="{radius:.1f} km yarıçap içindeki toplam yıldırım-şimşek: {total} olay",
|
||||
total_density="Toplam yıldırım-şimşek yoğunluğu: {density:.3f} olay/km²",
|
||||
density_calc="(Hesaplama: {total} toplam yıldırım-şimşek olayı / {area:.1f} km² alan)",
|
||||
daily_density="Günlük eşdeğer yoğunluk: {density:.3f} olay/km²/gün",
|
||||
daily_density_calc="(Hesaplama: {total} toplam yıldırım / {area:.1f} km² alan / {days_label})",
|
||||
daily_density_calc="(Hesaplama: {total} toplam yıldırım-şimşek olayı / {area:.1f} km² alan / {days_label})",
|
||||
day_in_period="dönemde 1 gün",
|
||||
days_in_period="dönemde {days} gün",
|
||||
ring_section_intro="Bu bölüm, analiz dönemindeki yıldırım olaylarını türbinlere olan mesafeye göre gösterir.",
|
||||
@ -440,7 +498,7 @@ _STRINGS_TR = ReportStrings(
|
||||
ring_item="{ring}: {total} toplam ({cg} yıldırım, {ic} şimşek)",
|
||||
frequent_lightning_report="Sık Yıldırım Aktivite Raporu",
|
||||
lightning_activity_overview="Yıldırım Aktivite Genel Bakış:",
|
||||
histogram_overview="Aşağıdaki grafik, {radius:.1f} km yarıçap içindeki zaman içi yıldırım aktivite örüntülerini göstererek yüksek riskli dönemleri ve yoğun fırtına olaylarını belirlemeye yardımcı olur. Tepe noktaları dikkat gerektiren yoğun yıldırım aktivitesini gösterir.",
|
||||
histogram_overview="Aşağıdaki grafik, {radius:.1f} km yarıçap içindeki yıldırım-şimşek aktivitesinin zaman içindeki dağılımını göstermekte ve yüksek riskli anların belirlenmesine yardımcı olmaktadır.",
|
||||
histogram_appendix_ref='Ayrıntılı bilgi için ek bölümdeki "Sık Yıldırım Aktivite Dönemi Tespit Algoritması" bölümüne bakınız.',
|
||||
frequent_lightning_periods="Sık Yıldırım Aktivite Dönemleri",
|
||||
storm_cells_analysis_summary="Fırtına Hücreleri Analiz Özeti",
|
||||
@ -463,16 +521,16 @@ _STRINGS_TR = ReportStrings(
|
||||
appendix="Ek",
|
||||
risk_calc_method="1. Risk Hesaplama Yöntemi",
|
||||
how_risk_determined="Risk Skorları Nasıl Belirlenir:",
|
||||
risk_exposure_1="Bir türbinin risk skoru, analiz döneminde yıldırıma maruz kalmasını temsil eder.",
|
||||
risk_exposure_2="Her yıldırım darbesi, türbine olan mesafe ve akım büyüklüğüne göre bir risk katkısı yapar; bu katkılar toplanır.",
|
||||
p0_desc="P0 (temel faktör): her darbenin katkısına uygulanan temel ölçekleme",
|
||||
risk_exposure_1="Bir türbinin risk skoru, analiz dönemi boyunca buluttan yere düşen yıldırımlara ne ölçüde maruz kaldığını gösterir.",
|
||||
risk_exposure_2="Her yıldırım darbesi, türbine olan mesafesine ve akım büyüklüğüne bağlı olarak bir risk katkısı sağlar; bu katkıların tümü toplanarak skor elde edilir.",
|
||||
p0_desc="P0 (temel faktör): her darbenin katkısına uygulanan temel ölçek katsayısı",
|
||||
current_weight_desc="current_weight: akım büyüklüğünün riski ne kadar artırdığını kontrol eder (büyük = |I| üzerinde daha fazla ağırlık)",
|
||||
distance_desc="Mesafe (d): türbin-darbe mesafesi (kilometre)",
|
||||
current_desc="Akım (|I|): mutlak akım büyüklüğü |I| (amper)",
|
||||
alpha_desc="α (mesafe azalma faktörü): riskin mesafeyle nasıl azaldığını kontrol eder (küçük = daha yavaş azalma)",
|
||||
included_events="Dahil edilen olaylar: yalnızca yıldırım (p_type = 0)",
|
||||
included_events="Dahil edilen olaylar: yalnızca buluttan yere düşen yıldırımlar (p_type = 0)",
|
||||
per_strike_contribution="Darbe başına katkı: |I| ile artar, mesafe ile üstel olarak azalır",
|
||||
turbine_risk_sum="Türbin risk skoru: dahil edilen tüm darbelerin katkılarının toplamı (genellikle en dış mesafe halkası içinde)",
|
||||
turbine_risk_sum="Türbin risk skoru: en dış mesafe halkası içindeki tüm yıldırım (buluttan yere) darbelerinin katkılarının toplamıdır",
|
||||
log_transformed="Görselleştirme ve raporlama için log dönüşümlü skor kullanılır",
|
||||
risk_interpretation="2. Risk Skoru Yorumu",
|
||||
understanding_risk="Risk Skoru Değerlerini Anlama:",
|
||||
@ -499,17 +557,17 @@ _STRINGS_TR = ReportStrings(
|
||||
centroid_bullet_1="Çiftlik merkezi izleme iş akışı tarafından sağlanır ve bu rapordaki her harita ve tabloda kullanılır",
|
||||
centroid_bullet_2="Mesafe halkaları çiftlik merkezinden çizilir; en iç halkalar en yüksek yakınlık riskini temsil eder",
|
||||
centroid_bullet_3="Her darbenin yakınlığı, çiftlik merkezinden Haversine formülü ile ölçülür",
|
||||
centroid_bullet_4="İzleme sınırı dış analiz yarıçapını tanımlar — dışındaki darbeler hariç tutulur",
|
||||
centroid_bullet_4="Analiz, en dış mesafe halkası ile sınırlıdır; bu halkanın dışında kalan darbeler tüm istatistik ve grafiklerin dışında bırakılır",
|
||||
freq_lightning_algo="5. Sık Yıldırım Aktivite Dönemi Tespit Algoritması",
|
||||
how_period_timespans="Dönem Zaman Aralıkları Nasıl Belirlenir:",
|
||||
gap_based_algo="Algoritma, yoğun yıldırım aktivite dönemlerini belirlemek için boşluk tabanlı bir yaklaşım kullanır.",
|
||||
gap_based_algo="Algoritma, yoğun yıldırım aktivitesi dönemlerini belirlemek için olaylar arasındaki zaman boşluklarına dayalı bir yaklaşım kullanır.",
|
||||
step_by_step="Adım Adım Süreç:",
|
||||
algo_chronological="Kronolojik Sıralama: Tüm yıldırım olayları zaman damgasına (local_time) göre sıralanır",
|
||||
algo_gap="Boşluk Hesaplama: Ardışık yıldırım olayları arasındaki zaman farkları hesaplanır",
|
||||
algo_period_boundary="Dönem Sınırı Tespiti: Ardışık iki olay arasındaki boşluk {gap} dakikayı aştığında bir dönem biter ve diğeri başlar",
|
||||
algo_period_validation="Dönem Doğrulama: Yalnızca ≥{min_events} yıldırım olayı içeren dönemler anlamlı kabul edilir",
|
||||
algo_timespan="Zaman Aralığı Tanımı: Başlangıç = ilk olay; Bitiş = son olay; Süre ilkten sona gerçek süredir",
|
||||
algo_peak="Tepe Alt-Dönem Tespiti: 3 dakikalık hareketli ortalama; aktivite ortalama + 1 standart sapmayı aştığında tepe; histogramda sarı ile vurgulanır",
|
||||
algo_timespan="Zaman Aralığı Tanımı: Başlangıç, dönemdeki ilk olay; bitiş ise son olaydır. Süre, ilk olaydan son olaya kadar geçen gerçek zamandır",
|
||||
algo_peak="Tepe Alt-Dönem Tespiti: 3 dakikalık hareketli ortalama kullanılır; aktivite, ortalamanın 1 standart sapma üzerine çıktığında tepe dönem olarak kabul edilir ve histogramda sarı ile vurgulanır",
|
||||
entln_title="6. EarthNetworks Toplam Yıldırım Ağı (ENTLN)",
|
||||
entln_block_1="Bu rapordaki yıldırım verileri doğrudan EarthNetworks Toplam Yıldırım Ağı'ndan (ENTLN) alınmıştır; hem bulut içi (şimşek) hem buluttan yere (yıldırım) darbeleri izler. ENTLN, küresel ölçekte konuşlandırılan ilk şimşek ve yıldırım tespit ağıdır. 1.500'den fazla sensör içerir.",
|
||||
entln_block_2="Ulusal Hava Durumu Servisi, Hava Kuvvetleri Hava Durumu Ajansı ve kamu güvenliği, acil müdahale, havaalanları ve enerji kuruluşları gibi birçok kurum ENTLN bilgisine güvenir.",
|
||||
@ -566,15 +624,20 @@ _STRINGS_TR = ReportStrings(
|
||||
map_longitude="Boylam",
|
||||
map_latitude="Enlem",
|
||||
map_legend="Gösterge",
|
||||
map_cg_plane_title="Yıldırım - Koordinat Düzlemi - Merkez Türbin: {name}",
|
||||
map_ic_plane_title="Şimşek - Koordinat Düzlemi - Merkez Türbin: {name}",
|
||||
map_cg_plane_title="Yıldırım Dağılımı",
|
||||
map_ic_plane_title="Şimşek Dağılımı",
|
||||
map_wind_turbines="Rüzgar Türbinleri",
|
||||
map_storm_cells="Fırtına Hücreleri",
|
||||
map_cg_lightning="Yıldırım",
|
||||
map_ic_lightning="Şimşek",
|
||||
map_distance_ring="{radius:.1f} km Mesafe Halkası",
|
||||
hist_minutes_from_start="Başlangıçtan dakika",
|
||||
map_severity_legend="{severity} Şiddet",
|
||||
storm_severity_names={"high": "Yüksek", "medium": "Orta", "low": "Düşük", "unknown": "Bilinmeyen"},
|
||||
hist_minutes_from_start="Başlangıçtan itibaren geçen dakika",
|
||||
hist_lightning_count="Yıldırım Sayısı",
|
||||
hist_period_title="Tarih: {date}<br>Dönem: {period}<br>Toplam yıldırım-şimşek: {total}",
|
||||
hist_cg_legend="Yıldırım",
|
||||
hist_ic_legend="Şimşek",
|
||||
storm_cells_day_title="Fırtına Hücreleri - {day}",
|
||||
heatmap_title="Risk Skoru Isı Haritası",
|
||||
heatmap_subtitle="Akım Büyüklüğü ve Mesafe (0.1-{max_km:.1f} km) - Yüksek değerler (kırmızı) = Yüksek risk",
|
||||
|
||||
@ -130,11 +130,14 @@ def format_period_display_for_report(start_value: Optional[str], end_value: Opti
|
||||
|
||||
def get_analysis_radius_m() -> int:
|
||||
from .config import config
|
||||
rings = config.distance_rings or []
|
||||
outermost_ring = int(max(rings)) if rings else 0
|
||||
boundary = config.analysis_boundary_m
|
||||
if isinstance(boundary, (int, float)) and boundary > 0:
|
||||
if outermost_ring > 0:
|
||||
return min(int(boundary), outermost_ring)
|
||||
return int(boundary)
|
||||
rings = config.distance_rings or []
|
||||
return int(max(rings)) if rings else 0
|
||||
return outermost_ring
|
||||
|
||||
def get_turbine_color_by_fixed_intervals(risk_log_value: float) -> str:
|
||||
"""
|
||||
|
||||
@ -170,7 +170,7 @@ def create_storm_cells_coordinate_plane(storm_data: List[Dict], turbine_df: pd.D
|
||||
mode='lines',
|
||||
line=dict(color=color, width=4),
|
||||
opacity=0.6,
|
||||
name=f'{radius/1000:.0f}km Ring',
|
||||
name=s.map_distance_ring.format(radius=radius / 1000),
|
||||
showlegend=True
|
||||
))
|
||||
|
||||
@ -195,7 +195,7 @@ def create_storm_cells_coordinate_plane(storm_data: List[Dict], turbine_df: pd.D
|
||||
text=turbine_df['name'].tolist(),
|
||||
textfont=dict(size=12, color='black'),
|
||||
textposition='middle center',
|
||||
name='Wind Turbines',
|
||||
name=s.map_wind_turbines,
|
||||
showlegend=True,
|
||||
hovertemplate=(
|
||||
"<b>Wind Turbine</b><br>"
|
||||
@ -274,7 +274,9 @@ def create_storm_cells_coordinate_plane(storm_data: List[Dict], turbine_df: pd.D
|
||||
y=[None],
|
||||
mode='lines',
|
||||
line=dict(color=color, width=3),
|
||||
name=f"{severity.title()} Severity",
|
||||
name=s.map_severity_legend.format(
|
||||
severity=s.storm_severity_names.get(severity, severity.title())
|
||||
),
|
||||
showlegend=True,
|
||||
hoverinfo='skip' # No hover for legend entries
|
||||
))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user