reddgr commited on
Commit
6ae14d4
·
1 Parent(s): 8d0d868

spider plot chart

Browse files
app.py CHANGED
@@ -88,6 +88,9 @@ with open(JSON_PATH / "app_column_config.json", "r") as f:
88
  with open(JSON_PATH / "app_column_config.json", "r") as f:
89
  caracteristicas_etf = json.load(f)["cols_tabla_etfs"]
90
 
 
 
 
91
  with open(JSON_PATH / "cat_cols.json", "r") as f:
92
  cat_cols = json.load(f)["cat_cols"]
93
 
@@ -367,28 +370,52 @@ with gr.Blocks(title="Swift Stock Screener, by Reddgr") as front:
367
  )
368
 
369
  # ---- TAB 2: COMPANY --------------------------------------------------
 
370
  with gr.TabItem("Company details")as company_tab: ####
371
  company_title = gr.Markdown(f"## {init_name}" if init_name else "### Company Name")
372
  company_summary = gr.Markdown(init_summary)
373
  company_details = gr.Dataframe(value=init_details, interactive=False)
 
 
 
 
 
 
 
 
 
 
 
374
 
375
  def on_company_tab(evt: gr.SelectData):
376
  global selected_ticker
377
  if evt.selected and selected_ticker:
378
- maestro_details = maestro.copy()
379
- maestro_details.drop(columns=["embeddings"], inplace=True, errors="ignore")
380
  name, summary, details_df = utils.get_company_info(maestro_details, selected_ticker, rename_columns)
 
 
 
 
 
 
 
 
 
 
 
381
  return (
382
  gr.update(value=f"## {name}"),
383
  gr.update(value=summary),
384
- gr.update(value=details_df)
 
385
  )
386
- return gr.update(), gr.update(), gr.update()
387
 
388
  company_tab.select(
389
  on_company_tab,
390
  inputs=[],
391
- outputs=[company_title, company_summary, company_details]
392
  )
393
 
394
 
@@ -415,6 +442,17 @@ with gr.Blocks(title="Swift Stock Screener, by Reddgr") as front:
415
  name, summary, details_df = utils.get_company_info(
416
  maestro, ticker, rename_columns
417
  )
 
 
 
 
 
 
 
 
 
 
 
418
  print(f"DEBUG ➡ selected ticker={ticker}, name={name}")
419
  return (
420
  last_result_df,
@@ -424,7 +462,8 @@ with gr.Blocks(title="Swift Stock Screener, by Reddgr") as front:
424
  gr.update(selected=1), # ← change here
425
  gr.update(value=f"## {name}"),
426
  gr.update(value=summary),
427
- gr.update(value=details_df)
 
428
  )
429
 
430
 
@@ -433,7 +472,7 @@ with gr.Blocks(title="Swift Stock Screener, by Reddgr") as front:
433
  inputs=[],
434
  outputs=[
435
  output_df, pagination_label, page_state, summary_display,
436
- main_tabs, company_title, company_summary, company_details
437
  ]
438
  )
439
 
@@ -450,18 +489,29 @@ with gr.Blocks(title="Swift Stock Screener, by Reddgr") as front:
450
  if new_ticker != selected_ticker:
451
  selected_ticker = new_ticker
452
  name, summary, details_df = utils.get_company_info(maestro, selected_ticker, rename_columns)
 
 
 
 
 
 
 
 
 
453
  return (
454
  gr.update(value=f"## {name}"),
455
  gr.update(value=summary),
456
- gr.update(value=details_df)
 
457
  )
 
458
  # otherwise leave components as‑is
459
- return gr.update(), gr.update(), gr.update()
460
 
461
  output_df.change(
462
  on_df_first_row_change,
463
  inputs=[output_df],
464
- outputs=[company_title, company_summary, company_details]
465
  )
466
 
467
  # ---------------------- EXCLUSION FILTER TOGGLES --------------------------------
@@ -565,12 +615,22 @@ with gr.Blocks(title="Swift Stock Screener, by Reddgr") as front:
565
  def on_tab_change(tab_index):
