diff --git a/opencsp/common/lib/render/View3d.py b/opencsp/common/lib/render/View3d.py index c31cd74dd..77300da01 100644 --- a/opencsp/common/lib/render/View3d.py +++ b/opencsp/common/lib/render/View3d.py @@ -369,7 +369,11 @@ def save(self, output_dir, output_figure_body, format='png', dpi=300) -> str: # Save the figure. output_figure_dir_body_ext = output_figure_dir_body + '.' + format lt.info('In View3d.save(), saving figure: ' + output_figure_dir_body_ext) - # plt.savefig(output_figure_dir_body_ext, format=format, dpi=dpi) + self.view.set_size_inches(self.view.get_figwidth(), self.view.get_figheight(), forward=True) + # it could be that another backend requires the following instead: + # self.view.set_figwidth(self.view.get_figwidth() * dpi) + # self.view.set_figheight(self.view.get_figheight() * dpi) + self.view.set_dpi(dpi) self.view.savefig(output_figure_dir_body_ext, format=format, dpi=dpi) # Return the outptu file path and directory. return output_figure_dir_body_ext diff --git a/opencsp/common/lib/render/figure_management.py b/opencsp/common/lib/render/figure_management.py index 2545465e2..0f7352e3c 100644 --- a/opencsp/common/lib/render/figure_management.py +++ b/opencsp/common/lib/render/figure_management.py @@ -230,7 +230,17 @@ def _setup_figure( upper_left_xy = figure_control.upper_left_xy x = upper_left_xy[0] y = upper_left_xy[1] - fig.canvas.manager.window.move(x, y) + window = fig.canvas.manager.window + if hasattr(window, "move"): + window.move(x, y) # qt + else: + window.geometry(f"+{x}+{y}") # tkinter + if figure_control.maximize: + window = fig.canvas.manager.window + if hasattr(window, "showMaximized"): + window.showMaximized() # qt + else: + window.state("zoomed") # tkinter # Copying this command, as from Randy, which suppresses duplicate axes in tile_figure(). ~ BGB plt.axis('off') diff --git a/opencsp/common/lib/render/test/test_figure_management.py b/opencsp/common/lib/render/test/test_figure_management.py index 2762c71a3..ae7700279 100644 --- a/opencsp/common/lib/render/test/test_figure_management.py +++ b/opencsp/common/lib/render/test/test_figure_management.py @@ -2,10 +2,13 @@ import unittest import matplotlib.pyplot as plt +from PIL import Image import opencsp.common.lib.render.figure_management as fm -import opencsp.common.lib.render.test.lib.RenderControlFigureRecordInfSave as rcfr_is +import opencsp.common.lib.render.view_spec as vs +import opencsp.common.lib.render_control.RenderControlAxis as rca import opencsp.common.lib.render_control.RenderControlFigure as rcfg +import opencsp.common.lib.render.test.lib.RenderControlFigureRecordInfSave as rcfr_is import opencsp.common.lib.tool.file_tools as ft is_original_call = "--funcname" in sys.argv @@ -18,14 +21,15 @@ class test_figure_management(unittest.TestCase): @classmethod def setUpClass(cls) -> None: path, name, _ = ft.path_components(__file__) - cls.dir_in = ft.join(path, 'data/input', name.split('test_')[-1]) - cls.dir_out = ft.join(path, 'data/output', name.split('test_')[-1]) - - ret = super().setUpClass() - ft.create_directories_if_necessary(cls.dir_out) + cls.in_dir = ft.join(path, 'data/input', name.split('test_')[-1]) + cls.out_dir = ft.join(path, 'data/output', name.split('test_')[-1]) + ft.create_directories_if_necessary(cls.out_dir) if is_original_call: - ft.delete_files_in_directory(cls.dir_out, "*") - return ret + ft.delete_files_in_directory(cls.out_dir, "*") + return super().setUpClass() + + def setUp(self) -> None: + self.test_name = self.id().split('.')[-1] def setUp(self) -> None: self.test_name = self.id().split('.')[-1] @@ -58,7 +62,7 @@ def test_save_all_figures_line(self): line = list(range(100)) view.draw_p_list(line) - figs_txts = fm.save_all_figures(self.dir_out) + figs_txts = fm.save_all_figures(self.out_dir) self.assert_exists(figs_txts, 1) def test_save_all_figures_two_lines(self): @@ -76,7 +80,7 @@ def test_save_all_figures_two_lines(self): line = lines[i] view.draw_p_list(line) - figs_txts = fm.save_all_figures(self.dir_out) + figs_txts = fm.save_all_figures(self.out_dir) self.assert_exists(figs_txts, 2) def _figure_manager_timeout_1(self): @@ -106,6 +110,87 @@ def _figure_manager_timeout_1(self): return fm + def test_upper_left_xy_no_exception(self): + """ + Verify that figure_management._setup_figure() with the figure control + parameter "upper_left_xy" set doesn't raise an exception. + """ + # TODO how to test that the window has actually been located correctly? + axis_control = rca.meters() + figure_control = rcfg.RenderControlFigure(tile=False, upper_left_xy=(100, 100)) + view_spec_2d = vs.view_spec_xy() + fig_record = fm.setup_figure( + figure_control, + axis_control, + view_spec_2d, + title=self.test_name, + code_tag=f"{__file__}.{self.test_name}", + equal=False, + ) + fig_record.view.show() + fig_record.close() + + def test_maximize_no_exception(self): + """ + Verify that figure_management._setup_figure() with the figure control + parameter "maximize" set doesn't raise an exception. + """ + # TODO how to test that the window has actually been maximized? + axis_control = rca.meters() + figure_control = rcfg.RenderControlFigure(tile=False, maximize=True) + view_spec_2d = vs.view_spec_xy() + try: + fig_record = fm.setup_figure( + figure_control, + axis_control, + view_spec_2d, + title=self.test_name, + code_tag=f"{__file__}.{self.test_name}", + equal=False, + ) + fig_record.view.show() + fig_record.close() + except Exception as ex: + ubi8_msg = '_tkinter.TclError: bad argument "zoomed": must be normal, iconic, or withdrawn' + if ubi8_msg in str(ex): + # TODO how to make this test work on ubi8? + self.skipTest("Window 'maximize' state doesn't working on our ubi8 test docker image.") + + def test_save_figsize(self): + """Verify that the size of the saved figure is as given in the save parameters.""" + # create and save the figure with pixel sizes: + # small: 900 x 600 + # regular: 1800 x 1200 + # large: 2700 x 1800 + axis_control = rca.meters() + figure_control = rcfg.RenderControlFigure(tile=False, figsize=(3, 2)) + view_spec_2d = vs.view_spec_xy() + fig_record = fm.setup_figure( + figure_control, + axis_control, + view_spec_2d, + title=self.test_name, + code_tag=f"{__file__}.{self.test_name}", + equal=False, + ) + # fig_record.view.show() + fig_record.save(self.out_dir, f"{self.test_name}_small", format="png", dpi=300, close_after_save=False) + fig_record.save(self.out_dir, f"{self.test_name}_regular", format="png", dpi=600, close_after_save=False) + fig_record.save(self.out_dir, f"{self.test_name}_large", format="png", dpi=900, close_after_save=False) + fig_record.view.show() + fig_record.close() + + # load the images and verify their size in pixels + with Image.open(ft.join(self.out_dir, f"{self.test_name}_small_xy.png")) as img_small: + self.assertEqual(img_small.width, 900) + self.assertEqual(img_small.height, 600) + with Image.open(ft.join(self.out_dir, f"{self.test_name}_regular_xy.png")) as img_regular: + self.assertEqual(img_regular.width, 1800) + self.assertEqual(img_regular.height, 1200) + with Image.open(ft.join(self.out_dir, f"{self.test_name}_large_xy.png")) as img_large: + self.assertEqual(img_large.width, 2700) + self.assertEqual(img_large.height, 1800) + if __name__ == '__main__': import argparse diff --git a/opencsp/common/lib/render_control/RenderControlFigure.py b/opencsp/common/lib/render_control/RenderControlFigure.py index 795c3929d..909c927ae 100644 --- a/opencsp/common/lib/render_control/RenderControlFigure.py +++ b/opencsp/common/lib/render_control/RenderControlFigure.py @@ -12,11 +12,12 @@ class RenderControlFigure: def __init__( self, tile=True, # True => Lay out figures in grid. False => Place at upper_left or default screen center. - tile_array=(3, 2), # (n_x, n_y) + tile_array: tuple[int, int] = (3, 2), # (n_x, n_y) tile_square=False, # Set to True for equal-axis 3d plots. figsize=(6.4, 4.8), # inch. upper_left_xy=None, # pixel. (0,0) --> Upper left corner of screen. grid=True, + maximize=False, ): # Whether or not to draw grid lines. """Set of controls for how to render figures. @@ -35,13 +36,27 @@ def __init__( view.draw_pq_list(energy_values, style=style) view.show(block=True) - Args: - - tile (bool): True => Lay out figures in grid. False => Place at upper_left or default screen center. Default True - - tile_array (tuple[int]): How many tiles across and down (n_x, n_y). Default (3, 2) - - tile_square (bool): Set to True for equal-axis 3d plots. Default False - - figsize (tuple[float]): Size of the figure in inches. Default (6.4, 4.8) - - upper_left_xy (tuple[int]): Pixel placement for the first tile. (0,0) --> Upper left corner of screen. Default None - - grid (bool): Whether or not to draw grid lines. Note: this value seems to be inverted. Default True + Params: + ------- + tile : bool, optional + True => Lay out figures in grid. False => Place at upper_left or + default screen center. If True, then figsize, upper_left_xy, and + maximize are ignored. Default True + tile_array : tuple[int] | None, optional + How many tiles across and down (n_x, n_y). + tile_square : bool, optional + Set to True for equal-axis 3d plots. Default False + figsize : tuple[float], optional + Size of the figure in inches. Ignored if tile is True. Default (6.4, 4.8) + upper_left_xy : tuple[int], optional + Pixel placement for the first tile. (0,0) --> Upper left corner of + screen. Ignored if tile is True. Default None + grid : bool, optional + Whether or not to draw grid lines. Note: this value seems to be + inverted. Default True + maximize : bool, optional + Whether the figure should be maximized (made full screen) as soon as + it is made visible. Ignored if tile is True. Default False. """ super(RenderControlFigure, self).__init__() @@ -55,6 +70,7 @@ def __init__( # Figure size and placement. self.figsize = figsize self.upper_left_xy = upper_left_xy + self.maximize = maximize # Axis control. self.x_label = 'x (m)'