55import sys
66import logging
77
8- # Use Plotly's Chrome bootstrapper instead of kaleido
98import plotly .io as pio
109try :
11- # This should find or install a compatible Chrome in a user-writable location
1210 pio .get_chrome ()
1311except Exception as e :
1412 logging .warning (
1513 "Plotly could not fetch or find Chrome automatically. "
1614 "Static exports may fail unless BROWSER_PATH is set. Details: %s" , e
1715 )
16+
17+ # DPI-aware writer
18+ try :
19+ from nanoplot .utils import write_static_image
20+ except Exception as e :
21+ logging .warning ("Could not import write_static_image from nanoplot.utils: %s" , e )
22+ write_static_image = None
23+
1824
1925class Plot (object ):
2026 """A Plot object is defined by a path to the output file and the title of the plot."""
2127
2228 only_report = False
23-
29+
2430 def __init__ (self , path , title ):
2531 self .path = path
2632 self .title = title
27- self .fig = None
33+ self .fig = None # Plotly fig for HTML/static; Matplotlib Figure in legacy mode
2834 self .html = None
2935
3036 def encode (self ):
@@ -40,60 +46,99 @@ def encode1(self):
4046 return '<img src="data:image/png;base64,{0}">' .format (data_uri )
4147
4248 def encode2 (self ):
49+ # Legacy Matplotlib path only
4350 buf = BytesIO ()
4451 self .fig .savefig (buf , format = "png" , bbox_inches = "tight" , dpi = 100 )
4552 buf .seek (0 )
4653 string = b64encode (buf .read ())
4754 return '<img src="data:image/png;base64,{0}">' .format (urlquote (string ))
4855
4956 def save (self , settings ):
50- if not self .only_report :
51- if self .html :
52- with open (self .path , "w" ) as html_out :
53- html_out .write (self .html )
54- if not settings ["no_static" ]:
55- try :
56- for fmt in settings ["format" ]:
57- self .save_static (fmt )
58- except (AttributeError , ValueError ) as e :
59- p = os .path .splitext (self .path )[0 ] + ".png"
60- if os .path .exists (p ):
61- os .remove (p )
62- logging .warning ("No static plots are saved due to an export problem:" )
63- logging .warning (e )
64-
65- elif self .fig :
66- if isinstance (settings ["format" ], list ):
67- for fmt in settings ["format" ]:
68- self .fig .savefig (
69- fname = self .path + "." + fmt ,
70- format = fmt ,
71- bbox_inches = "tight" ,
72- )
73- else :
57+ if self .only_report :
58+ return
59+
60+ if self .html :
61+ # Save the interactive HTML
62+ with open (self .path , "w" ) as html_out :
63+ html_out .write (self .html )
64+
65+ # Also save static images unless suppressed
66+ if not settings .get ("no_static" , False ):
67+ try :
68+ fmts = settings .get ("format" , ["png" ])
69+ for fmt in fmts if isinstance (fmts , list ) else [fmts ]:
70+ self .save_static (fmt , settings ) # pass settings
71+ except (AttributeError , ValueError ) as e :
72+ p = os .path .splitext (self .path )[0 ] + ".png"
73+ if os .path .exists (p ):
74+ os .remove (p )
75+ logging .warning ("No static plots are saved due to an export problem:" )
76+ logging .warning (e )
77+
78+ elif self .fig :
79+ # Legacy Matplotlib path
80+ fmts = settings .get ("format" , ["png" ])
81+ dpi = int (settings .get ("dpi" , 300 ))
82+ if isinstance (fmts , list ):
83+ for fmt in fmts :
7484 self .fig .savefig (
75- fname = self .path ,
76- format = settings [ "format" ] ,
85+ fname = self .path + "." + fmt ,
86+ format = fmt ,
7787 bbox_inches = "tight" ,
88+ dpi = dpi ,
7889 )
7990 else :
80- sys .exit ("No method to save plot object: no html or fig defined." )
91+ self .fig .savefig (
92+ fname = self .path ,
93+ format = fmts ,
94+ bbox_inches = "tight" ,
95+ dpi = dpi ,
96+ )
97+ else :
98+ sys .exit ("No method to save plot object: no html or fig defined." )
8199
82100 def show (self ):
83101 if self .fig :
84- return self .fig .fig
102+ return getattr ( self .fig , "fig" , self .fig )
85103 else :
86104 sys .stderr .write (".show not implemented for Plot instance without fig attribute!" )
87105
88- def save_static (self , figformat ):
106+ def save_static (self , figformat , settings ):
89107 """
90- Export a Plotly figure using Plotly's image writer.
108+ Export a Plotly figure as a static image with real DPI.
109+ Prefers utils.write_static_image; falls back to explicit pixel size.
91110 """
92111 output_path = self .path .replace (".html" , f".{ figformat } " )
112+ dpi = int (settings .get ("dpi" , 300 ))
113+
114+ if self .fig is None :
115+ logging .warning ("No figure attached to Plot; skipping static export for %s" , output_path )
116+ return
117+
118+ # JSON just dumps the figure spec
119+ if figformat .lower () == "json" :
120+ try :
121+ pio .write_json (self .fig , output_path )
122+ logging .info ("Saved %s as JSON" , output_path )
123+ except Exception as e :
124+ logging .warning ("Failed to write JSON for %s: %s" , output_path , e )
125+ return
126+
127+ # Preferred path: DPI-aware helper
93128 try :
94- pio .write_image (self .fig , output_path , format = figformat )
95- logging .info (f"Saved { output_path } as { figformat } " )
129+ if write_static_image is not None :
130+ write_static_image (self .fig , output_path , dpi = dpi )
131+ logging .info ("Saved %s as %s (dpi=%d)" , output_path , figformat , dpi )
132+ return
133+ except Exception as e :
134+ logging .warning ("DPI helper failed for %s: %s; falling back to explicit px size" , output_path , e )
135+
136+ # Hard fallback so we don't end up at 700x500 defaults
137+ width_px = int (6.4 * dpi )
138+ height_px = int (4.8 * dpi )
139+ try :
140+ pio .write_image (self .fig , output_path , width = width_px , height = height_px , scale = 1 )
141+ logging .info ("Fallback saved %s at %dx%d px" , output_path , width_px , height_px )
96142 except Exception as e :
97143 logging .warning ("No static plots are saved due to an export problem:" )
98144 logging .warning (e )
99-
0 commit comments