566
  if tab_index == 1 and selected_ticker:
567
  name, summary, details_df = utils.get_company_info(maestro, selected_ticker, rename_columns)
 
 
 
 
 
 
 
 
 
568
  return (
569
  gr.update(value=f"## {name}"),
570
  gr.update(value=summary),
571
- gr.update(value=details_df)
 
572
  )
573
- return gr.update(), gr.update(), gr.update()
574
 
575
 
576
  # ---------------------- FILTERS BY COLUMN ------------------ #
 
88
  with open(JSON_PATH / "app_column_config.json", "r") as f:
89
  caracteristicas_etf = json.load(f)["cols_tabla_etfs"]
90
 
91
+ with open(JSON_PATH / "app_column_config.json", "r") as f:
92
+ company_details_cols = json.load(f)["company_details_cols"]
93
+
94
  with open(JSON_PATH / "cat_cols.json", "r") as f:
95
  cat_cols = json.load(f)["cat_cols"]
96
 
 
370
  )
371
 
372
  # ---- TAB 2: COMPANY --------------------------------------------------
373
+ '''
374
  with gr.TabItem("Company details")as company_tab: ####
375
  company_title = gr.Markdown(f"## {init_name}" if init_name else "### Company Name")
376
  company_summary = gr.Markdown(init_summary)
377
  company_details = gr.Dataframe(value=init_details, interactive=False)
378
+ '''
379
+
380
+ with gr.TabItem("Company details") as company_tab:
381
+ with gr.Row():
382
+ with gr.Column(scale=1):
383
+ company_title = gr.Markdown(f"## {init_name}" if init_name else "### Company Name")
384
+ company_summary = gr.Markdown(init_summary)
385
+ company_details = gr.Dataframe(value=init_details, interactive=False)
386
+ with gr.Column(scale=1):
387
+ company_chart_title = gr.Markdown("## Key Metrics Radar Chart")
388
+ company_plot = gr.Plot(visible=True)
389
 
390
  def on_company_tab(evt: gr.SelectData):
391
  global selected_ticker
392
  if evt.selected and selected_ticker:
393
+ maestro_details = maestro[company_details_cols].copy()
394
+ # maestro_details.drop(columns=["embeddings"], inplace=True, errors="ignore")
395
  name, summary, details_df = utils.get_company_info(maestro_details, selected_ticker, rename_columns)
396
+
397
+ # Create spider plot figure
398
+ fig = None
399
+ try:
400
+ if not details_df.empty:
401
+ fig = utils.get_spider_plot_fig(details_df)
402
+ except Exception as e:
403
+ print(f"Error creating spider plot: {e}")
404
+
405
+
406
+
407
  return (
408
  gr.update(value=f"## {name}"),
409
  gr.update(value=summary),
410
+ gr.update(value=details_df),
411
+ gr.update(value=fig)
412
  )
413
+ return gr.update(), gr.update(), gr.update(), gr.update()
414
 
415
  company_tab.select(
416
  on_company_tab,
417
  inputs=[],
418
+ outputs=[company_title, company_summary, company_details, company_plot]
419
  )
420
 
421
 
 
442
  name, summary, details_df = utils.get_company_info(
443
  maestro, ticker, rename_columns
444
  )
445
+
446
+ # Create spider plot figure
447
+ fig = None
448
+ try:
449
+ if not details_df.empty:
450
+ fig = utils.get_spider_plot_fig(details_df)
451
+ except Exception as e:
452
+ print(f"Error creating spider plot: {e}")
453
+
454
+
455
+ # details_df.to_pickle(ROOT / "pkl" / "details_df_test.pkl")
456
  print(f"DEBUG ➡ selected ticker={ticker}, name={name}")
457
  return (
458
  last_result_df,
 
462
  gr.update(selected=1), # ← change here
463
  gr.update(value=f"## {name}"),
464
  gr.update(value=summary),
465
+ gr.update(value=details_df),
466
+ gr.update(value=fig)
467
  )
468
 
469
 
 
472
  inputs=[],
