11from datetime import date
22import enum
33import re
4+ from decimal import Decimal , localcontext
45from typing import Optional , List , Tuple , Literal , Any , Union , Sequence
56
67import matplotlib
@@ -35,6 +36,18 @@ def _is_grid_line(line: Line2D) -> bool:
3536 return False
3637
3738
39+ def _dynamic_round (number ):
40+ # Convert to Decimal for precise control
41+ decimal_number = Decimal (str (number ))
42+
43+ # Dynamically determine precision based on magnitude
44+ precision = max (1 , 16 - decimal_number .adjusted ()) # 16 digits of precision
45+
46+ with localcontext () as ctx :
47+ ctx .prec = precision # Set the dynamic precision
48+ return + decimal_number # The + operator applies rounding
49+
50+
3851class ChartType (str , enum .Enum ):
3952 LINE = "line"
4053 SCATTER = "scatter"
@@ -291,6 +304,7 @@ class BoxAndWhiskerData(BaseModel):
291304 median : float
292305 third_quartile : float
293306 max : float
307+ outliers : List [float ] = Field (default_factory = list )
294308
295309
296310class BoxAndWhiskerChart (Chart2D ):
@@ -301,20 +315,23 @@ class BoxAndWhiskerChart(Chart2D):
301315 def _extract_info (self , ax : Axes ) -> None :
302316 super ()._extract_info (ax )
303317
318+ labels = [item .get_text () for item in ax .get_xticklabels ()]
319+
304320 boxes = []
305- for box in ax .patches :
321+ for label , box in zip ( labels , ax .patches ) :
306322 vertices = box .get_path ().vertices
307- x_vertices = vertices [:, 0 ]
308- y_vertices = vertices [:, 1 ]
323+ x_vertices = [ _dynamic_round ( x ) for x in vertices [:, 0 ] ]
324+ y_vertices = [ _dynamic_round ( y ) for y in vertices [:, 1 ] ]
309325 x = min (x_vertices )
310326 y = min (y_vertices )
311327 boxes .append (
312328 {
313329 "x" : x ,
314330 "y" : y ,
315- "label" : box .get_label (),
316- "width" : round (max (x_vertices ) - x , 4 ),
317- "height" : round (max (y_vertices ) - y , 4 ),
331+ "label" : label ,
332+ "width" : max (x_vertices ) - x ,
333+ "height" : max (y_vertices ) - y ,
334+ "outliers" : [],
318335 }
319336 )
320337
@@ -328,13 +345,21 @@ def _extract_info(self, ax: Axes) -> None:
328345 box ["x" ], box ["y" ] = box ["y" ], box ["x" ]
329346 box ["width" ], box ["height" ] = box ["height" ], box ["width" ]
330347
331- for line in ax .lines :
332- xdata = line .get_xdata ()
333- ydata = line .get_ydata ()
348+ for i , line in enumerate ( ax .lines ) :
349+ xdata = [ _dynamic_round ( x ) for x in line .get_xdata ()]
350+ ydata = [ _dynamic_round ( y ) for y in line .get_ydata ()]
334351
335352 if orientation == "vertical" :
336353 xdata , ydata = ydata , xdata
337354
355+ if len (xdata ) == 1 :
356+ for box in boxes :
357+ if box ["x" ] <= xdata [0 ] <= box ["x" ] + box ["width" ]:
358+ break
359+ else :
360+ continue
361+
362+ box ["outliers" ].append (ydata [0 ])
338363 if len (ydata ) != 2 :
339364 continue
340365 for box in boxes :
@@ -344,6 +369,7 @@ def _extract_info(self, ax: Axes) -> None:
344369 continue
345370
346371 if (
372+ # Check if the line is inside the box, prevent floating point errors
347373 ydata [0 ] == ydata [1 ]
348374 and box ["y" ] <= ydata [0 ] <= box ["y" ] + box ["height" ]
349375 ):
@@ -365,6 +391,7 @@ def _extract_info(self, ax: Axes) -> None:
365391 median = box ["median" ],
366392 third_quartile = box ["y" ] + box ["height" ],
367393 max = box ["whisker_upper" ],
394+ outliers = box ["outliers" ],
368395 )
369396 for box in boxes
370397 ]
0 commit comments