spider plot chart
Browse files- app.py +72 -12
- html/front_layout.html +32 -32
- json/app_column_config.json +19 -0
- json/col_names_map.json +7 -1
- src/app_utils.py +128 -3
    	
        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  | 
| 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 | 
            -
              . | 
| 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 | 
            -
              . | 
| 31 | 
            -
              . | 
| 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 | 
            -
                . | 
| 39 | 
            -
                . | 
| 40 | 
             
                  color: #8ab4f8;              /* lighter blue for dark theme */
         | 
| 41 | 
             
                }
         | 
| 42 | 
             
              }
         | 
| 43 |  | 
| 44 | 
            -
              . | 
| 45 | 
             
                color: red;
         | 
| 46 | 
             
              }
         | 
| 47 |  | 
| 48 | 
             
                /* make the table use fixed layout so width rules apply */
         | 
| 49 | 
            -
                . | 
| 50 | 
             
                table-layout: fixed;
         | 
| 51 | 
             
              }
         | 
| 52 |  | 
| 53 | 
             
              /* CONFIGURACIÓN DE ANCHO DE COLUMNAS */
         | 
| 54 | 
             
              /* Ticker */
         | 
| 55 | 
            -
              . | 
| 56 | 
            -
              . | 
| 57 | 
             
                min-width: 40px; max-width: 100px;
         | 
| 58 | 
             
                overflow: hidden;
         | 
| 59 | 
             
              }
         | 
| 60 | 
            -
              . | 
| 61 | 
            -
              . | 
| 62 | 
             
                min-width: 75px; max-width: 220px;
         | 
| 63 | 
             
                overflow: hidden;
         | 
| 64 | 
             
              }
         | 
| 65 | 
            -
              . | 
| 66 | 
            -
              . | 
| 67 | 
             
                min-width: 70px; max-width: 160px;
         | 
| 68 | 
             
                overflow: hidden;
         | 
| 69 | 
             
              }
         | 
| 70 | 
            -
              . | 
| 71 | 
            -
              . | 
| 72 | 
             
                min-width: 70px; max-width: 200px;
         | 
| 73 | 
             
                overflow: hidden;
         | 
| 74 | 
             
              }
         | 
| 75 | 
            -
              . | 
| 76 | 
            -
              . | 
| 77 | 
             
                min-width: 60px; max-width: 80px;
         | 
| 78 | 
             
                overflow: hidden;
         | 
| 79 | 
             
              }
         | 
| 80 | 
             
              /* 1yr return */
         | 
| 81 | 
            -
              . | 
| 82 | 
            -
              . | 
| 83 | 
             
                min-width: 60px; max-width: 80px;
         | 
| 84 | 
             
                overflow: hidden;
         | 
| 85 | 
             
              }
         | 
| 86 | 
            -
              . | 
| 87 | 
            -
              . | 
| 88 | 
             
                min-width: 70px; max-width: 100px;
         | 
| 89 | 
             
                overflow: hidden;
         | 
| 90 | 
             
              }
         | 
| 91 | 
            -
              . | 
| 92 | 
            -
              . | 
| 93 | 
             
                min-width: 70px; max-width: 100px;
         | 
| 94 | 
             
                overflow: hidden;
         | 
| 95 | 
             
              }
         | 
| 96 | 
            -
              . | 
| 97 | 
            -
              . | 
| 98 | 
             
                min-width: 70px; max-width: 100px;
         | 
| 99 | 
             
                overflow: hidden;
         | 
| 100 | 
             
              }
         | 
| 101 | 
            -
              . | 
| 102 | 
            -
              . | 
| 103 | 
             
                min-width: 70px; max-width: 100px;
         | 
| 104 | 
             
                overflow: hidden;
         | 
| 105 | 
             
              }
         | 
| 106 | 
            -
              . | 
| 107 | 
            -
              . | 
| 108 | 
             
                min-width: 60px; max-width: 70px;
         | 
| 109 | 
             
                overflow: hidden;
         | 
| 110 | 
             
              }
         | 
| 111 | 
            -
              . | 
| 112 | 
            -
              . | 
| 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(" | 
| 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(" | 
| 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  
         | 