473
  outputs=[
474
  output_df, pagination_label, page_state, summary_display,
475
+ main_tabs, company_title, company_summary, company_details, company_plot
476
  ]
477
  )
478
 
 
489
  if new_ticker != selected_ticker:
490
  selected_ticker = new_ticker
491
  name, summary, details_df = utils.get_company_info(maestro, selected_ticker, rename_columns)
492
+
493
+ # Create spider plot figure
494
+ fig = None
495
+ try:
496
+ if not details_df.empty:
497
+ fig = utils.get_spider_plot_fig(details_df)
498
+ except Exception as e:
499
+ print(f"Error creating spider plot: {e}")
500
+
501
  return (
502
  gr.update(value=f"## {name}"),
503
  gr.update(value=summary),
504
+ gr.update(value=details_df),
505
+ gr.update(value=fig)
506
  )
507
+
508
  # otherwise leave components as‑is
509
+ return gr.update(), gr.update(), gr.update(), gr.update()
510
 
511
  output_df.change(
512
  on_df_first_row_change,
513
  inputs=[output_df],
514
+ outputs=[company_title, company_summary, company_details, company_plot]
515
  )
516
 
517
  # ---------------------- EXCLUSION FILTER TOGGLES --------------------------------
 
615
  def on_tab_change(tab_index):
616
  if tab_index == 1 and selected_ticker:
617
  name, summary, details_df = utils.get_company_info(maestro, selected_ticker, rename_columns)
618
+
619
+ # Create spider plot figure
620
+ fig = None
621
+ try:
622
+ if not details_df.empty:
623
+ fig = utils.get_spider_plot_fig(details_df)
624
+ except Exception as e:
625
+ print(f"Error creating spider plot: {e}")
626
+
627
  return (
628
  gr.update(value=f"## {name}"),
629
  gr.update(value=summary),
630
+ gr.update(value=details_df),
631
+ gr.update(value=fig)
632
  )
633
+ return gr.update(), gr.update(), gr.update(), gr.update()
634
 
635
 
636
  # ---------------------- FILTERS BY COLUMN ------------------ #
html/front_layout.html CHANGED
@@ -3,7 +3,7 @@
3
  Swift Stock Screener
4
  </h1>
5
  <p style="margin-left:10px">
6
- Browse and search over 12,000 stocks. Search assets by theme, filter, sort, analyze, and get ideas to build portfolios and indices. Search by <b>ticker symbol</b> to display a ranked list of similar companies based on fundamentals and performance. Enter any keyword in <b>thematic search</b> to search companies by theme. Click on <u>country names</u> or <u>GICS sectors</u> for strict filtering. <b>Reset</b> the search and <b>sort</b> all assets by any of the displayed metrics.
7
 
8
  <style>
9
  /* Botón de tamaño contenido */
@@ -21,95 +21,95 @@
21
  }
22
 
23
  /* cap the Gradio table + keep pagination row below */
24
- .clickable-columns .dataframe-container {
25
  max-height: calc(100vh - 300px); /* adjust px to match header+controls height */
26
  overflow-y: auto;
27
  }
28
 
29
  /* Columnas filtrables (click en la celda) */
30
- .clickable-columns tbody td:nth-child(3),
31
- .clickable-columns tbody td:nth-child(4) {
32
  color: #1a0dab; /* link blue for light theme */
33
  text-decoration: underline; /* underline */
34
  cursor: pointer; /* pointer cursor */
35
  }
36
 
37
  @media (prefers-color-scheme: dark) {
38
- .clickable-columns tbody td:nth-child(3),
39
- .clickable-columns tbody td:nth-child(4) {
40
  color: #8ab4f8; /* lighter blue for dark theme */
41
  }
42
  }
43
 
44
- .clickable-columns span.negative-value {
45
  color: red;
46
  }
47
 
48
  /* make the table use fixed layout so width rules apply */
49
- .clickable-columns table {
50
  table-layout: fixed;
51
  }
52
 
53
  /* CONFIGURACIÓN DE ANCHO DE COLUMNAS */
54
  /* Ticker */
