def __init__(self, io): # create the widgets connectivity = v.Select(label=cm.acc.connectivity, items=cp.connectivity, v_model=cp.connectivity[0]['value']) edge_width = v.Slider(label=cm.mspa.edge_width, min=1, max=30, v_model=1) transition = v.Switch(label=cm.mspa.transition, false_value=0, true_value=1, v_model=1) int_ext = v.Switch(label=cm.mspa.int_ext, false_value=0, true_value=1, v_model=1) # bind to the io self.output = sw.Alert() \ .bind(connectivity, io, 'connectivity') \ .bind(edge_width, io, 'edge_width') \ .bind(transition, io, 'transition') \ .bind(int_ext, io, 'int_ext') super().__init__( io=io, inputs=[connectivity, edge_width, transition, int_ext], output=self.output)
def download_tile(obj, w_selection): def bind_change(change, obj, attr): setattr(obj, attr, change['new']) w_overwrite = v.Switch(v_model=obj.overwrite, inset=True, label="Overwrite SEPAL images") w_overwrite.observe(partial(bind_change, obj=obj, attr='overwrite'), 'v_model') w_remove = v.Switch(v_model=obj.rmdrive, inset=True, label="Remove Google Drive Images") w_remove.observe(partial(bind_change, obj=obj, attr='rmdrive'), 'v_model') out = widgets.Output() btn = s.Btn(text="Download", icon='download') # Create an alert element for the process process_alert = s.Alert() on_download(obj, w_selection, btn, out, process_alert) html_header = """ <style> .widget-html span { color:black!important; } div.output_stderr{ color:none; } </style> <p>With this module you can track and download the images processed into your Google Earth Engine account by providing the 'tasks' text file, the results will be stored directly into your SEPAL account.</br> <b>Note that if you check the overwrite and remove options, the result can't be undone.</b> </p> """ download_content = v.Layout( class_="pa-5", row=True, align_center=True, children=[ v.Flex(xs12=True, children=[ v.Sheet(class_="pa-5", children=[ widgets.HTML(html_header), w_selection, w_overwrite, w_remove, btn, process_alert, out, ]) ]) ]) return download_content
def __init__(self, aoi_model, model, viz_tile, export_tile, **kwargs): # gather the model self.aoi_model = aoi_model self.model = model self.viz_tile = viz_tile self.export_tile = export_tile # widgets w_time_title = v.Html(tag="H3", class_="mt-3", children=[ms.selection.time_range]) self.start_picker = sw.DatePicker(label=ms.selection.start) self.end_picker = sw.DatePicker(label=ms.selection.end) w_time_range = v.Layout(row=True, children=[self.start_picker, self.end_picker]) w_collection_title = v.Html(tag="H3", class_="mt-3", children=[ms.selection.collection]) self.sensors = v.Select( label=ms.selection.sensor, items=pm.sensors, v_model=None, chips=True, multiple=True, ) self.t2 = v.Switch(class_="ml-5", label=ms.selection.t2, v_model=False) self.sr = v.Switch(class_="ml-5", label=ms.selection.sr, v_model=False) self.model.bind(self.start_picker, "start").bind(self.end_picker, "end").bind( self.sensors, "sensors").bind(self.t2, "t2").bind(self.sr, "sr") # construct the Tile with the widget we have initialized super().__init__( id_= "selection_widget", # the id will be used to make the Tile appear and disapear title=ms.selection. title, # the Title will be displayed on the top of the tile inputs=[ w_time_title, w_time_range, w_collection_title, self.sensors, self.t2, self.sr, ], btn=sw.Btn(ms.selection.btn), alert=sw.Alert(), ) # now that the Tile is created we can link it to a specific function self.btn.on_event("click", self._on_run)
def __init__(self, model): # create the widgets connectivity = v.Select( label=cm.acc.connectivity, items=cp.connectivity, v_model=cp.connectivity[0]["value"], ) res = v.TextField( label=cm.acc.res, type="number", v_model=None, hint=cm.acc.res_hint ) thresholds = cw.Thresholds(label=cm.acc.thresholds) options = v.Select( label=cm.acc.options, items=cp.acc_options, v_model=cp.acc_options[0]["value"], ) big_3_pink = v.Switch(label=cm.acc.big3pink, v_model=True) # bind to the ( model.bind(connectivity, "connectivity") .bind(res, "res") .bind(thresholds.save, "thresholds") .bind(options, "options") .bind(big_3_pink, "big_3_pink") ) # extra js behaviour res.on_event("focusout", self._on_focus_out) super().__init__( model=model, inputs=[connectivity, res, thresholds, options, big_3_pink] )
def __init__(self, aoi_model, model, **kwargs): # gather the model self.aoi_model = aoi_model self.model = model # widgets self.measure = v.Select(label=ms.selection.measure, v_model=None, items=pm.measures) self.annual = v.Switch(class_="ml-5", label=ms.selection.annual, v_model=False) # add the map self.m = sm.SepalMap() # create an output alert self.model.bind(self.measure, "measure").bind(self.annual, "annual") # construct the Tile with the widget we have initialized super().__init__( id_= "visualization_widget", # the id will be used to make the Tile appear and disapear title=ms.visualization. title, # the Title will be displayed on the top of the tile inputs=[self.measure, self.annual, self.m], alert=sw.Alert(), ) self.measure.observe(self._on_change, "v_model") self.annual.observe(self._on_change, "v_model")
def __init__(self, test_case_widget, lb_scheme_widget, discret_widget, **kwargs): self.test_case_widget = test_case_widget self.lb_scheme_widget = lb_scheme_widget self.discret_widget = discret_widget self.params, self.relax_params = None, None self.select_param = v.Select(label='Parameters', v_model=None, items=[]) self.select_relax = v.Select(label='relaxation rates', v_model=[], items=[], multiple=True, class_='d-none') self.srt_relax = v.Switch(label='Single relaxation time', v_model=True, class_='d-none') self.sigma = v.Switch(label='Using sigma', v_model=False, class_='d-none') self.in_log = v.Switch(label='Using log10', v_model=False, class_='d-none') self.minmax = v.Layout() self.update_select_fields(None) test_case_widget.select_case.observe(self.update_select_fields, 'v_model') lb_scheme_widget.select_case.observe(self.update_select_fields, 'v_model') discret_widget['dx'].observe(self.update_select_fields, 'v_model') self.fields = [ self.select_param, self.select_relax, self.srt_relax, self.sigma, self.in_log, self.minmax ] super().__init__(v_model='valid', children=self.fields) self.select_param.observe(self.select_param_changed, 'v_model') self.select_relax.observe(self.select_relax_all, 'v_model') self.select_relax.observe(self.check_changes, 'v_model') self.in_log.observe(self.check_changes, 'v_model') self.sigma.observe(self.check_changes, 'v_model')
def __init__(self, model, aoi_model, viz_tile, export_tile, **kwargs): # Define the model and the aoi_model as class attribute so that they can be manipulated in its custom methods self.model = model self.aoi_model = aoi_model # Link to the result tile self.viz_tile = viz_tile self.export_tile = export_tile # widgets self.year = v.Select(label=ms.process.slider, v_model=None, items=pm.years[::-1]) self.filter = v.Select(label=ms.process.filter, v_model=None, items=pm.speckle_filters) self.ls_mask = v.Switch(class_="ml-5", label=ms.process.ls_mask, v_model=True) self.dB = v.Switch(class_="ml-5", label=ms.process.dB, v_model=True) # it also has the embeded `bind` method that link mutable variable to component v_model # bind return self so it can be chained to bind everything in one statement. # args are (widget, model, model_attribute_name) self.model.bind(self.year, "year").bind(self.filter, "filter").bind( self.ls_mask, "ls_mask").bind(self.dB, "dB") # construct the Tile with the widget we have initialized super().__init__( id_= "process_widget", # the id will be used to make the Tile appear and disapear title=ms.process. title, # the Title will be displayed on the top of the tile inputs=[self.year, self.filter, self.ls_mask, self.dB], # self.asset, btn=sw.Btn(ms.process.process), alert=sw.Alert(), ) # now that the Tile is created we can link it to a specific function self.btn.on_event("click", self._on_run)
def switch(v_model=False, label=None, hint=None, persistent_hint=False, class_=None, style_=None, **kwargs): """ Switch input Function to generate an ipyvuetify Switch input widget. Essentially a checkbox. With style. The value of the widget can be accessed or modified by the `v_model` property of the return value. See the vuetify documention for other arguments that can be passed as keyword arguments: https://vuetifyjs.com/en/components/selection-controls/ Parameters: v_model : bool (optional, default None) Value of the time input, must be an element of choices label : str (optional, default False) Default value of the checkbox input hint : str (optional, default None) Hint text persistent_hint : bool (optional, default False) Set to True to display the hint when widget is not focused class_ : str (optional, default None) ipyvuetify HTML class string style_ : str (optional, default None) ipyvuetify HTML CSS string **kwargs Other arguments supported by ipyvuetify.TextField Returns: ipyvuetify.TextInput An ipyvuetify time input widget """ ret = ipyvuetify.Switch( class_=class_, style_=style_, v_model=v_model, label=label, hint=hint, persistent_hint=persistent_hint) # Set other keyword arguments for arg in kwargs: setattr(ret, arg, kwargs[arg]) # Return widget return ret
def __init__(self, model): # create the widgets connectivity = v.Select( label=cm.acc.connectivity, items=cp.connectivity, v_model=cp.connectivity[0]["value"], ) edge_width = v.Slider(label=cm.mspa.edge_width, min=1, max=30, v_model=1, thumb_label=True) transition = v.Switch(label=cm.mspa.transition, false_value=0, true_value=1, v_model=1) int_ext = v.Switch(label=cm.mspa.int_ext, false_value=0, true_value=1, v_model=1) disk = v.Switch(label=cm.mspa.disk, false_value=0, true_value=1, v_model=0) stats = v.Switch(label=cm.mspa.stats, fals_value=0, true_value=1, v_model=0) # bind to the io (model.bind(connectivity, "connectivity").bind( edge_width, "edge_width").bind(transition, "transition").bind( int_ext, "int_ext").bind(disk, "disk").bind(stats, "statistics")) super().__init__( model=model, inputs=[ connectivity, edge_width, transition, int_ext, disk, stats ], )
def __init__(self, name, header, id_, **kwargs): widget = v.Switch( # readonly = True, persistent_hint=True, v_model=True, label=name, **kwargs, ) super().__init__(widget, name=name, header=header, id_=id_)
def run_parametric_study(): from pathos.multiprocessing import ProcessingPool pool = pp.ProcessPool() output = pool.map(run_simulation, args) dimensions = [ dict(values=np.asarray([o[0] for o in output], dtype=np.float64), label='stability') ] dimensions.extend([ dict(values=sampling[:, ik], label=f'{k}') for ik, k in enumerate(design_space.keys()) ]) for i, r in enumerate(self.responses.widget.v_model): if output[0][i + 1] is not None: dimensions.append( dict(values=np.asarray([o[i + 1] for o in output], dtype=np.float64), label=r)) for isamp in range(len(sampling)): tmp_design = { f'{k}': sampling[isamp, ik] for ik, k in enumerate(design_space.keys()) } tmp_responses = { r: output[isamp][ir + 1] for ir, r in enumerate(self.responses.widget.v_model) } tmp_responses['stability'] = output[isamp][0] simu_path = os.path.join(path, f'simu_{isamp}') save_param_study_for_simu(simu_path, 'param_study.json', tmp_design, tmp_responses) fig = v.Row(children=[ go.FigureWidget(data=go.Parcoords( line=dict(color=dimensions[0]['values']), dimensions=dimensions, )), ], align='center', justify='center') def change_plot(change): if only_stable.v_model: mask = dimensions[0]['values'] == 1 else: mask = slice(dimensions[0]['values'].size) new_data = [] for i in items.v_model: d = dimensions[i] new_data.append( dict(values=d['values'][mask], label=d['label'])) fig.children = [ go.FigureWidget( go.Parcoords( line=dict(color=dimensions[color.v_model] ['values'][mask]), dimensions=new_data, )) ] color = v.Select(label='color', items=[{ 'text': v['label'], 'value': i } for i, v in enumerate(dimensions)], v_model=0) items = v.Select(label='Show items', items=[{ 'text': v['label'], 'value': i } for i, v in enumerate(dimensions)], v_model=[i for i in range(len(dimensions))], multiple=True) only_stable = v.Switch(label='Show only stable results', v_model=False) color.observe(change_plot, 'v_model') items.observe(change_plot, 'v_model') only_stable.observe(change_plot, 'v_model') self.plotly_plot.children = [color, items, only_stable, fig] self.stop_simulation(None)
def __init__(self, aoi_model, model, **kwargs): # gather the model self.aoi_model = aoi_model self.model = model # create an output alert self.output = sw.Alert() # self.backscatter = v.Switch(class_="ml-5", label=ms.export.backscatter, v_model=True) self.rfdi = v.Switch(class_="ml-5", label=ms.export.rfdi, v_model=True) self.texture = v.Switch(class_="ml-5", label=ms.export.texture, v_model=False) self.aux = v.Switch(class_="ml-5", label=ms.export.aux, v_model=False) self.fnf = v.Switch(class_="ml-5", label=ms.export.fnf, v_model=False) self.scale = v.TextField(label=ms.export.scale, v_model=25) # create buttons self.asset_btn = sw.Btn(ms.export.asset_btn, "mdi-download", disabled=True, class_="ma-5") self.sepal_btn = sw.Btn(ms.export.sepal_btn, "mdi-download", disabled=True, class_="ma-5") # bindings self.model.bind(self.backscatter, "backscatter").bind( self.rfdi, "rfdi").bind(self.texture, "texture").bind( self.aux, "aux").bind(self.fnf, "fnf").bind(self.scale, "scale") # note that btn and output are not a madatory attributes super().__init__( id_="export_widget", title=ms.export.title, inputs=[ self.backscatter, self.rfdi, self.texture, self.aux, self.fnf, self.scale, ], alert=sw.Alert(), btn=v.Layout(row=True, children=[self.asset_btn, self.sepal_btn]), ) # decorate each function as we are using multiple btns self._on_asset_click = su.loading_button(self.alert, self.asset_btn, debug=False)( self._on_asset_click) self._on_sepal_click = su.loading_button(self.alert, self.sepal_btn, debug=False)( self._on_sepal_click) # link the btn self.asset_btn.on_event("click", self._on_asset_click) self.sepal_btn.on_event("click", self._on_sepal_click)
def __init__(self): # create the different widgets # I will not use Io as the information doesn't need to be communicated to any other tile self.folder = cw.FolderSelect() self.out_dir = cw.OutDirSelect() self.tiles = cw.TilesSelect() self.poly = v.Select(label=cm.widget.harmonic.label, v_model=3, items=[i for i in range(3, 11)]) self.freq = v.Slider(label=cm.widget.freq.label, v_model=365, min=1, max=365, thumb_label="always", class_='mt-5') self.trend = v.Switch(v_model=False, label=cm.widget.trend.label) self.hfrac = v.Select(label=cm.widget.hfrac.label, v_model=.25, items=[.25, .5, 1.]) self.level = v.Slider(label=cm.widget.level.label, v_model=.95, step=.001, min=.95, max=1, thumb_label="always", class_='mt-5') self.backend = cw.BackendSelect() self.monitoring = cw.DateRangeSlider(label=cm.widget.monitoring.label) self.history = cw.DateSlider(label=cm.widget.history.label) # stack the advance parameters in a expandpanel advance_params = v.ExpansionPanels( class_='mb-5', popout=True, children=[ v.ExpansionPanel(children=[ v.ExpansionPanelHeader( children=[cm.widget.advance_params]), v.ExpansionPanelContent(children=[ v.Flex(xs12=True, children=[self.hfrac]), v.Flex(xs12=True, children=[self.level]), v.Flex(xs12=True, children=[self.backend]) ]) ]) ]) # create the tile super().__init__( "bfast_tile", cm.bfast. folder, # the title is used to describe the first section inputs=[ self.folder, self.out_dir, self.tiles, v.Html(tag="h2", children=[cm.bfast.process]), self.poly, self.freq, self.trend, advance_params, v.Html(tag="h2", children=[cm.bfast.periods]), self.history, self.monitoring ], output=cw.CustomAlert(), btn=sw.Btn(cm.bfast.btn)) # add js behaviour self.folder.observe(self._on_folder_change, 'v_model') self.btn.on_event('click', self._start_process) self.monitoring.observe(self._check_periods, 'v_model') self.history.observe(self._check_periods, 'v_model')
def __init__(self, test_case_widget, lb_scheme_widget, discret_widget, codegen_widget): """ Widget definition for parametric study of lattice Boltzmann methods. Parameters ========== - test_case_widget: widget of the test case (see test_case.py). - lb_scheme_widget: widget of the lattice Boltzmann scheme (see lb_scheme.py). This widget is composed by a menu where you can define the design space of the parametric study, the responses computed on each sample and the method used to generate the sampling as well as the number of points. This widget is also composed by a main widget where the result of the parametric study is represented by a parallel coordinates plot using plotly. """ self.test_case_widget = test_case_widget self.lb_scheme_widget = lb_scheme_widget self.discret_widget = discret_widget self.codegen = codegen_widget self.tmp_dir = tempfile.TemporaryDirectory() self.results = [] ## ## The menu ## self.study_name = v.TextField(label='Study name', v_model='PS_0') self.param_cfg = v.Select(label='Load parametric study', items=[], v_model=None) self.update_param_cfg_list() self.design = DesignWidget(test_case_widget, lb_scheme_widget, discret_widget) self.responses = ResponsesWidget(test_case_widget, lb_scheme_widget) self.sampling_method = v.Select(label='Method', items=list(skopt_method.keys()), v_model=list(skopt_method.keys())[0]) self.sample_size = NbPointsField(label='Number of samples', v_model=10) self.run = v.Btn(v_model=True, children=['Run parametric study'], class_="ma-5", color='success') self.menu = [ self.study_name, self.param_cfg, v.ExpansionPanels(children=[ v.ExpansionPanel(children=[ v.ExpansionPanelHeader(children=['Design space']), v.ExpansionPanelContent(children=[self.design.widget]), ]), v.ExpansionPanel(children=[ v.ExpansionPanelHeader(children=['Responses']), v.ExpansionPanelContent(children=[self.responses.widget]), ]), v.ExpansionPanel(children=[ v.ExpansionPanelHeader(children=['Sampling method']), v.ExpansionPanelContent( children=[self.sampling_method, self.sample_size]), ]), ], multiple=True), ] ## ## The main ## self.color = v.Select(label='color', v_model=0) self.items = v.Select(label='Show items', multiple=True) self.only_stable = v.Switch(label='Show only stable results', v_model=False) self.fig = v.Row(children=[], align='center', justify='center') self.plotly_plot = v.Container(align_content_center=True) self.dialog = DialogPath() self.main = [ v.Row(children=[self.run], align='center', justify='center'), v.Row(children=[self.plotly_plot]), self.dialog ] ## ## Widget events ## self.run.on_event('click', self.start_PS) self.test_case_widget.select_case.observe(self.purge, 'v_model') self.lb_scheme_widget.select_case.observe(self.purge, 'v_model') self.param_cfg.observe(self.load_param_cfg, 'v_model') self.color.observe(self.change_plot, 'v_model') self.items.observe(self.change_plot, 'v_model') self.only_stable.observe(self.change_plot, 'v_model')
children=[ v.Select(label='Fruits', items=['Apple', 'Pear', 'Cherry'], v_model='Pear') ]), v.Col(cols=4, children=[ v.Select(label='Fruits', items=['Apple', 'Pear', 'Cherry'], chips=True, multiple=True, v_model=['Pear', 'Cherry']) ]), v.Col(cols=4, children=[ v.Select(label='Fruits', items=['Apple', 'Pear', 'Cherry'], outlined=True) ]), ]), v.Row(children=[ v.Col(cols=4, children=[v.Slider()]), v.Col(cols=4, children=[v.Slider(thumb_label='always')]), v.Col(cols=4, children=[v.RangeSlider(value=[20, 80])]), ]), v.Row(children=[ v.Col(cols=4, children=[v.Switch(label='switch', margin_top='0')]), v.Col(cols=4, children=[menu]), v.Col(cols=4, children=[dialog]), ]), ])
def __init__(self): self.select_dim = v.Select(label='Dimension', items=[1, 2], v_model=1) self.previous_dim = 1 headers = [ { 'text': 'Id', 'value': 'id' }, { 'text': 'Iteration', 'value': 'iteration' }, { 'text': 'Time', 'value': 'time' }, { 'text': 'Field', 'value': 'field' }, { 'text': 'Model', 'value': 'model' }, { 'text': 'Test case', 'value': 'test case' }, { 'text': 'LB scheme', 'value': 'lb scheme' }, { 'text': 'Filename', 'value': 'file' }, { 'text': 'Directory', 'value': 'directory' }, ] headers_select = v.Select(label='Show columns', items=[{ 'text': v['text'], 'value': i + 1 } for i, v in enumerate(headers[1:])], v_model=list(range(1, 7)), multiple=True) search = v.TextField( v_model=None, append_icon="mdi-magnify", label="Search", single_line=True, hide_details=True, ) self.table = v.DataTable( v_model=[], headers=[headers[i] for i in headers_select.v_model], items=[], item_key="id", single_select=False, show_select=True, search='', ) self.selected_cache = [[]] * 3 self.select_item_cache = [[]] * 3 self.properties_cache = [[]] * 3 self.select_table = SelectedDataTable( self.table, headers=[headers[i] for i in headers_select.v_model] + [{ 'text': 'Action', 'value': 'action' }], ) self.plot = Plot() def select(change): with out: for e in list(self.table.v_model): self.select_table.items.append(e) if e['dim'] == 1: self.select_table.properties.append({ 'label': e['field'], 'color': plot_config['colors'][0], 'alpha': plot_config['alpha'], 'linewidth': plot_config['linewidth'], 'linestyle': plot_config['linestyle'], 'marker': plot_config['marker'], 'markersize': plot_config['markersize'] }) elif e['dim'] == 2: h5 = os.path.join(e['directory'], e['file']) h5_data = h5py.File(h5) data = h5_data[e['field']][:] self.select_table.properties.append({ 'label': e['field'], 'min_value': np.nanmin(data), 'max_value': np.nanmax(data), 'cmap': plt.colormaps().index(plot_config['cmap']) }) self.table.items.remove(e) self.table.v_model = [] self.select_table.notify_change({ 'name': 'items', 'type': 'change' }) self.table.notify_change({'name': 'items', 'type': 'change'}) def search_text(change): self.table.search = search.v_model # self.update(None) self.select_table.observe(self.plot_result, 'items') self.select_table.observe(self.plot_result, 'properties') self.select_table.observe(self.plot_result, 'selected') search.observe(search_text, 'v_model') self.table.observe(select, 'v_model') self.select_dim.observe(self.cache, 'v_model') def update_headers(change): with out: self.table.headers = [ headers[i] for i in headers_select.v_model ] self.select_table.headers = [ headers[i] for i in headers_select.v_model ] + [{ 'text': 'Action', 'value': 'action' }] self.select_table.notify_change({ 'name': 'items', 'type': 'change' }) self.table.notify_change({'name': 'items', 'type': 'change'}) headers_select.observe(update_headers, 'v_model') download_zip = v.Btn(children=['Download results']) def create_zip(widget, event, data): from zipfile import ZipFile zipfilename = os.path.join(voila_notebook, 'results.zip') with ZipFile(zipfilename, 'w') as zipObj: for folderName, subfolders, filenames in os.walk(default_path): for filename in filenames: #create complete filepath of file in directory filePath = os.path.join(folderName, filename) # Add file to zip zipObj.write(filePath, filePath.replace(default_path, '')) dialog.children = [ v.Card(children=[ v.CardTitle(children=[ widgets.HTML( f'<a href="./results.zip" download="results.zip">Download the archive</a>' ) ]) ]) ] dialog.v_model = True self.title = v.TextField(label='Plot title', v_model='') self.xlabel = v.TextField(label='x label', v_model='') self.ylabel = v.TextField(label='y label', v_model='') self.legend = v.Switch(label='Add legend', v_model=False) self.title.observe(self.set_plot_properties, 'v_model') self.xlabel.observe(self.set_plot_properties, 'v_model') self.ylabel.observe(self.set_plot_properties, 'v_model') self.legend.observe(self.set_plot_properties, 'v_model') dialog = v.Dialog() dialog.v_model = False dialog.width = '200' download_zip.on_event('click', create_zip) self.menu = [ self.select_dim, headers_select, self.title, self.xlabel, self.ylabel, self.legend, download_zip, dialog ] self.main = [ v.Card( children=[ v.CardTitle( children=['Available results', v.Spacer(), search]), self.table, ], class_='ma-2', ), v.Card( children=[ v.CardTitle(children=[ 'Selected results', ]), self.select_table, self.select_table.dialog, ], class_='ma-2', ), v.Row(children=[self.plot.fig.canvas], justify='center'), ]