wack0 commited on
Commit
ee082cd
·
verified ·
1 Parent(s): 79fb6ab

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +53 -47
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, upload_ui, upload_yield
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, slider_count):
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(index, tx, ty, group_name, species_name, avg_yield, num_sites):
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="tip-{index}" class="tip" transform="translate({tx},{ty})">
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: fill_color = SITE_COLOR_BOTH
352
- elif in_s1: fill_color = SITE_COLOR_S1_ONLY
353
- elif in_s2: fill_color = SITE_COLOR_S2_ONLY
354
- else: fill_color = SITE_COLOR_NEITHER
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-{seg_index}" d="{path_d}" '
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(seg_index, tx, ty, group_name,
382
  item["id"], item["yield"], item["num_sites"])
383
  )
384
 
385
  # add hovering effect
386
  css_hover_rules.append(
387
- f"svg:has(#seg-{seg_index}:hover) #seg-{seg_index} "
388
  f"{{ filter: brightness(1.35) drop-shadow(0 0 7px rgba(255,255,255,0.6)); }}\n"
389
- f"svg:has(#seg-{seg_index}:hover) #tip-{seg_index} "
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 # top-left corner of the legend box
431
  LW, LH = 210, 150 # box dimensions
432
- ROW_H = 26 # vertical spacing between rows
433
- SWATCH_S = 14 # swatch square size
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
- {css}
503
- <svg width="1000" height="1000" viewBox="0 0 1000 1000"
504
- xmlns="http://www.w3.org/2000/svg">
505
- <defs>
506
- <filter id="tipshadow" x="-5%" y="-5%" width="120%" height="130%">
507
- <feDropShadow dx="0" dy="3" stdDeviation="5"
508
- flood-color="#000" flood-opacity="0.45"/>
509
- </filter>
510
- </defs>
511
-
512
- <circle cx="{CX}" cy="{CY}" r="{MIN_RADIUS}" fill="#f4f4f4" stroke="none"/>
513
-
514
- {"".join(core_paths)}
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 final_dashboard,
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()