55
- .clickable-columns table th:nth-child(1),
56
- .clickable-columns table td:nth-child(1) {
57
  min-width: 40px; max-width: 100px;
58
  overflow: hidden;
59
  }
60
- .clickable-columns table th:nth-child(2),
61
- .clickable-columns table td:nth-child(2) {
62
  min-width: 75px; max-width: 220px;
63
  overflow: hidden;
64
  }
65
- .clickable-columns table th:nth-child(3),
66
- .clickable-columns table td:nth-child(3) {
67
  min-width: 70px; max-width: 160px;
68
  overflow: hidden;
69
  }
70
- .clickable-columns table th:nth-child(4),
71
- .clickable-columns table td:nth-child(4) {
72
  min-width: 70px; max-width: 200px;
73
  overflow: hidden;
74
  }
75
- .clickable-columns table th:nth-child(5),
76
- .clickable-columns table td:nth-child(5) {
77
  min-width: 60px; max-width: 80px;
78
  overflow: hidden;
79
  }
80
  /* 1yr return */
81
- .clickable-columns table th:nth-child(6),
82
- .clickable-columns table td:nth-child(6) {
83
  min-width: 60px; max-width: 80px;
84
  overflow: hidden;
85
  }
86
- .clickable-columns table th:nth-child(7),
87
- .clickable-columns table td:nth-child(7) {
88
  min-width: 70px; max-width: 100px;
89
  overflow: hidden;
90
  }
91
- .clickable-columns table th:nth-child(8),
92
- .clickable-columns table td:nth-child(8) {
93
  min-width: 70px; max-width: 100px;
94
  overflow: hidden;
95
  }
96
- .clickable-columns table th:nth-child(9),
97
- .clickable-columns table td:nth-child(9) {
98
  min-width: 70px; max-width: 100px;
99
  overflow: hidden;
100
  }
101
- .clickable-columns table th:nth-child(10),
102
- .clickable-columns table td:nth-child(10) {
103
  min-width: 70px; max-width: 100px;
104
  overflow: hidden;
105
  }
106
- .clickable-columns table th:nth-child(11),
107
- .clickable-columns table td:nth-child(11) {
108
  min-width: 60px; max-width: 70px;
109
  overflow: hidden;
110
  }
111
- .clickable-columns table th:nth-child(12),
112
- .clickable-columns table td:nth-child(12) {
113
  min-width: 50px; max-width: 70px;
114
  overflow: hidden;
115
  }
 
3
  Swift Stock Screener
4
  </h1>
5
  <p style="margin-left:10px">
6
+ Browse and search over 12,000 stocks. Search assets by theme, filter, sort, analyze, and get ideas to build portfolios and indices. Search by <b>ticker symbol</b> to display a list of ranked related companies. Enter any keyword in <b>thematic search</b> to search by theme. Click on <u>country names</u> or <u>GICS sectors</u> for strict filtering. <b>Reset</b> the search and <b>sort</b> all assets by any of the displayed metrics.
7
 
8
  <style>
9
  /* Botón de tamaño contenido */
 
21
  }
22
 
23
  /* cap the Gradio table + keep pagination row below */
24
+ .df-cells .dataframe-container {
25
  max-height: calc(100vh - 300px); /* adjust px to match header+controls height */
26
  overflow-y: auto;
27
  }
28
 
29
  /* Columnas filtrables (click en la celda) */
30
+ .df-cells tbody td:nth-child(3),
31
+ .df-cells tbody td:nth-child(4) {
32
  color: #1a0dab; /* link blue for light theme */
33
  text-decoration: underline; /* underline */
34
  cursor: pointer; /* pointer cursor */
35
  }
36
 
37
  @media (prefers-color-scheme: dark) {
38
+ .df-cells tbody td:nth-child(3),
39
+ .df-cells tbody td:nth-child(4) {
40
  color: #8ab4f8; /* lighter blue for dark theme */
41
  }
42
  }
43
 
44
+ .df-cells span.negative-value {
45
  color: red;
46
  }
47
 
48
  /* make the table use fixed layout so width rules apply */
