1+ """
2+ Module providing charting functions.
3+
4+ This module will likely be moved to the /shared/python directory in the future once it's more generic.
5+ """
6+
7+ import pandas as pd
8+ import matplotlib .pyplot as plt
9+ from matplotlib .patches import Rectangle as pltRectangle
10+ import matplotlib as mpl
11+ import json
12+
13+
14+ # ------------------------------
15+ # CLASSES
16+ # ------------------------------
17+
18+ # TODO: A specialized barchart for multi-request scenarios should be created and use a more generic base class barchart.
19+ # TODO: BarChart should be a base class for other chart types once it's more generic.
20+ class BarChart (object ):
21+ """
22+ Class for creating bar charts with colored bars based on backend indexes.
23+ """
24+
25+ def __init__ (self , title : str , x_label : str , y_label : str , api_results : list [dict ], fig_text : str = None ):
26+ """
27+ Initialize the BarChart with API results.
28+
29+ Args:
30+ api_results (list[dict]): List of API result dictionaries.
31+ """
32+
33+ self .title = title
34+ self .x_label = x_label
35+ self .y_label = y_label
36+ self .api_results = api_results
37+ self .fig_text = fig_text
38+
39+ def plot (self ):
40+ """
41+ Plot the bar chart based on the provided API results.
42+ """
43+ self ._plot_barchart (self .api_results )
44+
45+ def _plot_barchart (self , api_results : list [dict ]):
46+ """
47+ Internal method to plot the bar chart.
48+
49+ Args:
50+ api_results (list[dict]): List of API result dictionaries.
51+ """
52+ # Parse the data into a DataFrame
53+ rows = []
54+
55+ for entry in api_results :
56+ run = entry ['run' ]
57+ response_time = entry ['response_time' ]
58+ status_code = entry ['status_code' ]
59+
60+ if status_code == 200 and entry ['response' ]:
61+ try :
62+ resp = json .loads (entry ['response' ])
63+ backend_index = resp .get ('index' , 99 )
64+ except Exception :
65+ backend_index = 99
66+ else :
67+ backend_index = 99
68+ rows .append ({
69+ 'Run' : run ,
70+ 'Response Time (ms)' : response_time * 1000 , # Convert to ms
71+ 'Backend Index' : backend_index ,
72+ 'Status Code' : status_code
73+ })
74+
75+ df = pd .DataFrame (rows )
76+
77+ mpl .rcParams ['figure.figsize' ] = [15 , 7 ]
78+
79+ # Define a color map for each backend index (200) and errors (non-200 always lightcoral)
80+ backend_indexes_200 = sorted (df [df ['Status Code' ] == 200 ]['Backend Index' ].unique ())
81+ color_palette = ['lightyellow' , 'lightblue' , 'lightgreen' , 'plum' , 'orange' ]
82+ color_map_200 = {idx : color_palette [i % len (color_palette )] for i , idx in enumerate (backend_indexes_200 )}
83+
84+ bar_colors = []
85+ for _ , row in df .iterrows ():
86+ if row ['Status Code' ] == 200 :
87+ bar_colors .append (color_map_200 .get (row ['Backend Index' ], 'gray' ))
88+ else :
89+ bar_colors .append ('lightcoral' )
90+
91+ # Plot the dataframe with colored bars
92+ ax = df .plot (
93+ kind = 'bar' ,
94+ x = 'Run' ,
95+ y = 'Response Time (ms)' ,
96+ color = bar_colors ,
97+ legend = False ,
98+ edgecolor = 'black'
99+ )
100+
101+ # Add dynamic legend based on backend indexes present in the data
102+ legend_labels = []
103+ legend_names = []
104+ for idx in backend_indexes_200 :
105+ legend_labels .append (pltRectangle ((0 , 0 ), 1 , 1 , color = color_map_200 [idx ]))
106+ legend_names .append (f'Backend index { idx } (200)' )
107+ legend_labels .append (pltRectangle ((0 , 0 ), 1 , 1 , color = 'lightcoral' ))
108+ legend_names .append ('Error/Other (non-200)' )
109+ ax .legend (legend_labels , legend_names )
110+
111+ plt .title (self .title )
112+ plt .xlabel (self .x_label )
113+ plt .ylabel (self .y_label )
114+ plt .xticks (rotation = 0 )
115+
116+ # Exclude high outliers for average calculation
117+ valid_200 = df [(df ['Status Code' ] == 200 )].copy ()
118+ if not valid_200 .empty :
119+ # Exclude high outliers (e.g., above 95th percentile)
120+ if not valid_200 .empty :
121+ upper = valid_200 ['Response Time (ms)' ].quantile (0.95 )
122+ filtered = valid_200 [valid_200 ['Response Time (ms)' ] <= upper ]
123+ if not filtered .empty :
124+ avg = filtered ['Response Time (ms)' ].mean ()
125+ avg_label = f'Mean APIM response time: { avg :.1f} ms'
126+ plt .axhline (y = avg , color = 'b' , linestyle = '--' )
127+ plt .text (len (df ) - 1 , avg , avg_label , color = 'b' , va = 'bottom' , ha = 'right' , fontsize = 10 )
128+
129+ # Add figtext under the chart
130+ plt .figtext (0.13 , - 0.1 , wrap = True , ha = 'left' , fontsize = 11 , s = self .fig_text )
131+
132+ plt .show ()
0 commit comments