mg-pso-gui 0.1.19__py3-none-any.whl → 0.1.21__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mg_pso_gui-0.1.19.dist-info → mg_pso_gui-0.1.21.dist-info}/METADATA +1 -1
- {mg_pso_gui-0.1.19.dist-info → mg_pso_gui-0.1.21.dist-info}/RECORD +8 -8
- mgpsogui/gui/HomePage.py +50 -50
- mgpsogui/gui/RunTab/RunTab.py +1 -20
- mgpsogui/util/GraphGenerator.py +28 -0
- {mg_pso_gui-0.1.19.dist-info → mg_pso_gui-0.1.21.dist-info}/WHEEL +0 -0
- {mg_pso_gui-0.1.19.dist-info → mg_pso_gui-0.1.21.dist-info}/entry_points.txt +0 -0
- {mg_pso_gui-0.1.19.dist-info → mg_pso_gui-0.1.21.dist-info}/top_level.txt +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
mgpsogui/__init__.py,sha256=q7AfBjeJABnFtbsZnsObpUwaXKPDVYtz46G6MKXLF74,42
|
|
2
2
|
mgpsogui/mgpsogui.py,sha256=NIZmyNcbwC8EgSwf1ubdMUSJscrIEgoD4jLYziqHQ-k,148
|
|
3
3
|
mgpsogui/start.yaml,sha256=ZjCVLb-MLqAxrGRm9kA7_SDpa-45EuKIELNQ2QqCAiU,4713
|
|
4
|
-
mgpsogui/gui/HomePage.py,sha256=
|
|
4
|
+
mgpsogui/gui/HomePage.py,sha256=1WTBEhe6KRujPgNb-NwRhFGDbAmP2Zayx3Pp6FHHwD0,22313
|
|
5
5
|
mgpsogui/gui/OptionManager.py,sha256=bFN2jWJbx1Din3waOFLmbWGt0BXD_KthGJ8Mgue_FEE,11712
|
|
6
6
|
mgpsogui/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
7
|
mgpsogui/gui/PlatformTab/PlatformTab.py,sha256=KyIoHlMKD9nfQqHeGJwRDA4RCoe4UykjB2l6xMTKK_M,10486
|
|
8
8
|
mgpsogui/gui/PlatformTab/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
mgpsogui/gui/RunTab/RunTab.py,sha256=
|
|
9
|
+
mgpsogui/gui/RunTab/RunTab.py,sha256=GPKhwCZh6FXhmRypPl0zo7XByM2AsBqeY3gxctANyP8,1839
|
|
10
10
|
mgpsogui/gui/RunTab/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
11
|
mgpsogui/gui/SetupTab/BoundsEditorWindow.py,sha256=_6_xEl46PY9MmNvpmoMNdNDbVav6ODwWSDROboy57Cs,2866
|
|
12
12
|
mgpsogui/gui/SetupTab/BoundsList.py,sha256=L77UEaUJNiUE-ZsYIl44tWRb1zBfxaEQ-bAG1V5_IH0,12268
|
|
@@ -30,7 +30,7 @@ mgpsogui/gui/images/stop.png,sha256=JPuxXQerCGpLikcp7cAj3iLCOjULMYYZ2sZe0lArh68,
|
|
|
30
30
|
mgpsogui/gui/images/test.png,sha256=MUnVpRK-isxhEHzx4Q6Yh0M6FRZD1qvgCHH2XmiSBbk,3642
|
|
31
31
|
mgpsogui/gui/images/trash.png,sha256=j8cf0kWbJd-4Jp20lUVV1o1NSeQ4v1Ej4gfcIA3DVRQ,2958
|
|
32
32
|
mgpsogui/gui/images/up.png,sha256=AQvFWCUqSQNaQ1E6LKZ9zNfSvW6t4mgy8uswdg9T2Hg,2457
|
|
33
|
-
mgpsogui/util/GraphGenerator.py,sha256=
|
|
33
|
+
mgpsogui/util/GraphGenerator.py,sha256=DDP8bqdfGxPPbvTjyYYW5GiE5AV6Z_KfVbeqRCoWrz0,9266
|
|
34
34
|
mgpsogui/util/PSORunner.py,sha256=wfkrZLysgSS_5BrdFEm8U-0S7DHYpxmHPau_Ec5W8ak,4510
|
|
35
35
|
mgpsogui/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
36
|
mgpsogui/util/CTkToolTip/__init__.py,sha256=G1jxV55hGtGgwyC1sR-uUUdasDdh0XZgcI-aILgGYA0,225
|
|
@@ -44,8 +44,8 @@ mgpsogui/util/recosu/utils/trace_writer.py,sha256=V9BJlOjCbNYGoXGEk3CF5wjifBxvar
|
|
|
44
44
|
mgpsogui/util/recosu/utils/utils.py,sha256=QB8vftq3142ekG0ORjz0ZBHU5YknXbR0oTsrxrPAsF0,3951
|
|
45
45
|
mgpsogui/util/recosu/utils/plot/__init__.py,sha256=h1KjM7_tNDv351pcwt8A6Ibb1jhwWyx5Gbu-zj-sI3Q,71
|
|
46
46
|
mgpsogui/util/recosu/utils/plot/cost_steps.py,sha256=1Ce11AJyweWkmvjXPxEygzS-h8yVLmQEDLS53yjPLqQ,3779
|
|
47
|
-
mg_pso_gui-0.1.
|
|
48
|
-
mg_pso_gui-0.1.
|
|
49
|
-
mg_pso_gui-0.1.
|
|
50
|
-
mg_pso_gui-0.1.
|
|
51
|
-
mg_pso_gui-0.1.
|
|
47
|
+
mg_pso_gui-0.1.21.dist-info/METADATA,sha256=yTHYuuaKMSXJBLT20CXrqxbDOjxdIRAO6mdUbCEh1cU,9458
|
|
48
|
+
mg_pso_gui-0.1.21.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92
|
|
49
|
+
mg_pso_gui-0.1.21.dist-info/entry_points.txt,sha256=jg82VOFjR1XDGrchs1wJSCqKYE4Ozv12aBcCSp--koA,117
|
|
50
|
+
mg_pso_gui-0.1.21.dist-info/top_level.txt,sha256=y7JuS9xJN5YdxUsQ3PSVjN8MzQAnR146bP3ZN3PYWdE,9
|
|
51
|
+
mg_pso_gui-0.1.21.dist-info/RECORD,,
|
mgpsogui/gui/HomePage.py
CHANGED
|
@@ -70,8 +70,11 @@ class App(customtkinter.CTk):
|
|
|
70
70
|
self.data_x = [0]
|
|
71
71
|
self.data_y = [0]
|
|
72
72
|
|
|
73
|
+
self.image_scale = 1
|
|
73
74
|
self.image_width = 1280
|
|
74
75
|
self.image_height = 720
|
|
76
|
+
self.progress_data = None
|
|
77
|
+
self.calibration_data = None
|
|
75
78
|
|
|
76
79
|
# configure window
|
|
77
80
|
self.title("CSIP PSO")
|
|
@@ -172,7 +175,7 @@ class App(customtkinter.CTk):
|
|
|
172
175
|
ctt(self.download_button, delay=0.1, alpha=0.95, message="Download Results")
|
|
173
176
|
self.download_button.grid(row=0, column=10, padx=(5, 20), pady=header_padding_y)
|
|
174
177
|
|
|
175
|
-
def _resize_image(self, event):
|
|
178
|
+
'''def _resize_image(self, event):
|
|
176
179
|
self.graph_label.update_idletasks()
|
|
177
180
|
new_width = self.graph_label.winfo_width()
|
|
178
181
|
new_height = self.graph_label.winfo_height()
|
|
@@ -190,10 +193,7 @@ class App(customtkinter.CTk):
|
|
|
190
193
|
|
|
191
194
|
self.graph_image = customtkinter.CTkImage(self.graph_image_obj, size=(new_width, new_height))
|
|
192
195
|
self.graph_label.configure(image=self.graph_image)
|
|
193
|
-
self.graph_label.update_idletasks()
|
|
194
|
-
|
|
195
|
-
def callback_test(self, *args):
|
|
196
|
-
print("callback_test called")
|
|
196
|
+
self.graph_label.update_idletasks()'''
|
|
197
197
|
|
|
198
198
|
def update_graph(self, value):
|
|
199
199
|
selected_graph = self.graph_selector_value.get()
|
|
@@ -205,39 +205,30 @@ class App(customtkinter.CTk):
|
|
|
205
205
|
|
|
206
206
|
if (selected_graph == "Best Cost Stacked"):
|
|
207
207
|
self.selected_graph_name = "best_cost_stacked"
|
|
208
|
-
image_path = os.path.join(folder, self.selected_graph_name + ".png")
|
|
209
|
-
if not os.path.exists(image_path):
|
|
210
|
-
image_path = os.path.join("./images", "up.png")
|
|
211
|
-
self.graph_image_obj = Image.open(image_path)
|
|
212
|
-
self.graph_image = customtkinter.CTkImage(self.graph_image_obj, size=(self.image_width, self.image_height))
|
|
213
|
-
self.graph_label.configure(image=self.graph_image)
|
|
214
208
|
elif (selected_graph == "Best Cost by Round"):
|
|
215
209
|
self.selected_graph_name = "best_cost_by_round"
|
|
216
|
-
image_path = os.path.join(folder, self.selected_graph_name + ".png")
|
|
217
|
-
if not os.path.exists(image_path):
|
|
218
|
-
image_path = os.path.join("./images", "up.png")
|
|
219
|
-
self.graph_image_obj = Image.open(image_path)
|
|
220
|
-
self.graph_image = customtkinter.CTkImage(self.graph_image_obj, size=(self.image_width, self.image_height))
|
|
221
|
-
self.graph_label.configure(image=self.graph_image)
|
|
222
210
|
elif (selected_graph == "Iteration Table"):
|
|
223
211
|
self.selected_graph_name = "table"
|
|
224
|
-
image_path = os.path.join(folder, self.selected_graph_name + ".png")
|
|
225
|
-
if not os.path.exists(image_path):
|
|
226
|
-
image_path = os.path.join("./images", "up.png")
|
|
227
|
-
self.graph_image_obj = Image.open(image_path)
|
|
228
|
-
self.graph_image = customtkinter.CTkImage(self.graph_image_obj, size=(self.image_width, self.image_height))
|
|
229
|
-
self.graph_label.configure(image=self.graph_image)
|
|
230
212
|
elif (selected_graph == "Calibrated Parameters"):
|
|
231
213
|
self.selected_graph_name = "calibrated_params_by_round"
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
214
|
+
|
|
215
|
+
image_path = os.path.join(folder, self.selected_graph_name + ".png")
|
|
216
|
+
if not os.path.exists(image_path):
|
|
217
|
+
image_path = os.path.join("./images", "up.png")
|
|
218
|
+
self.graph_image_obj = Image.open(image_path)
|
|
219
|
+
self.graph_image = customtkinter.CTkImage(self.graph_image_obj, size=(self.image_width * self.image_scale, self.image_height * self.image_scale))
|
|
220
|
+
self.graph_label.configure(image=self.graph_image)
|
|
238
221
|
|
|
239
222
|
def save_project(self):
|
|
240
223
|
metrics = self.option_manager.get_metrics()
|
|
224
|
+
|
|
225
|
+
# Add the calibration_progress dataframe to the json
|
|
226
|
+
if (self.progress_data is not None):
|
|
227
|
+
metrics["calibration_progress"] = self.progress_data.to_json()
|
|
228
|
+
# Add the calibration_data to the json
|
|
229
|
+
if (self.calibration_data is not None):
|
|
230
|
+
metrics["calibration_data"] = self.calibration_data
|
|
231
|
+
|
|
241
232
|
filename = asksaveasfilename(filetypes=[("JSON", "*.json")], initialfile="config", defaultextension="json", title="Save Project")
|
|
242
233
|
|
|
243
234
|
try:
|
|
@@ -276,6 +267,15 @@ class App(customtkinter.CTk):
|
|
|
276
267
|
if "service_parameters" in metrics:
|
|
277
268
|
self.option_manager.set_service_parameters(metrics["service_parameters"])
|
|
278
269
|
self.tabview.configure(state="enabled")
|
|
270
|
+
|
|
271
|
+
if "calibration_progress" in metrics:
|
|
272
|
+
self.progress_data = pd.read_json(metrics["calibration_progress"])
|
|
273
|
+
self.calibration_data = metrics["calibration_data"]
|
|
274
|
+
print(self.progress_data)
|
|
275
|
+
|
|
276
|
+
if "calibration_data" in metrics:
|
|
277
|
+
self.calibration_data = metrics["calibration_data"]
|
|
278
|
+
print(self.calibration_data)
|
|
279
279
|
|
|
280
280
|
print(metrics)
|
|
281
281
|
|
|
@@ -349,6 +349,9 @@ class App(customtkinter.CTk):
|
|
|
349
349
|
metrics = self.option_manager.get_metrics()
|
|
350
350
|
self.running_config = metrics
|
|
351
351
|
|
|
352
|
+
self.progress_data = None
|
|
353
|
+
self.calibration_data = None
|
|
354
|
+
|
|
352
355
|
#self.progress_bar.configure(mode="indeterminnate")
|
|
353
356
|
#self.progress_bar.start()
|
|
354
357
|
self.footer_progress_bar.configure(mode="indeterminnate")
|
|
@@ -366,6 +369,15 @@ class App(customtkinter.CTk):
|
|
|
366
369
|
try:
|
|
367
370
|
info = self.option_manager.get_project_data()
|
|
368
371
|
folder = os.path.join(info['path'], info['name'])
|
|
372
|
+
|
|
373
|
+
if not os.path.exists(folder):
|
|
374
|
+
os.makedirs(folder)
|
|
375
|
+
|
|
376
|
+
if (os.path.exists(os.path.join(folder, 'output.txt'))):
|
|
377
|
+
os.remove(os.path.join(folder, 'output.txt'))
|
|
378
|
+
|
|
379
|
+
if (os.path.exists(os.path.join(folder, 'error.txt'))):
|
|
380
|
+
os.remove(os.path.join(folder, 'error.txt'))
|
|
369
381
|
|
|
370
382
|
self.train_process = Process(target=PSORunner.run_process, args=(stdout_queue, stderr_queue, results_queue, cosu_queue, metrics, folder))
|
|
371
383
|
self.train_process.daemon = True
|
|
@@ -422,7 +434,6 @@ class App(customtkinter.CTk):
|
|
|
422
434
|
|
|
423
435
|
def watch_loop(self):
|
|
424
436
|
# Check if file exists:
|
|
425
|
-
|
|
426
437
|
info = self.option_manager.get_project_data()
|
|
427
438
|
folder = os.path.join(info['path'], info['name'])
|
|
428
439
|
|
|
@@ -493,10 +504,12 @@ class App(customtkinter.CTk):
|
|
|
493
504
|
best_particle_values = [ast.literal_eval(i) for i in best_particle_values]
|
|
494
505
|
progress_values = [tuple(map(float, i)) for i in progress_values]
|
|
495
506
|
|
|
496
|
-
print("Current params: " + str(calibrated_params), flush=True)
|
|
507
|
+
#print("Current params: " + str(calibrated_params), flush=True)
|
|
508
|
+
|
|
509
|
+
self.calibration_data = calibrated_params
|
|
497
510
|
|
|
498
|
-
GraphGenerator.calibrated_params_by_round(self.running_config['steps'], calibrated_params, self.option_manager)
|
|
499
|
-
self.update_graph("")
|
|
511
|
+
#GraphGenerator.calibrated_params_by_round(self.running_config['steps'], calibrated_params, self.option_manager)
|
|
512
|
+
#self.update_graph("")
|
|
500
513
|
except Exception as e:
|
|
501
514
|
# Print stack trace
|
|
502
515
|
traceback.print_exc()
|
|
@@ -521,11 +534,7 @@ class App(customtkinter.CTk):
|
|
|
521
534
|
df = df.drop_duplicates()
|
|
522
535
|
df['round_step'] = (df['completed_rounds'].diff() < 0).cumsum()
|
|
523
536
|
df = df.drop_duplicates(subset=['completed_rounds', 'round_step'])
|
|
524
|
-
|
|
525
|
-
GraphGenerator.best_cost_stacked(self.running_config['steps'], df, self.option_manager)
|
|
526
|
-
GraphGenerator.best_cost_by_round(self.running_config['steps'], df, self.option_manager)
|
|
527
|
-
GraphGenerator.table(self.running_config['steps'], df, self.option_manager)
|
|
528
|
-
self.update_graph("")
|
|
537
|
+
self.progress_data = df
|
|
529
538
|
|
|
530
539
|
match = matches[-1]
|
|
531
540
|
percent = int(match[0])
|
|
@@ -559,18 +568,6 @@ class App(customtkinter.CTk):
|
|
|
559
568
|
if self.train_process.is_alive():
|
|
560
569
|
self.after(1000, self.watch_loop)
|
|
561
570
|
else:
|
|
562
|
-
# Check if queue is empty
|
|
563
|
-
"""
|
|
564
|
-
if (self.process_queue.empty()):
|
|
565
|
-
print("PROCESS FINISHED BUT WE GOT NO OBJECTS :(")
|
|
566
|
-
self.footer_progress_label.configure(text="Failed")
|
|
567
|
-
else:
|
|
568
|
-
self.final_results = self.process_queue.get()
|
|
569
|
-
print("PROCESS FINISHED WE GOT THINGS")
|
|
570
|
-
print(self.final_results)
|
|
571
|
-
self.footer_progress_label.configure(text="Finished")
|
|
572
|
-
"""
|
|
573
|
-
|
|
574
571
|
#self.progress_bar.stop()
|
|
575
572
|
self.footer_progress_bar.stop()
|
|
576
573
|
#self.progress_bar.configure(mode="indeterminate")
|
|
@@ -582,6 +579,9 @@ class App(customtkinter.CTk):
|
|
|
582
579
|
self.progress_message_right.configure(text="")
|
|
583
580
|
self.textbox.insert("0.0", "\nCalibration finished!\n")
|
|
584
581
|
|
|
582
|
+
GraphGenerator.generate_graphs(self)
|
|
583
|
+
|
|
584
|
+
|
|
585
585
|
def start():
|
|
586
586
|
app = App()
|
|
587
587
|
app.mainloop()
|
mgpsogui/gui/RunTab/RunTab.py
CHANGED
|
@@ -19,21 +19,7 @@ def create_tab(self, tab):
|
|
|
19
19
|
tab.grid_columnconfigure(0, weight=1)
|
|
20
20
|
tab.grid_rowconfigure(0, weight=1)
|
|
21
21
|
tab.grid_rowconfigure(1, weight=200)
|
|
22
|
-
|
|
23
|
-
"""
|
|
24
|
-
self.url = customtkinter.CTkEntry(tab, textvariable=self.option_manager.get_arguments()['url'])
|
|
25
|
-
self.url.grid(row=0, column=0, columnspan=1, padx=(20, 20), pady=(20, 20), sticky="nsew")
|
|
26
22
|
|
|
27
|
-
self.run_button = customtkinter.CTkButton(tab, text="Run", command=self.run)
|
|
28
|
-
self.run_button.grid(row=0, column=1, padx=(20, 20), pady=(20, 20), sticky="nsew")
|
|
29
|
-
ctt(self.run_button, delay=0.5, message="Start calibration...")
|
|
30
|
-
|
|
31
|
-
self.stop_button = customtkinter.CTkButton(tab, text="Stop", command=self.stop)
|
|
32
|
-
self.stop_button.grid(row=0, column=2, padx=(20, 20), pady=(20, 20), sticky="nsew")
|
|
33
|
-
ctt(self.stop_button, delay=0.5, message="Stop calibration...")
|
|
34
|
-
|
|
35
|
-
"""
|
|
36
|
-
|
|
37
23
|
self.progress_container = customtkinter.CTkFrame(tab)
|
|
38
24
|
self.progress_container.grid_columnconfigure(0, weight=1)
|
|
39
25
|
self.progress_container.grid_columnconfigure(1, weight=1)
|
|
@@ -50,12 +36,7 @@ def create_tab(self, tab):
|
|
|
50
36
|
self.progress_message_right = customtkinter.CTkLabel(self.progress_container, text="")
|
|
51
37
|
self.progress_message_right.grid(row=0, column=2, padx=(10, 10), pady=(10, 10), sticky="e")
|
|
52
38
|
|
|
53
|
-
#self.progress_bar = customtkinter.CTkProgressBar(self.progress_container)
|
|
54
|
-
#self.progress_bar.grid(row=1, column=0, columnspan=3, padx=(10, 10), pady=(10, 10), sticky="ew")
|
|
55
|
-
#self.progress_bar.set(0)
|
|
56
|
-
#ctt(self.progress_bar, delay=0.5, message="Current calibration progress")
|
|
57
|
-
|
|
58
39
|
self.textbox = customtkinter.CTkTextbox(tab)
|
|
59
40
|
self.textbox.grid(row=1, column=0, padx=(20, 20), pady=(20, 20), sticky="nsew")
|
|
60
|
-
self.textbox.insert("0.0", "Welcome to the CSIP PSO Calibration Tool!\n\nUse the
|
|
41
|
+
self.textbox.insert("0.0", "Welcome to the CSIP PSO Calibration Tool!\n\nUse the Setup tab to define steps and calibration parameters. Use this tab to view logs and observe calibration progress. Once finished, use the Results tab to generate figures and graphs.")
|
|
61
42
|
|
mgpsogui/util/GraphGenerator.py
CHANGED
|
@@ -3,6 +3,34 @@ import plotly.graph_objs as go
|
|
|
3
3
|
import pandas as pd
|
|
4
4
|
import numpy as np
|
|
5
5
|
import os
|
|
6
|
+
from PIL import Image, ImageTk
|
|
7
|
+
|
|
8
|
+
def generate_graphs(HomePage):
|
|
9
|
+
selected_graph = HomePage.graph_selector_value.get()
|
|
10
|
+
info = HomePage.option_manager.get_project_data()
|
|
11
|
+
folder = os.path.join(info['path'], info['name'])
|
|
12
|
+
if not os.path.exists(folder):
|
|
13
|
+
os.makedirs(folder)
|
|
14
|
+
|
|
15
|
+
if (selected_graph == "Best Cost Stacked"):
|
|
16
|
+
HomePage.selected_graph_name = "best_cost_stacked"
|
|
17
|
+
best_cost_stacked(HomePage.running_config['steps'], HomePage.progress_data, HomePage.option_manager)
|
|
18
|
+
elif (selected_graph == "Best Cost by Round"):
|
|
19
|
+
HomePage.selected_graph_name = "best_cost_by_round"
|
|
20
|
+
best_cost_by_round(HomePage.running_config['steps'], HomePage.progress_data, HomePage.option_manager)
|
|
21
|
+
elif (selected_graph == "Iteration Table"):
|
|
22
|
+
HomePage.selected_graph_name = "table"
|
|
23
|
+
table(HomePage.running_config['steps'], HomePage.progress_data, HomePage.option_manager)
|
|
24
|
+
elif (selected_graph == "Calibrated Parameters"):
|
|
25
|
+
HomePage.selected_graph_name = "calibrated_params_by_round"
|
|
26
|
+
calibrated_params_by_round(HomePage.running_config['steps'], HomePage.calibration_data, HomePage.option_manager)
|
|
27
|
+
|
|
28
|
+
image_path = os.path.join(folder, HomePage.selected_graph_name + ".png")
|
|
29
|
+
|
|
30
|
+
if not os.path.exists(image_path):
|
|
31
|
+
image_path = os.path.join("./images", "up.png")
|
|
32
|
+
|
|
33
|
+
HomePage.graph_image_obj = Image.open(image_path)
|
|
6
34
|
|
|
7
35
|
def best_cost_stacked(config, dataframe, option_manager):
|
|
8
36
|
fig = go.Figure()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|