49
+ .df-cells table {
50
  table-layout: fixed;
51
  }
52
 
53
  /* CONFIGURACIÓN DE ANCHO DE COLUMNAS */
54
  /* Ticker */
55
+ .df-cells table th:nth-child(1),
56
+ .df-cells table td:nth-child(1) {
57
  min-width: 40px; max-width: 100px;
58
  overflow: hidden;
59
  }
60
+ .df-cells table th:nth-child(2),
61
+ .df-cells table td:nth-child(2) {
62
  min-width: 75px; max-width: 220px;
63
  overflow: hidden;
64
  }
65
+ .df-cells table th:nth-child(3),
66
+ .df-cells table td:nth-child(3) {
67
  min-width: 70px; max-width: 160px;
68
  overflow: hidden;
69
  }
70
+ .df-cells table th:nth-child(4),
71
+ .df-cells table td:nth-child(4) {
72
  min-width: 70px; max-width: 200px;
73
  overflow: hidden;
74
  }
75
+ .df-cells table th:nth-child(5),
76
+ .df-cells table td:nth-child(5) {
77
  min-width: 60px; max-width: 80px;
78
  overflow: hidden;
79
  }
80
  /* 1yr return */
81
+ .df-cells table th:nth-child(6),
82
+ .df-cells table td:nth-child(6) {
83
  min-width: 60px; max-width: 80px;
84
  overflow: hidden;
85
  }
86
+ .df-cells table th:nth-child(7),
87
+ .df-cells table td:nth-child(7) {
88
  min-width: 70px; max-width: 100px;
89
  overflow: hidden;
90
  }
91
+ .df-cells table th:nth-child(8),
92
+ .df-cells table td:nth-child(8) {
93
  min-width: 70px; max-width: 100px;
94
  overflow: hidden;
95
  }
96
+ .df-cells table th:nth-child(9),
97
+ .df-cells table td:nth-child(9) {
98
  min-width: 70px; max-width: 100px;
99
  overflow: hidden;
100
  }
101
+ .df-cells table th:nth-child(10),
102
+ .df-cells table td:nth-child(10) {
103
  min-width: 70px; max-width: 100px;
104
  overflow: hidden;
105
  }
106
+ .df-cells table th:nth-child(11),
107
+ .df-cells table td:nth-child(11) {
108
  min-width: 60px; max-width: 70px;
109
  overflow: hidden;
110
  }
111
+ .df-cells table th:nth-child(12),
112
+ .df-cells table td:nth-child(12) {
113
  min-width: 50px; max-width: 70px;
114
  overflow: hidden;
115
  }
json/app_column_config.json CHANGED
@@ -67,5 +67,24 @@
67
  "netExpenseRatio",
68
  "fundInceptionDate",
69
  "fundFamily"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  ]
71
  }
 
67
  "netExpenseRatio",
68
  "fundInceptionDate",
69
  "fundFamily"
70
+ ],
71
+ "company_details_cols": [
72
+ "ticker",
73
+ "security",
74
+ "country",
75
+ "sector",
76
+ "marketCap",
77
+ "ret_365",
78
+ "vol_365",
79
+ "trailingPE",
80
+ "revenueGrowth",
81
+ "dividendYield",
82
+ "beta",
83
+ "beta_norm",
84
+ "debtToEquity_norm",
85
+ "ret_365_norm",
86
+ "vol_365_norm",
87
+ "revenueGrowth_norm",
88
+ "trailingPE_norm"
89
  ]
90
  }
json/col_names_map.json CHANGED
@@ -109,6 +109,12 @@
109
  "vol_365": "Volatility",
110
  "yield": "Yield",
111
  "ytdReturn": "YTD Return",
112
- "zip": "Zip"
 
 
 
 
 
 
113
  }
114
  }
 
109
  "vol_365": "Volatility",
110
  "yield": "Yield",
111
  "ytdReturn": "YTD Return",
