@@ -767,6 +767,58 @@ def main():
767767 st .markdown ("🔄 **Ready for Analysis**" )
768768 st .markdown ("Performance monitoring will appear after first calculation." )
769769
770+ # Configuration Management
771+ st .markdown ("---" )
772+ st .markdown ("### 💾 Configuration Management" )
773+
774+ config_col1 , config_col2 = st .columns (2 )
775+
776+ with config_col1 :
777+ if st .button ("💾 Save Config" , help = "Save current funnel configuration" ):
778+ if st .session_state .funnel_steps :
779+ config_name = f"Funnel_{ len (st .session_state .saved_configurations ) + 1 } "
780+ config_json = FunnelConfigManager .save_config (
781+ st .session_state .funnel_config ,
782+ st .session_state .funnel_steps ,
783+ config_name ,
784+ )
785+ st .session_state .saved_configurations .append ((config_name , config_json ))
786+ st .toast (f"💾 Saved as { config_name } !" , icon = "💾" )
787+
788+ with config_col2 :
789+ uploaded_config = st .file_uploader (
790+ "📁 Load Config" ,
791+ type = ["json" ],
792+ help = "Upload saved configuration" ,
793+ key = "sidebar_config_upload"
794+ )
795+
796+ if uploaded_config is not None :
797+ try :
798+ config_json = uploaded_config .read ().decode ()
799+ config , steps , name = FunnelConfigManager .load_config (config_json )
800+ st .session_state .funnel_config = config
801+ st .session_state .funnel_steps = steps
802+ st .toast (f"📁 Loaded { name } !" , icon = "📁" )
803+ st .rerun ()
804+ except Exception as e :
805+ st .error (f"Error: { str (e )} " )
806+
807+ # Download saved configurations
808+ if st .session_state .saved_configurations :
809+ st .markdown ("**Saved Configs:**" )
810+ for i , (name , config_json ) in enumerate (st .session_state .saved_configurations ):
811+ config_col1 , config_col2 = st .columns ([2 , 1 ])
812+ config_col1 .caption (name )
813+ config_col2 .download_button (
814+ "⬇️" ,
815+ config_json ,
816+ file_name = f"{ name } .json" ,
817+ mime = "application/json" ,
818+ key = f"sidebar_download_{ i } " ,
819+ help = "Download" ,
820+ )
821+
770822 # Cache Management
771823 st .markdown ("---" )
772824 st .markdown ("### 💾 Cache Management" )
@@ -1017,57 +1069,7 @@ def main():
10171069 else :
10181070 st .toast ("⚠️ Please add at least 2 steps to create a funnel" , icon = "⚠️" )
10191071
1020- st .markdown ("---" )
1021-
1022- # Configuration Management - перенесено в основную область
1023- st .markdown ("### 💾 Save & Load Configurations" )
1024-
1025- col1 , col2 = st .columns (2 )
1026-
1027- with col1 :
1028- if st .button ("💾 Save Current Config" , use_container_width = True ):
1029- if st .session_state .funnel_steps :
1030- config_name = f"Funnel_{ len (st .session_state .saved_configurations ) + 1 } "
1031- config_json = FunnelConfigManager .save_config (
1032- st .session_state .funnel_config ,
1033- st .session_state .funnel_steps ,
1034- config_name ,
1035- )
1036- st .session_state .saved_configurations .append ((config_name , config_json ))
1037- st .success (f"Configuration saved as { config_name } " )
1038-
1039- with col2 :
1040- uploaded_config = st .file_uploader (
1041- "📁 Load Configuration File" ,
1042- type = ["json" ],
1043- help = "Upload a previously saved funnel configuration" ,
1044- )
1045-
1046- if uploaded_config is not None :
1047- try :
1048- config_json = uploaded_config .read ().decode ()
1049- config , steps , name = FunnelConfigManager .load_config (config_json )
1050- st .session_state .funnel_config = config
1051- st .session_state .funnel_steps = steps
1052- st .success (f"Loaded configuration: { name } " )
1053- st .rerun ()
1054- except Exception as e :
1055- st .error (f"Error loading configuration: { str (e )} " )
10561072
1057- # Download saved configurations
1058- if st .session_state .saved_configurations :
1059- st .markdown ("**Saved Configurations:**" )
1060- for i , (name , config_json ) in enumerate (st .session_state .saved_configurations ):
1061- col1 , col2 = st .columns ([3 , 1 ])
1062- col1 .write (name )
1063- col2 .download_button (
1064- "⬇️" ,
1065- config_json ,
1066- file_name = f"{ name } .json" ,
1067- mime = "application/json" ,
1068- key = f"download_{ i } " ,
1069- help = "Download configuration" ,
1070- )
10711073
10721074 # STEP 4: Results - показываем только если есть результаты
10731075 if st .session_state .analysis_results :
@@ -2539,20 +2541,7 @@ def format_time(minutes):
25392541
25402542 tab_idx += 1
25412543
2542- # Test visualizations button
2543- if "analysis_results" in st .session_state and st .session_state .analysis_results :
2544- st .markdown ("---" )
2545- test_col1 , test_col2 , test_col3 = st .columns ([1 , 1 , 1 ])
2546- with test_col2 :
2547- if st .button ("🧪 Test Visualizations" , use_container_width = True ):
2548- with st .spinner ("Testing all visualizations..." ):
2549- test_results = test_visualizations ()
2550-
2551- if test_results ["success" ]:
2552- st .success ("✅ All visualizations passed!" )
2553- else :
2554- failed_tests = [name for name , _ in test_results ["failed" ]]
2555- st .error (f"❌ Failed tests: { ', ' .join (failed_tests )} " )
2544+
25562545
25572546 # Footer
25582547 st .markdown ("---" )
@@ -2567,250 +2556,8 @@ def format_time(minutes):
25672556 )
25682557
25692558
2570- def test_visualizations ():
2571- """
2572- Universal test function to verify all visualizations render correctly.
2573- Can be run with:
2574- 1. python app.py test_vis - to run in standalone mode with dummy data
2575- 2. Called from within the app with actual data
2576- """
2577-
2578- import numpy as np
2579- import streamlit as st
2580-
2581- # Function to create minimal dummy data for testing
2582- def create_dummy_data ():
2583- # Minimal FunnelResults
2584- class DummyFunnelResults :
2585- def __init__ (self ):
2586- self .steps = ["Step 1" , "Step 2" , "Step 3" ]
2587- self .users_count = [1000 , 700 , 400 ]
2588- self .drop_offs = [0 , 300 , 300 ]
2589- self .drop_off_rates = [0 , 30.0 , 42.9 ]
2590- self .conversion_rates = [100.0 , 70.0 , 40.0 ]
2591- self .segment_data = {
2592- "Segment A" : [600 , 400 , 250 ],
2593- "Segment B" : [400 , 300 , 150 ],
2594- }
2595-
2596- # Minimal TimeToConvertStats
2597- class DummyTimeStats :
2598- def __init__ (self , step_from , step_to ):
2599- self .step_from = step_from
2600- self .step_to = step_to
2601- self .conversion_times = np .random .exponential (scale = 2.0 , size = 10 )
2602- self .mean_hours = np .mean (self .conversion_times )
2603- self .median_hours = np .median (self .conversion_times )
2604- self .p25_hours = np .percentile (self .conversion_times , 25 )
2605- self .p75_hours = np .percentile (self .conversion_times , 75 )
2606- self .p90_hours = np .percentile (self .conversion_times , 90 )
2607- self .std_hours = np .std (self .conversion_times )
2608-
2609- # Minimal CohortData
2610- class DummyCohortData :
2611- def __init__ (self ):
2612- self .cohort_labels = ["Cohort 1" , "Cohort 2" ]
2613- self .cohort_sizes = {"Cohort 1" : 500 , "Cohort 2" : 400 }
2614- self .conversion_rates = {
2615- "Cohort 1" : [100.0 , 75.0 , 50.0 ],
2616- "Cohort 2" : [100.0 , 70.0 , 45.0 ],
2617- }
2618-
2619- # Minimal PathAnalysisData
2620- class DummyPathData :
2621- def __init__ (self ):
2622- self .dropoff_paths = {
2623- "Step 1" : {"Other Path 1" : 150 , "Other Path 2" : 100 },
2624- "Step 2" : {"Other Path 3" : 200 , "Other Path 4" : 100 },
2625- }
2626- self .between_steps_events = {
2627- "Step 1 → Step 2" : {"Event 1" : 700 },
2628- "Step 2 → Step 3" : {"Event 2" : 400 },
2629- }
2630-
2631- # Minimal StatSignificanceResult
2632- class DummyStatTest :
2633- def __init__ (self ):
2634- self .segment_a = "Segment A"
2635- self .segment_b = "Segment B"
2636- self .conversion_a = 40.0
2637- self .conversion_b = 25.0
2638- self .p_value = 0.03
2639- self .is_significant = True
2640- self .z_score = 2.5
2641- self .confidence_interval = (0.05 , 0.15 )
2642-
2643- return {
2644- "funnel_results" : DummyFunnelResults (),
2645- "time_stats" : [
2646- DummyTimeStats ("Step 1" , "Step 2" ),
2647- DummyTimeStats ("Step 2" , "Step 3" ),
2648- ],
2649- "cohort_data" : DummyCohortData (),
2650- "path_data" : DummyPathData (),
2651- "stat_tests" : [DummyStatTest (), DummyStatTest ()],
2652- }
2653-
2654- # Function to get real data if available, otherwise use dummy data
2655- def get_test_data ():
2656- # Try to get real data from session state if exists
2657- data = {}
2658-
2659- try :
2660- # Check if we have session state and if we're in the Streamlit context
2661- has_session = (
2662- "session_state" in globals () or "st" in globals () and hasattr (st , "session_state" )
2663- )
2664-
2665- if has_session and hasattr (st .session_state , "analysis_results" ):
2666- results = st .session_state .analysis_results
2667- if results :
2668- data ["funnel_results" ] = results
2669- if hasattr (results , "time_to_convert" ):
2670- data ["time_stats" ] = results .time_to_convert
2671- if hasattr (results , "cohort_data" ):
2672- data ["cohort_data" ] = results .cohort_data
2673- if hasattr (results , "path_analysis" ):
2674- data ["path_data" ] = results .path_analysis
2675- if hasattr (results , "stat_significance" ):
2676- data ["stat_tests" ] = results .stat_significance
2677- except Exception :
2678- pass # If we can't access session state or it's not properly initialized
2679-
2680- # For any missing data, fill with dummy data
2681- dummy_data = create_dummy_data ()
2682- for key in dummy_data :
2683- if key not in data or not data [key ]:
2684- data [key ] = dummy_data [key ]
2685-
2686- return data
2687-
2688- # Track test results
2689- test_results = {"passed" : [], "failed" : []}
2690-
2691- # Get test data (real or dummy)
2692- data = get_test_data ()
2693-
2694- # Set up Streamlit page
2695- st .title ("Visualization Tests" )
2696- st .markdown (
2697- "This test page verifies that all visualizations render correctly with dark theme."
2698- )
2699-
2700- # Run tests for each visualization
2701- with st .expander ("Test Details" , expanded = True ):
2702- # Test 1: Funnel Chart
2703- try :
2704- funnel_chart = FunnelVisualizer .create_funnel_chart (data ["funnel_results" ])
2705- test_results ["passed" ].append ("Funnel Chart" )
2706- st .success ("✅ Funnel Chart" )
2707- except Exception as e :
2708- test_results ["failed" ].append (("Funnel Chart" , str (e )))
2709- st .error (f"❌ Funnel Chart: { str (e )} " )
2710-
2711- # Test 2: Segmented Funnel Chart
2712- try :
2713- segmented_funnel = FunnelVisualizer .create_funnel_chart (
2714- data ["funnel_results" ], show_segments = True
2715- )
2716- test_results ["passed" ].append ("Segmented Funnel" )
2717- st .success ("✅ Segmented Funnel" )
2718- except Exception as e :
2719- test_results ["failed" ].append (("Segmented Funnel" , str (e )))
2720- st .error (f"❌ Segmented Funnel: { str (e )} " )
2721-
2722- # Test 3: Conversion Flow Sankey
2723- try :
2724- flow_chart = FunnelVisualizer .create_conversion_flow_sankey (data ["funnel_results" ])
2725- test_results ["passed" ].append ("Conversion Flow Sankey" )
2726- st .success ("✅ Conversion Flow Sankey" )
2727- except Exception as e :
2728- test_results ["failed" ].append (("Conversion Flow Sankey" , str (e )))
2729- st .error (f"❌ Conversion Flow Sankey: { str (e )} " )
2730-
2731- # Test 4: Time to Convert Chart
2732- try :
2733- time_chart = FunnelVisualizer .create_time_to_convert_chart (data ["time_stats" ])
2734- test_results ["passed" ].append ("Time to Convert Chart" )
2735- st .success ("✅ Time to Convert Chart" )
2736- except Exception as e :
2737- test_results ["failed" ].append (("Time to Convert Chart" , str (e )))
2738- st .error (f"❌ Time to Convert Chart: { str (e )} " )
2739-
2740- # Test 5: Cohort Heatmap
2741- try :
2742- cohort_chart = FunnelVisualizer .create_cohort_heatmap (data ["cohort_data" ])
2743- test_results ["passed" ].append ("Cohort Heatmap" )
2744- st .success ("✅ Cohort Heatmap" )
2745- except Exception as e :
2746- test_results ["failed" ].append (("Cohort Heatmap" , str (e )))
2747- st .error (f"❌ Cohort Heatmap: { str (e )} " )
2748-
2749- # Test 6: Path Analysis Chart
2750- try :
2751- path_chart = FunnelVisualizer .create_path_analysis_chart (data ["path_data" ])
2752- test_results ["passed" ].append ("Path Analysis Chart" )
2753- st .success ("✅ Path Analysis Chart" )
2754- except Exception as e :
2755- test_results ["failed" ].append (("Path Analysis Chart" , str (e )))
2756- st .error (f"❌ Path Analysis Chart: { str (e )} " )
2757-
2758- # Test 7: Statistical Significance Table
2759- try :
2760- stat_table = FunnelVisualizer .create_statistical_significance_table (data ["stat_tests" ])
2761- test_results ["passed" ].append ("Statistical Significance Table" )
2762- st .success ("✅ Statistical Significance Table" )
2763- except Exception as e :
2764- test_results ["failed" ].append (("Statistical Significance Table" , str (e )))
2765- st .error (f"❌ Statistical Significance Table: { str (e )} " )
2766-
2767- # Show overall test result
2768- if not test_results ["failed" ]:
2769- st .success (f"✅ All { len (test_results ['passed' ])} visualizations passed!" )
2770- else :
2771- st .error (
2772- f"❌ { len (test_results ['failed' ])} of { len (test_results ['passed' ]) + len (test_results ['failed' ])} tests failed."
2773- )
27742559
2775- # Show successful visualizations
2776- if test_results ["passed" ]:
2777- st .subheader ("Successful Visualizations" )
2778-
2779- # Display the charts that passed
2780- for viz_name in test_results ["passed" ]:
2781- if viz_name == "Funnel Chart" :
2782- st .subheader ("1. Funnel Chart" )
2783- st .plotly_chart (funnel_chart , use_container_width = True )
2784- elif viz_name == "Segmented Funnel" :
2785- st .subheader ("2. Segmented Funnel Chart" )
2786- st .plotly_chart (segmented_funnel , use_container_width = True )
2787- elif viz_name == "Conversion Flow Sankey" :
2788- st .subheader ("3. Conversion Flow Sankey" )
2789- st .plotly_chart (flow_chart , use_container_width = True )
2790- elif viz_name == "Time to Convert Chart" :
2791- st .subheader ("4. Time to Convert Chart" )
2792- st .plotly_chart (time_chart , use_container_width = True )
2793- elif viz_name == "Cohort Heatmap" :
2794- st .subheader ("5. Cohort Heatmap" )
2795- st .plotly_chart (cohort_chart , use_container_width = True )
2796- elif viz_name == "Path Analysis Chart" :
2797- st .subheader ("6. Path Analysis Chart" )
2798- st .plotly_chart (path_chart , use_container_width = True )
2799- elif viz_name == "Statistical Significance Table" :
2800- st .subheader ("7. Statistical Significance Table" )
2801- st .dataframe (stat_table )
2802-
2803- return {
2804- "success" : len (test_results ["failed" ]) == 0 ,
2805- "passed" : test_results ["passed" ],
2806- "failed" : test_results ["failed" ],
2807- }
28082560
28092561
28102562if __name__ == "__main__" :
2811- import sys
2812-
2813- if len (sys .argv ) > 1 and sys .argv [1 ] == "test_vis" :
2814- test_visualizations ()
2815- else :
2816- main ()
2563+ main ()
0 commit comments