Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -36,10 +36,10 @@ def _(mo):
|
|
| 36 |
upload_species,
|
| 37 |
upload_decomp
|
| 38 |
], align="center")
|
| 39 |
-
|
| 40 |
# Place as the last statement to ensure Marimo renders it!
|
| 41 |
upload_ui
|
| 42 |
-
return upload_decomp, upload_species,
|
| 43 |
|
| 44 |
|
| 45 |
@app.cell
|
|
@@ -123,7 +123,7 @@ def _(ALL_SITES, mo, species_data):
|
|
| 123 |
|
| 124 |
|
| 125 |
@app.cell
|
| 126 |
-
def _(drop_comp1, drop_comp2, drop_single, mo
|
| 127 |
# CSS styling
|
| 128 |
dropdown_styles = mo.Html("""
|
| 129 |
<style>
|
|
@@ -174,19 +174,22 @@ def _(drop_comp1, drop_comp2, drop_single, mo, slider_count):
|
|
| 174 |
"Individual Site": drop_single,
|
| 175 |
"Compare Sites": mo.hstack([drop_comp1, drop_comp2], gap=4)
|
| 176 |
})
|
|
|
|
|
|
|
| 177 |
|
|
|
|
|
|
|
| 178 |
# Build the final control panel
|
| 179 |
controls = mo.vstack([
|
| 180 |
dropdown_styles,
|
| 181 |
tabs,
|
| 182 |
mo.hstack([slider_count], justify="center")
|
| 183 |
], align="center", gap=4)
|
| 184 |
-
|
| 185 |
-
return controls, tabs
|
| 186 |
|
| 187 |
|
| 188 |
@app.cell
|
| 189 |
-
def _(math):
|
| 190 |
# Sunburst chart: dimensions + position
|
| 191 |
CX, CY = 500, 380
|
| 192 |
MAX_RADIUS = 370
|
|
@@ -218,7 +221,7 @@ def _(math):
|
|
| 218 |
large_arc = "0" if end_angle - start_angle <= 180 else "1"
|
| 219 |
return f"M {p1[0]} {p1[1]} A {r_outer} {r_outer} 0 {large_arc} 1 {p2[0]} {p2[1]} L {p3[0]} {p3[1]} A {r_inner} {r_inner} 0 {large_arc} 0 {p4[0]} {p4[1]} Z"
|
| 220 |
|
| 221 |
-
def make_tooltip(
|
| 222 |
# Bound the tooltip coordinates so it doesn't clip off the 1000x1000 SVG canvas
|
| 223 |
tx = max(10, min(tx, 1000 - TW - 10))
|
| 224 |
ty = max(10, min(ty, 1000 - TH - 10))
|
|
@@ -226,8 +229,9 @@ def _(math):
|
|
| 226 |
safe_name = species_name.strip()[:38]
|
| 227 |
yield_str = f"Average yield: {avg_yield:.1f} kg ha\u207b\u00b9"
|
| 228 |
sites_str = f"Occurs in: {num_sites} site(s)"
|
|
|
|
| 229 |
return f"""
|
| 230 |
-
<g id="
|
| 231 |
<rect width="{TW}" height="{TH}" rx="10" ry="10"
|
| 232 |
fill="#1e1e2e" stroke="#45475a" stroke-width="1.5"
|
| 233 |
filter="url(#tipshadow)"/>
|
|
@@ -241,7 +245,7 @@ def _(math):
|
|
| 241 |
<text x="12" y="92" font-family="Inter,sans-serif" font-size="12"
|
| 242 |
fill="#f9e2af">{sites_str}</text>
|
| 243 |
</g>"""
|
| 244 |
-
|
| 245 |
return (
|
| 246 |
CX,
|
| 247 |
CY,
|
|
@@ -308,13 +312,14 @@ def _(
|
|
| 308 |
tooltip_elements = []
|
| 309 |
css_hover_rules = []
|
| 310 |
|
|
|
|
| 311 |
seg_index = 0 # unique id counter shared across all segments
|
| 312 |
|
| 313 |
for group_idx, group_name in enumerate(GROUPS):
|
| 314 |
group_items = grouped_data[group_name]
|
| 315 |
if not group_items:
|
| 316 |
continue
|
| 317 |
-
|
| 318 |
base_angle = group_idx * 120 # hardcoded 120 degree angle bcs only 3 groups
|
| 319 |
tiers = {}
|
| 320 |
for item in group_items:
|
|
@@ -322,7 +327,7 @@ def _(
|
|
| 322 |
if sites not in tiers:
|
| 323 |
tiers[sites] = []
|
| 324 |
tiers[sites].append(item)
|
| 325 |
-
|
| 326 |
sorted_tier_keys = sorted(tiers.keys(), reverse=True)
|
| 327 |
num_tiers = len(sorted_tier_keys)
|
| 328 |
ring_thickness = (MAX_RADIUS - MIN_RADIUS) / max(1, num_tiers)
|
|
@@ -336,11 +341,14 @@ def _(
|
|
| 336 |
current_start_angle = base_angle
|
| 337 |
|
| 338 |
for item in tier_items:
|
|
|
|
|
|
|
|
|
|
| 339 |
if total_yield > 0:
|
| 340 |
sweep_angle = (item["yield"] / total_yield) * 120
|
| 341 |
else:
|
| 342 |
sweep_angle = 120 / len(tier_items)
|
| 343 |
-
|
| 344 |
end_angle = current_start_angle + sweep_angle
|
| 345 |
opacity = 1.0
|
| 346 |
fill_color = ""
|
|
@@ -348,10 +356,10 @@ def _(
|
|
| 348 |
if active_tab == "Compare Sites": # coloring depends on which tab the user is viewing
|
| 349 |
in_s1 = drop_comp1.value in item["sites"]
|
| 350 |
in_s2 = drop_comp2.value in item["sites"]
|
| 351 |
-
if in_s1 and in_s2:
|
| 352 |
-
elif in_s1:
|
| 353 |
-
elif in_s2:
|
| 354 |
-
else:
|
| 355 |
elif active_tab == "Individual Site":
|
| 356 |
hue = group_hues[group_name]
|
| 357 |
lightness = 85 - min(50, (item["yield"] / max(1, total_yield)) * 45)
|
|
@@ -367,7 +375,7 @@ def _(
|
|
| 367 |
angle_center = current_start_angle + sweep_angle / 2
|
| 368 |
|
| 369 |
core_paths.append(
|
| 370 |
-
f'<path id="seg-{
|
| 371 |
f'fill="{fill_color}" opacity="{opacity}" '
|
| 372 |
f'stroke="white" stroke-width="2" cursor="pointer"/>'
|
| 373 |
)
|
|
@@ -378,15 +386,15 @@ def _(
|
|
| 378 |
ty = tip_cy - TH / 2
|
| 379 |
|
| 380 |
tooltip_elements.append(
|
| 381 |
-
make_tooltip(
|
| 382 |
item["id"], item["yield"], item["num_sites"])
|
| 383 |
)
|
| 384 |
|
| 385 |
# add hovering effect
|
| 386 |
css_hover_rules.append(
|
| 387 |
-
f"svg:has(#seg-{
|
| 388 |
f"{{ filter: brightness(1.35) drop-shadow(0 0 7px rgba(255,255,255,0.6)); }}\n"
|
| 389 |
-
f"svg:has(#seg-{
|
| 390 |
f"{{ visibility: visible; }}"
|
| 391 |
)
|
| 392 |
|
|
@@ -427,10 +435,10 @@ def _(
|
|
| 427 |
(SITE_COLOR_NEITHER, "In neither site"),
|
| 428 |
]
|
| 429 |
|
| 430 |
-
LX, LY = 0, 0
|
| 431 |
LW, LH = 210, 150 # box dimensions
|
| 432 |
-
ROW_H = 26
|
| 433 |
-
SWATCH_S = 14
|
| 434 |
|
| 435 |
annotation_elements.append(
|
| 436 |
f'<g id="compare-legend" style="pointer-events:none;">'
|
|
@@ -481,6 +489,7 @@ def _(
|
|
| 481 |
CX,
|
| 482 |
CY,
|
| 483 |
MIN_RADIUS,
|
|
|
|
| 484 |
annotation_elements,
|
| 485 |
controls,
|
| 486 |
core_paths,
|
|
@@ -489,40 +498,37 @@ def _(
|
|
| 489 |
tooltip_elements,
|
| 490 |
):
|
| 491 |
# Combine everything + visualize
|
|
|
|
|
|
|
| 492 |
css = f"""
|
| 493 |
<style>
|
| 494 |
-
.tip {{ visibility: hidden; pointer-events: none; }}
|
| 495 |
-
path[id^="seg-"] {{ transition: filter 0.15s; }}
|
| 496 |
-
{chr(10).join(css_hover_rules)}
|
| 497 |
</style>
|
| 498 |
"""
|
| 499 |
|
| 500 |
-
# core + annotions are merged here
|
| 501 |
svg_markup = f"""
|
| 502 |
-
{
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
{"".join(annotation_elements)}
|
| 517 |
-
|
| 518 |
-
{"".join(tooltip_elements)}
|
| 519 |
-
</svg>
|
| 520 |
"""
|
| 521 |
|
| 522 |
final_dashboard = mo.vstack([controls, mo.Html(svg_markup)], align="center", gap=8)
|
| 523 |
final_dashboard
|
| 524 |
-
return
|
| 525 |
|
| 526 |
|
| 527 |
if __name__ == "__main__":
|
| 528 |
-
app.run()
|
|
|
|
| 36 |
upload_species,
|
| 37 |
upload_decomp
|
| 38 |
], align="center")
|
| 39 |
+
|
| 40 |
# Place as the last statement to ensure Marimo renders it!
|
| 41 |
upload_ui
|
| 42 |
+
return upload_decomp, upload_species, upload_yield
|
| 43 |
|
| 44 |
|
| 45 |
@app.cell
|
|
|
|
| 123 |
|
| 124 |
|
| 125 |
@app.cell
|
| 126 |
+
def _(drop_comp1, drop_comp2, drop_single, mo):
|
| 127 |
# CSS styling
|
| 128 |
dropdown_styles = mo.Html("""
|
| 129 |
<style>
|
|
|
|
| 174 |
"Individual Site": drop_single,
|
| 175 |
"Compare Sites": mo.hstack([drop_comp1, drop_comp2], gap=4)
|
| 176 |
})
|
| 177 |
+
return dropdown_styles, tabs
|
| 178 |
+
|
| 179 |
|
| 180 |
+
@app.cell
|
| 181 |
+
def _(dropdown_styles, mo, slider_count, tabs):
|
| 182 |
# Build the final control panel
|
| 183 |
controls = mo.vstack([
|
| 184 |
dropdown_styles,
|
| 185 |
tabs,
|
| 186 |
mo.hstack([slider_count], justify="center")
|
| 187 |
], align="center", gap=4)
|
| 188 |
+
return (controls,)
|
|
|
|
| 189 |
|
| 190 |
|
| 191 |
@app.cell
|
| 192 |
+
def _(math, tabs):
|
| 193 |
# Sunburst chart: dimensions + position
|
| 194 |
CX, CY = 500, 380
|
| 195 |
MAX_RADIUS = 370
|
|
|
|
| 221 |
large_arc = "0" if end_angle - start_angle <= 180 else "1"
|
| 222 |
return f"M {p1[0]} {p1[1]} A {r_outer} {r_outer} 0 {large_arc} 1 {p2[0]} {p2[1]} L {p3[0]} {p3[1]} A {r_inner} {r_inner} 0 {large_arc} 0 {p4[0]} {p4[1]} Z"
|
| 223 |
|
| 224 |
+
def make_tooltip(unique_id, tx, ty, group_name, species_name, avg_yield, num_sites):
|
| 225 |
# Bound the tooltip coordinates so it doesn't clip off the 1000x1000 SVG canvas
|
| 226 |
tx = max(10, min(tx, 1000 - TW - 10))
|
| 227 |
ty = max(10, min(ty, 1000 - TH - 10))
|
|
|
|
| 229 |
safe_name = species_name.strip()[:38]
|
| 230 |
yield_str = f"Average yield: {avg_yield:.1f} kg ha\u207b\u00b9"
|
| 231 |
sites_str = f"Occurs in: {num_sites} site(s)"
|
| 232 |
+
tip_id = unique_id.replace("seg", "tip")
|
| 233 |
return f"""
|
| 234 |
+
<g id="{tip_id}" class="tip" transform="translate({tx},{ty})">
|
| 235 |
<rect width="{TW}" height="{TH}" rx="10" ry="10"
|
| 236 |
fill="#1e1e2e" stroke="#45475a" stroke-width="1.5"
|
| 237 |
filter="url(#tipshadow)"/>
|
|
|
|
| 245 |
<text x="12" y="92" font-family="Inter,sans-serif" font-size="12"
|
| 246 |
fill="#f9e2af">{sites_str}</text>
|
| 247 |
</g>"""
|
| 248 |
+
_ = tabs
|
| 249 |
return (
|
| 250 |
CX,
|
| 251 |
CY,
|
|
|
|
| 312 |
tooltip_elements = []
|
| 313 |
css_hover_rules = []
|
| 314 |
|
| 315 |
+
tab_id_prefix = active_tab.replace(" ", "_").lower() # unique tab prefix
|
| 316 |
seg_index = 0 # unique id counter shared across all segments
|
| 317 |
|
| 318 |
for group_idx, group_name in enumerate(GROUPS):
|
| 319 |
group_items = grouped_data[group_name]
|
| 320 |
if not group_items:
|
| 321 |
continue
|
| 322 |
+
|
| 323 |
base_angle = group_idx * 120 # hardcoded 120 degree angle bcs only 3 groups
|
| 324 |
tiers = {}
|
| 325 |
for item in group_items:
|
|
|
|
| 327 |
if sites not in tiers:
|
| 328 |
tiers[sites] = []
|
| 329 |
tiers[sites].append(item)
|
| 330 |
+
|
| 331 |
sorted_tier_keys = sorted(tiers.keys(), reverse=True)
|
| 332 |
num_tiers = len(sorted_tier_keys)
|
| 333 |
ring_thickness = (MAX_RADIUS - MIN_RADIUS) / max(1, num_tiers)
|
|
|
|
| 341 |
current_start_angle = base_angle
|
| 342 |
|
| 343 |
for item in tier_items:
|
| 344 |
+
unique_seg_id = f"{tab_id_prefix}-seg-{seg_index}"
|
| 345 |
+
unique_tip_id = f"{tab_id_prefix}-tip-{seg_index}"
|
| 346 |
+
|
| 347 |
if total_yield > 0:
|
| 348 |
sweep_angle = (item["yield"] / total_yield) * 120
|
| 349 |
else:
|
| 350 |
sweep_angle = 120 / len(tier_items)
|
| 351 |
+
|
| 352 |
end_angle = current_start_angle + sweep_angle
|
| 353 |
opacity = 1.0
|
| 354 |
fill_color = ""
|
|
|
|
| 356 |
if active_tab == "Compare Sites": # coloring depends on which tab the user is viewing
|
| 357 |
in_s1 = drop_comp1.value in item["sites"]
|
| 358 |
in_s2 = drop_comp2.value in item["sites"]
|
| 359 |
+
if in_s1 and in_s2: fill_color = SITE_COLOR_BOTH
|
| 360 |
+
elif in_s1: fill_color = SITE_COLOR_S1_ONLY
|
| 361 |
+
elif in_s2: fill_color = SITE_COLOR_S2_ONLY
|
| 362 |
+
else: fill_color = SITE_COLOR_NEITHER
|
| 363 |
elif active_tab == "Individual Site":
|
| 364 |
hue = group_hues[group_name]
|
| 365 |
lightness = 85 - min(50, (item["yield"] / max(1, total_yield)) * 45)
|
|
|
|
| 375 |
angle_center = current_start_angle + sweep_angle / 2
|
| 376 |
|
| 377 |
core_paths.append(
|
| 378 |
+
f'<path id="seg-{unique_seg_id}" d="{path_d}" '
|
| 379 |
f'fill="{fill_color}" opacity="{opacity}" '
|
| 380 |
f'stroke="white" stroke-width="2" cursor="pointer"/>'
|
| 381 |
)
|
|
|
|
| 386 |
ty = tip_cy - TH / 2
|
| 387 |
|
| 388 |
tooltip_elements.append(
|
| 389 |
+
make_tooltip(unique_seg_id, tx, ty, group_name,
|
| 390 |
item["id"], item["yield"], item["num_sites"])
|
| 391 |
)
|
| 392 |
|
| 393 |
# add hovering effect
|
| 394 |
css_hover_rules.append(
|
| 395 |
+
f"svg:has(#seg-{unique_seg_id}:hover) #seg-{unique_seg_id} "
|
| 396 |
f"{{ filter: brightness(1.35) drop-shadow(0 0 7px rgba(255,255,255,0.6)); }}\n"
|
| 397 |
+
f"svg:has(#seg-{unique_seg_id}:hover) #tip-{unique_tip_id} "
|
| 398 |
f"{{ visibility: visible; }}"
|
| 399 |
)
|
| 400 |
|
|
|
|
| 435 |
(SITE_COLOR_NEITHER, "In neither site"),
|
| 436 |
]
|
| 437 |
|
| 438 |
+
LX, LY = 0, 0 # top-left corner of the legend box
|
| 439 |
LW, LH = 210, 150 # box dimensions
|
| 440 |
+
ROW_H = 26 # vertical spacing between rows
|
| 441 |
+
SWATCH_S = 14 # swatch square size
|
| 442 |
|
| 443 |
annotation_elements.append(
|
| 444 |
f'<g id="compare-legend" style="pointer-events:none;">'
|
|
|
|
| 489 |
CX,
|
| 490 |
CY,
|
| 491 |
MIN_RADIUS,
|
| 492 |
+
active_tab,
|
| 493 |
annotation_elements,
|
| 494 |
controls,
|
| 495 |
core_paths,
|
|
|
|
| 498 |
tooltip_elements,
|
| 499 |
):
|
| 500 |
# Combine everything + visualize
|
| 501 |
+
render_key = active_tab.replace(" ", "-").lower() # unique ID for each tab
|
| 502 |
+
|
| 503 |
css = f"""
|
| 504 |
<style>
|
| 505 |
+
#{render_key}-container .tip {{ visibility: hidden; pointer-events: none; }}
|
| 506 |
+
#{render_key}-container path[id^="seg-"] {{ transition: filter 0.15s; }}
|
| 507 |
+
{chr(10).join([f"#{render_key}-container {rule}" for rule in css_hover_rules])}
|
| 508 |
</style>
|
| 509 |
"""
|
| 510 |
|
|
|
|
| 511 |
svg_markup = f"""
|
| 512 |
+
<div id="{render_key}-container">
|
| 513 |
+
{css}
|
| 514 |
+
<svg width="1000" height="1000" viewBox="0 0 1000 1000" xmlns="http://www.w3.org/2000/svg">
|
| 515 |
+
<defs>
|
| 516 |
+
<filter id="tipshadow" x="-5%" y="-5%" width="120%" height="130%">
|
| 517 |
+
<feDropShadow dx="0" dy="3" stdDeviation="5" flood-color="#000" flood-opacity="0.45"/>
|
| 518 |
+
</filter>
|
| 519 |
+
</defs>
|
| 520 |
+
<circle cx="{CX}" cy="{CY}" r="{MIN_RADIUS}" fill="#f4f4f4" stroke="none"/>
|
| 521 |
+
{"".join(core_paths)}
|
| 522 |
+
{"".join(annotation_elements)}
|
| 523 |
+
{"".join(tooltip_elements)}
|
| 524 |
+
</svg>
|
| 525 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 526 |
"""
|
| 527 |
|
| 528 |
final_dashboard = mo.vstack([controls, mo.Html(svg_markup)], align="center", gap=8)
|
| 529 |
final_dashboard
|
| 530 |
+
return
|
| 531 |
|
| 532 |
|
| 533 |
if __name__ == "__main__":
|
| 534 |
+
app.run()
|