112
+ "zip": "Zip",
113
+ "beta_norm": "Beta norm.",
114
+ "debtToEquity_norm": "Debt to Equity norm.",
115
+ "ret_365_norm": "1-year Return norm.",
116
+ "vol_365_norm": "Volatility norm.",
117
+ "revenueGrowth_norm": "Revenue Growth norm.",
118
+ "trailingPE_norm": "Trailing PE norm."
119
  }
120
  }
src/app_utils.py CHANGED
@@ -1,6 +1,7 @@
1
  import pandas as pd
2
  from typing import Tuple
3
-
 
4
  import re
5
 
6
  _NEG_COLOR = "red"
@@ -95,7 +96,7 @@ def get_company_info(
95
 
96
  # Round _norm fields to 3 decimal places
97
  for i, field in enumerate(df["Field"]):
98
- if field.endswith("_norm"):
99
  value = df.iloc[i]["Value"]
100
  if isinstance(value, (int, float)) and not pd.isna(value):
101
  df.iloc[i, df.columns.get_loc("Value")] = round(value, 3)
@@ -106,7 +107,7 @@ def get_company_info(
106
  numeric_indices = []
107
 
108
  for i, (display_field, value) in enumerate(zip(df["Field"], df["Value"])):
109
- if not display_field.endswith("_norm") and isinstance(value, (int, float)) and not pd.isna(value):
110
  # Get original field name using inverse rename dictionary
111
  orig_field = next((k for k, v in rename_columns.items() if v == display_field), display_field)
112
  numeric_fields.append(orig_field)
@@ -127,3 +128,127 @@ def get_company_info(
127
 
128
 
129
  return name, summary, df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import pandas as pd
2
  from typing import Tuple
3
+ import numpy as np
4
+ import plotly.graph_objects as go
5
  import re
6
 
7
  _NEG_COLOR = "red"
 
96
 
97
  # Round _norm fields to 3 decimal places
98
  for i, field in enumerate(df["Field"]):
99
+ if field.endswith("norm."):
100
  value = df.iloc[i]["Value"]
101
  if isinstance(value, (int, float)) and not pd.isna(value):
102
  df.iloc[i, df.columns.get_loc("Value")] = round(value, 3)
 
107
  numeric_indices = []
108
 
109
  for i, (display_field, value) in enumerate(zip(df["Field"], df["Value"])):
110
+ if not display_field.endswith("norm.") and isinstance(value, (int, float)) and not pd.isna(value):
111
  # Get original field name using inverse rename dictionary
112
  orig_field = next((k for k, v in rename_columns.items() if v == display_field), display_field)
113
  numeric_fields.append(orig_field)
 
128
 
129
 
130
  return name, summary, df
131
+
132
+
133
+ def spider_plot(df: pd.DataFrame) -> None:
134
+ spider_plot_cols = ['Beta norm.', 'Debt to Equity norm.', '1-year Return norm.', 'Revenue Growth norm.', 'Volatility norm.']
135
+ plot_data = df[df['Field'].isin(spider_plot_cols)].set_index('Field')
136
+ values = plot_data.loc[spider_plot_cols, 'Value'].fillna(0.5).astype(float).tolist()
137
+ metrics_to_invert = ['Debt to Equity norm.', 'Beta norm.', 'Volatility norm.']
138
+ values = [1 - v if col in metrics_to_invert else v for v, col in zip(values, spider_plot_cols)]
139
+ categories = [s.replace(' norm.', '').replace('1-year', '1yr').replace('Debt to Equity', 'D/E') for s in spider_plot_cols]
140
+ fig = go.Figure()
141
+
142
+ fig.add_trace(go.Scatterpolar(
143
+ r=values,
144
+ theta=categories,
145
+ fill='toself',
146
+ name='Company Profile'
147
+ ))
148
+
149
+ fig.add_trace(go.Scatterpolar(
150
+ r=[0.5] * len(categories) + [0.5], # Append the first r value to close the loop
151
+ theta=categories + [categories[0]], # Append the first theta value to close the loop
152
+ mode='lines',
153
+ line=dict(dash='dot', color='grey'),
154
+ fill='toself', # Keep fill='none' if you only want the line
155
+ fillcolor='rgba(0,0,0,0)', # Make fill transparent if only line is desired
156
+ name='Median (0.5)'
157
+ ))
158
+
159
+ legend_text = (
160
+ "<b>Quantile Scale: 0 to 1</b><br>"
161
+ "D/E, Beta, and Volatility:<br>"
162
+ "0 is highest, 1 is lowest<br>"
163
+ "Rev. growth and 1yr return:<br>"
164
+ "0 is lowest, 1 is highest<br>"
165
+ )
166
+
167
+ fig.update_layout(
168
+ polar=dict(
169
+ radialaxis=dict(
170
+ visible=True,
171
+ range=[0, 1] # Set the range from 0 to 1
172
+ )),
173
+ showlegend=True,
174
+ title='Normalized Company Metrics',
175
+ annotations=[
176
+ go.layout.Annotation(
177
+ text=legend_text,
178
+ align='right',
179
+ showarrow=False,
180
+ xref='paper',
181
+ yref='paper',
182
+ x=1.41,
183
+ y=-0.1
184
+ )
185
+ ],
186
+ margin=dict(b=120),
187
+ width=600,
188
+ height=500
189
+ )
190
+
191
+ fig.show()
192
+
193
+
194
+ # Create a new function in app_utils.py that returns the figure instead of showing it
195
+ def get_spider_plot_fig(df: pd.DataFrame):
196
+ spider_plot_cols = ['Beta norm.', 'Debt to Equity norm.', '1-year Return norm.', 'Revenue Growth norm.', 'Volatility norm.']
197
+ plot_data = df[df['Field'].isin(spider_plot_cols)].set_index('Field')
198
+ values = plot_data.loc[spider_plot_cols, 'Value'].fillna(0.5).astype(float).tolist()
199
+ metrics_to_invert = ['Debt to Equity norm.', 'Beta norm.', 'Volatility norm.']
200
+ values = [1 - v if col in metrics_to_invert else v for v, col in zip(values, spider_plot_cols)]
201
+ categories = [s.replace(' norm.', '').replace('1-year', '1yr').replace('Debt to Equity', 'D/E') for s in spider_plot_cols]
202
+ company_name = df.loc[df['Field'] == 'Name', 'Value'].values[0]
203
+ fig = go.Figure()
204
+
205
+ fig.add_trace(go.Scatterpolar(
206
+ r=values,
207
+ theta=categories,
208
+ fill='toself',
209
+ name='Company Profile'
210
+ ))
211
+
212
+ fig.add_trace(go.Scatterpolar(
213
+ r=[0.5] * len(categories) + [0.5], # Append the first r value to close the loop
214
+ theta=categories + [categories[0]], # Append the first theta value to close the loop
215
+ mode='lines',
216
+ line=dict(dash='dot', color='grey'),
217
+ fill='toself', # Keep fill='none' if you only want the line
218
+ fillcolor='rgba(0,0,0,0)', # Make fill transparent if only line is desired
219
+ name='Median (0.5)'
220
+ ))
221
+
222
+ legend_text = (
223
+ "<b>Quantile Scale: 0 to 1</b><br>"
224
+ "D/E, Beta, and Volatility:<br>"
225
+ "0 is highest, 1 is lowest<br>"
226
+ "Rev. growth and 1yr return:<br>"
227
+ "0 is lowest, 1 is highest<br>"
228
+ )
229
+
230
+ fig.update_layout(
231
+ polar=dict(
232
+ radialaxis=dict(
233
+ visible=True,
234
+ range=[0, 1] # Set the range from 0 to 1
235
+ )),
236
+ showlegend=True,
237
+ title=f'{company_name} - Normalized Metrics',
238
+ annotations=[
239
+ go.layout.Annotation(
240
+ text=legend_text,
241
+ align='right',
242
+ showarrow=False,
243
+ xref='paper',
244
+ yref='paper',
245
+ x=1.41,
246
+ y=-0.1
247
+ )
248
+ ],
249
+ margin=dict(b=120),
250
+ width=600,
251
+ height=500
252
+ )
253
+
254
+ return fig