agi-page-simplex-map 0.1.0__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.
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: agi-page-simplex-map
3
+ Version: 0.1.0
4
+ Summary: AGILAB page bundle for simplex projection and barycentric analysis.
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: agi-gui<2027.0,>=2026.05.12.post3
8
+ Requires-Dist: plotly>=6.3.0
9
+ Requires-Dist: barviz>=1.2.2
10
+ Requires-Dist: scikit-learn>=1.7.2
11
+ Requires-Dist: scipy<2,>=1.16
12
+ Requires-Dist: sqlalchemy>=2.0.43
13
+
14
+ AGILAB page bundle for simplex projection and barycentric analysis.
@@ -0,0 +1,8 @@
1
+ view_barycentric/__init__.py,sha256=02AAE68o8jE8b4DfJz9dqATWut-uRbKMG6-DdD1g10c,172
2
+ view_barycentric/barycentric_graph.py,sha256=cHrjvnEUR7y_jgR5c2uk9mVi--bpNrO_DsSghhTnOqk,433
3
+ view_barycentric/view_barycentric.py,sha256=qr3fseF6HxrtanuNUGXLateEy4zgl8QM2AY8mBpiDcM,25617
4
+ agi_page_simplex_map-0.1.0.dist-info/METADATA,sha256=IsmVezx8dcYHht3UTzpQbbZpKYHoJRnR9uRl5QUrDlM,480
5
+ agi_page_simplex_map-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
6
+ agi_page_simplex_map-0.1.0.dist-info/entry_points.txt,sha256=WJUjm_-E-08ExrBKP2IBhcxLdP7wvabQQiM0sTeBh44,63
7
+ agi_page_simplex_map-0.1.0.dist-info/top_level.txt,sha256=0ubRMVGOn-SJFCDQykuuyd6JQ4-Sm5yIKPc3leo5KHU,17
8
+ agi_page_simplex_map-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [agilab.pages]
2
+ view_barycentric = view_barycentric:bundle_root
@@ -0,0 +1 @@
1
+ view_barycentric
@@ -0,0 +1,7 @@
1
+ from pathlib import Path
2
+
3
+
4
+ def bundle_root() -> Path:
5
+ """Return the installed root for this AGILAB analysis page bundle."""
6
+
7
+ return Path(__file__).resolve().parent
@@ -0,0 +1,14 @@
1
+ """Support module for the barycentric Streamlit page."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ try:
9
+ from .view_barycentric import * # type: ignore # noqa: F401,F403
10
+ except ImportError: # pragma: no cover
11
+ _HERE = Path(__file__).resolve().parent
12
+ if str(_HERE) not in sys.path:
13
+ sys.path.insert(0, str(_HERE))
14
+ from view_barycentric import * # type: ignore # noqa: F401,F403
@@ -0,0 +1,689 @@
1
+ # SPDX-License-Identifier: BSD-3-Clause AND MIT
2
+ #
3
+ # Portions of this file are adapted from “barviz / barviz-mod”
4
+ # Copyright (c) 2022 Jean-Luc Parouty
5
+ # Licensed under the MIT License (see LICENSES/LICENSE-MIT-barviz-mod)
6
+ #
7
+ # Additional modifications:
8
+ # Copyright (c) 2025, Jean-Pierre Morard, THALES SIX GTS FRANCE SAS
9
+ # Licensed under the BSD 3-Clause License (see LICENSE)
10
+ #
11
+ # BSD 3-Clause License
12
+ #
13
+ # Copyright (c) 2025, Jean-Pierre Morard, THALES SIX GTS FRANCE SAS
14
+ # All rights reserved.
15
+ #
16
+ # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
17
+ #
18
+ # 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
19
+ # 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
20
+ # 3. Neither the name of Jean-Pierre Morard nor the names of its contributors, or THALES SIX GTS FRANCE SAS, may be used to endorse or promote products derived from this software without specific prior written permission.
21
+ #
22
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23
+
24
+ import os
25
+ import sys
26
+ import math
27
+ import numpy as np
28
+ from pathlib import Path
29
+ import pandas as pd
30
+ import toml as toml
31
+ import plotly.graph_objects as go
32
+ from barviz import Simplex, Collection, Scrawler, Attributes
33
+ from math import sqrt, cos, sin
34
+ import streamlit as st
35
+ from sklearn.preprocessing import StandardScaler
36
+ from scipy.signal import savgol_filter
37
+ import argparse
38
+
39
+
40
+ def _ensure_repo_on_path() -> None:
41
+ here = Path(__file__).resolve()
42
+ for parent in here.parents:
43
+ candidate = parent / "agilab"
44
+ if candidate.is_dir():
45
+ src_root = candidate.parent
46
+ repo_root = src_root.parent
47
+ for entry in (str(src_root), str(repo_root)):
48
+ if entry not in sys.path:
49
+ sys.path.insert(0, entry)
50
+ break
51
+
52
+
53
+ _ensure_repo_on_path()
54
+
55
+ from agi_env import AgiEnv
56
+ from agi_env.app_settings_support import prepare_app_settings_for_write
57
+ from agi_gui.pagelib import sidebar_views, find_files, load_df, on_project_change, select_project, JumpToMain, update_datadir, \
58
+ initialize_csv_files, update_var, _dump_toml_payload
59
+ import tomllib as _toml
60
+
61
+ var = ["discrete", "continuous", "lat", "long"]
62
+ var_default = [0, None]
63
+
64
+ st.title(":chart_with_upwards_trend: Barycentric Graph")
65
+
66
+
67
+ class ModifiedScrawler(Scrawler):
68
+ """
69
+ A class representing a modified version of a scrawler.
70
+
71
+ Attributes:
72
+ simplex (Scrawler): The scrawler object.
73
+ fig (plotly.graph_objs.Figure): The plotly figure object.
74
+
75
+ Methods:
76
+ plot(*stuffs, save_as=None, observed_point=None, format='png'): Plot method for creating visualizations.
77
+
78
+ Args:
79
+ *stuffs: Variable length arguments for additional data to plot.
80
+ save_as (str): The filename to save the plot as. Default is None.
81
+ observed_point: The observed point to update the center to. Default is None.
82
+ format (str): The format for saving the plot. Default is 'png'.
83
+
84
+ Returns:
85
+ None
86
+ """
87
+ """ """
88
+
89
+ def plot(self, *stuffs, save_as=None, observed_point=None, format="png"):
90
+ """
91
+
92
+ Args:
93
+ *stuffs:
94
+ save_as: (Default value = None)
95
+ observed_point: (Default value = None)
96
+ format: (Default value = 'png')
97
+
98
+ Returns:
99
+
100
+ """
101
+ attrs = self.simplex.attrs
102
+ renderer = attrs.renderer
103
+ skeleton = self.simplex.get_skeleton()
104
+ traces = self._trace_collection(skeleton)
105
+ config = {
106
+ "toImageButtonOptions": {
107
+ "format": format,
108
+ "filename": self.simplex.name,
109
+ "width": attrs.width,
110
+ "height": attrs.height,
111
+ "scale": attrs.save_scale,
112
+ }
113
+ }
114
+ if stuffs is not None:
115
+ for c in stuffs:
116
+ traces.extend(self._trace_collection(c))
117
+ fig = go.Figure(data=[*traces])
118
+ if observed_point is not None:
119
+ self.update_center(observed_point)
120
+ fig.update_layout(self._get_layout())
121
+ st.plotly_chart(fig, config=config, renderer=renderer)
122
+ self.fig = fig
123
+ self.plot_save(save_as)
124
+
125
+
126
+ class ModifiedSimplex(Simplex):
127
+ """
128
+ A class representing a modified simplex.
129
+
130
+ Attributes:
131
+ points (list): List of points that define the simplex.
132
+ name (str): The name of the simplex.
133
+ colors (list): List of colors for the simplex.
134
+ labels (list): List of labels for the simplex.
135
+ attrs (dict): Dictionary of attributes for the simplex.
136
+ n_points (int): The number of points in the simplex.
137
+ """
138
+ """ """
139
+
140
+ def __init__(
141
+ self,
142
+ points=[],
143
+ name="unknown",
144
+ colors=None,
145
+ labels=None,
146
+ attrs={},
147
+ n_points=None,
148
+ ):
149
+ """
150
+ Initialize a Simplex object.
151
+
152
+ Args:
153
+ points (list, optional): A list of points that define the simplex. Defaults to an empty list.
154
+ name (str, optional): The name of the simplex. Defaults to 'unknown'.
155
+ colors (list, optional): A list of colors for the simplex. Defaults to None.
156
+ labels (list, optional): A list of labels for the points of the simplex. Defaults to None.
157
+ attrs (dict, optional): A dictionary of attributes for the simplex. Defaults to an empty dictionary.
158
+ n_points (int, optional): The number of points to generate for the simplex. If provided, points will be generated automatically based on this value.
159
+
160
+ Raises:
161
+ None
162
+
163
+ Returns:
164
+ None
165
+ """
166
+ if n_points is not None:
167
+ points = self.__create_simplex_points(n_points)
168
+ super(Simplex, self).__init__(points, name, colors, labels, attrs)
169
+ self.version = Simplex.version
170
+ self._attrs = Attributes(attrs, Simplex._attributes_default)
171
+ self.scrawler = ModifiedScrawler(self)
172
+ if labels is None:
173
+ self.labels = [f"P{i}" for i in range(self.nbp)]
174
+ if colors is None:
175
+ self.colors = [i for i in range(self.nbp)]
176
+ if self.attrs.markers_colormap["cmax"] is None:
177
+ self.attrs.markers_colormap["cmax"] = self.nbp - 1
178
+ if self.attrs.lines_colormap["cmax"] is None:
179
+ self.attrs.lines_colormap["cmax"] = self.nbp - 1
180
+
181
+ def __create_simplex_points(self, n):
182
+ """
183
+ Create a set of points forming a simplex in 3D space.
184
+
185
+ Args:
186
+ n (int): The number of points to generate.
187
+
188
+ Returns:
189
+ numpy.ndarray: An array of 3D points forming a simplex.
190
+
191
+ Note:
192
+ The points are generated using a phi value calculated based on the golden ratio.
193
+
194
+ Raises:
195
+ None
196
+ """
197
+ points = []
198
+ phi = math.pi * (3.0 - sqrt(5.0))
199
+ for i in range(n):
200
+ y = 1 - (i / float(n - 1)) * 2
201
+ radius = sqrt(1 - y * y)
202
+ theta = phi * i
203
+ x = cos(theta) * radius
204
+ z = sin(theta) * radius
205
+ points.append((x, y, z))
206
+ return np.array(points)
207
+
208
+
209
+ def __normalize_data(data):
210
+ """
211
+ Normalize the input data using StandardScaler.
212
+
213
+ Args:
214
+ data (DataFrame): Input data to be normalized.
215
+
216
+ Returns:
217
+ DataFrame: Normalized data using StandardScaler.
218
+
219
+ Raises:
220
+ None
221
+ """
222
+ scaler = StandardScaler()
223
+ data = data.fillna(0)
224
+ normalized_data = pd.DataFrame(scaler.fit_transform(data), columns=data.columns)
225
+ return normalized_data
226
+
227
+
228
+ def _maybe_smooth_long_column(df: pd.DataFrame) -> None:
229
+ """
230
+ Apply a Savitzky-Golay filter to the 'long' column when sufficient data exists.
231
+ """
232
+ if "long" not in df.columns:
233
+ return
234
+
235
+ long_numeric = pd.to_numeric(df["long"], errors="coerce")
236
+ valid_mask = long_numeric.notna()
237
+ valid_count = int(valid_mask.sum())
238
+ if valid_count < 5:
239
+ return
240
+
241
+ # Choose an odd window length no larger than 21 and not exceeding valid_count
242
+ window_length = min(21, valid_count if valid_count % 2 else valid_count - 1)
243
+
244
+ polyorder = 2 if window_length > 3 else 1
245
+
246
+ try:
247
+ smoothed_values = savgol_filter(long_numeric[valid_mask], window_length=window_length, polyorder=polyorder)
248
+ except ValueError:
249
+ # Fall back to no smoothing if the parameters are incompatible
250
+ return
251
+
252
+ df.loc[valid_mask, "long"] = smoothed_values
253
+
254
+
255
+ def __bary_visualisation(df, selected_format, selected_name, selected_x1, selected_x2, color=None):
256
+ """
257
+ Visualize barycentric coordinates using a simplex plot.
258
+
259
+ Args:
260
+ df (DataFrame): The input DataFrame with the data to visualize.
261
+ selected_format (str): The selected format for visualization.
262
+ selected_name (str): The selected name for visualization.
263
+ selected_x1 (str): The selected x-axis parameter for visualization.
264
+ selected_x2 (str): The selected y-axis parameter for visualization.
265
+ color (str): Optional parameter for color coding.
266
+
267
+ Returns:
268
+ None
269
+
270
+ Raises:
271
+ JumpToMain: If an exception occurs while trying to update the visualization.
272
+
273
+ Notes:
274
+ This function visualizes barycentric coordinates using a simplex plot, with optional color coding based on a specified parameter.
275
+ The input DataFrame should contain the data to be visualized.
276
+ """
277
+ normalized_data = __normalize_data(df)
278
+ numpy_array = normalized_data.values
279
+ barycentric_data = np.exp(numpy_array) / np.sum(
280
+ np.exp(numpy_array), axis=1, keepdims=True
281
+ )
282
+ barycentric_data = pd.DataFrame(barycentric_data, columns=df.columns)
283
+
284
+ if color is not None:
285
+ color_df = st.session_state.loaded_df[color]
286
+ labels = [
287
+ (
288
+ f"Index: {index} | "
289
+ " | ".join(
290
+ f"{selected_x2}: {col} | {selected_x1}: {val}"
291
+ for col, val in row.items()
292
+ if pd.notna(val)
293
+ )
294
+ + f" | {color}: {color_df.iloc[index]}"
295
+ )
296
+ for index, row in df.iterrows()
297
+ if pd.notna(index) and isinstance(index, int)
298
+ ]
299
+ if color_df.dtypes in ["object", "bool"]:
300
+ color_mapping = {
301
+ color: index for index, color in enumerate(color_df.unique())
302
+ }
303
+ color_array = color_df.map(color_mapping)
304
+ colorscale = "Jet"
305
+ else:
306
+ color_array = color_df.values
307
+ colorscale = "Blues"
308
+ cmin = np.min(color_array)
309
+ cmax = np.max(color_array)
310
+ c = Collection(points=barycentric_data, labels=labels, colors=color_array)
311
+ c.attrs.markers_colormap = {
312
+ "colorscale": colorscale,
313
+ "cmin": cmin,
314
+ "cmax": cmax,
315
+ }
316
+ else:
317
+ labels = [
318
+ (
319
+ f"Index: {index} | {' | '.join(f'{col}: {val}' for col, val in row.items())}"
320
+ )
321
+ for index, row in df.iterrows()
322
+ ]
323
+ c = Collection(points=barycentric_data, labels=labels)
324
+ c.attrs.markers_colormap = {
325
+ "colorscale": ["blue", "blue"],
326
+ "cmin": 0,
327
+ "cmax": 1,
328
+ }
329
+
330
+ c.attrs.markers_opacity = 1
331
+ c.attrs.markers_size = 3
332
+ c.attrs.markers_border_width = 0
333
+ s = ModifiedSimplex(
334
+ n_points=df.shape[1], name=selected_name, labels=df.columns.tolist()
335
+ )
336
+ s.attrs.lines_visible = False
337
+ s.attrs.markers_size = 3
338
+ s.attrs.markers_colormap = {
339
+ "colorscale": ["white", "white"],
340
+ "cmin": 0,
341
+ "cmax": 1,
342
+ }
343
+ s.attrs.width = 700
344
+ s.attrs.height = 700
345
+ s.attrs.text_size = 12
346
+ st.header(f"{selected_x1} per {selected_x2}")
347
+ s.plot(c, format=selected_format)
348
+
349
+ try:
350
+ st.write(st.session_state.loaded_df[[f"{selected_x2}", f"{selected_x1}"]].T)
351
+ except Exception as e:
352
+ JumpToMain(e)
353
+
354
+ tables = [f"Normalized {selected_x1}", "Barycentric coordinates"]
355
+ selected_table = st.selectbox(
356
+ label="Data", label_visibility="hidden", options=tables
357
+ )
358
+ if selected_table == tables[0]:
359
+ st.write(normalized_data)
360
+ else:
361
+ st.write(barycentric_data)
362
+
363
+
364
+ def page(env):
365
+ # Initialize session state
366
+ """
367
+ Page function for displaying data visualization tools.
368
+
369
+ This function sets up the data directory, project, and visualization parameters for the user interface.
370
+
371
+ Returns:
372
+ None
373
+
374
+ Raises:
375
+ None
376
+ """
377
+
378
+ if "project" not in st.session_state:
379
+ st.session_state["project"] = env.target
380
+
381
+ if "projects" not in st.session_state:
382
+ st.session_state["projects"] = env.projects
383
+
384
+ # Load persisted settings
385
+ settings_path = Path(env.app_settings_file)
386
+ persisted = {}
387
+ try:
388
+ with open(settings_path, "rb") as fh:
389
+ persisted = _toml.load(fh)
390
+ except Exception:
391
+ persisted = {}
392
+ raw_view_settings = persisted.get("view_barycentric", {}) if isinstance(persisted, dict) else {}
393
+ view_settings = raw_view_settings if isinstance(raw_view_settings, dict) else {}
394
+
395
+ # Seed session from persisted values
396
+ if "datadir" not in st.session_state and "datadir" in view_settings:
397
+ st.session_state["datadir"] = view_settings["datadir"]
398
+ if "df_file" not in st.session_state and "df_file" in view_settings:
399
+ st.session_state["df_file"] = view_settings["df_file"]
400
+
401
+ datadir = Path(st.session_state.datadir)
402
+ # Data directory input
403
+ st.sidebar.text_input(
404
+ "Data Directory",
405
+ value=str(st.session_state.datadir),
406
+ key="input_datadir",
407
+ on_change=update_datadir,
408
+ args=("datadir", "input_datadir"),
409
+ )
410
+
411
+ if not datadir.exists() or not datadir.is_dir():
412
+ st.sidebar.error("Directory not found.")
413
+ st.warning("A valid data directory is required to proceed.")
414
+ return # Stop further processing
415
+
416
+ # Find CSV files in the data directory
417
+ files = find_files(st.session_state["datadir"])
418
+ visible = []
419
+ for f in files:
420
+ try:
421
+ parts = f.relative_to(datadir).parts
422
+ except Exception:
423
+ parts = f.parts
424
+ if any(part.startswith(".") for part in parts):
425
+ continue
426
+ visible.append(f)
427
+ st.session_state["csv_files"] = visible
428
+ if not st.session_state["csv_files"]:
429
+ st.warning("A dataset is required to proceed. Please added via memu execute/export.")
430
+ st.stop() # Stop further processing
431
+
432
+ # Prepare list of CSV files relative to the data directory
433
+ csv_files_rel = []
434
+ for file in st.session_state["csv_files"]:
435
+ try:
436
+ csv_files_rel.append(Path(file).relative_to(datadir).as_posix())
437
+ except Exception:
438
+ continue
439
+ csv_files_rel = sorted(csv_files_rel)
440
+ settings_file = st.session_state.get("df_file")
441
+ if settings_file and settings_file in csv_files_rel:
442
+ default_idx = csv_files_rel.index(settings_file)
443
+ else:
444
+ default_idx = 0
445
+
446
+ # DataFrame selection
447
+ st.sidebar.selectbox(
448
+ label="DataFrame",
449
+ options=csv_files_rel,
450
+ key="df_file",
451
+ index=default_idx,
452
+ # on_change=update_var,
453
+ args=("df_file"),
454
+ )
455
+
456
+ # Check if a DataFrame has been selected
457
+ if not st.session_state.get("df_file"):
458
+ st.warning("Please select a dataset to proceed.")
459
+ return # Stop further processing
460
+
461
+ # Load the selected DataFrame
462
+ df_file_abs = Path(st.session_state.datadir) / st.session_state.df_file
463
+ cache_buster = None
464
+ try:
465
+ cache_buster = df_file_abs.stat().st_mtime_ns
466
+ except Exception:
467
+ pass
468
+ try:
469
+ st.session_state["loaded_df"] = load_df(df_file_abs, with_index=True, cache_buster=cache_buster)
470
+ except Exception as e:
471
+ st.error(f"Error loading data: {e}")
472
+ st.warning("The selected data file could not be loaded. Please select a valid file.")
473
+ return # Stop further processing
474
+
475
+ # Check if data is loaded and valid
476
+ if (
477
+ "loaded_df" not in st.session_state
478
+ or not isinstance(st.session_state.loaded_df, pd.DataFrame)
479
+ or not st.session_state.loaded_df.shape[1] > 0
480
+ ):
481
+ st.warning("The dataset is empty or could not be loaded. Please select a valid data file.")
482
+ return # Stop further processing
483
+
484
+ # Persist selections
485
+ save_fields = {
486
+ "datadir": str(st.session_state.get("datadir", "")),
487
+ "df_file": st.session_state.get("df_file", ""),
488
+ }
489
+ mutated = False
490
+ for k, v in save_fields.items():
491
+ if view_settings.get(k) != v and v not in (None, ""):
492
+ view_settings[k] = v
493
+ mutated = True
494
+ if mutated:
495
+ persisted["view_barycentric"] = view_settings
496
+ try:
497
+ settings_path.parent.mkdir(parents=True, exist_ok=True)
498
+ with open(settings_path, "wb") as fh:
499
+ _dump_toml_payload(prepare_app_settings_for_write(persisted), fh)
500
+ except Exception:
501
+ pass
502
+
503
+
504
+ if "df_file" in st.session_state and st.session_state["df_file"]:
505
+ df_file_abs = Path(st.session_state.datadir) / st.session_state.df_file
506
+ cache_buster = None
507
+ try:
508
+ cache_buster = df_file_abs.stat().st_mtime_ns
509
+ except Exception:
510
+ pass
511
+ st.session_state["loaded_df"] = load_df(df_file_abs, cache_buster=cache_buster)
512
+
513
+ if "loaded_df" in st.session_state:
514
+ if (
515
+ isinstance(st.session_state.loaded_df, pd.DataFrame)
516
+ and not st.session_state.loaded_df.empty
517
+ ):
518
+ nrows = st.session_state.loaded_df.shape[0]
519
+ lines = st.slider(
520
+ "Number of rows:",
521
+ min_value=10,
522
+ max_value=nrows,
523
+ value=nrows // 10,
524
+ step=100,
525
+ )
526
+ if lines >= 0:
527
+ st.session_state.loaded_df = st.session_state.loaded_df.iloc[:lines, :]
528
+
529
+ _maybe_smooth_long_column(st.session_state.loaded_df)
530
+
531
+ if "project" in st.session_state:
532
+ st.markdown(f"{env.target} worker arguments:")
533
+ settings = toml.load(env.app_settings_file)
534
+ current_filename = Path(__file__).stem
535
+ # set default values
536
+ if (
537
+ current_filename in settings
538
+ and isinstance(settings[current_filename], dict)
539
+ and "variables" in settings[current_filename]
540
+ ):
541
+ st.session_state["variables"] = settings[current_filename][
542
+ "variables"
543
+ ]
544
+
545
+ # Get the list of column names from the loaded DataFrame.
546
+ st.session_state.df_cols = st.session_state.loaded_df.columns.tolist()
547
+
548
+ numeric_cols = []
549
+ for col in st.session_state.df_cols:
550
+ try:
551
+ # Use the DataFrame (loaded_df) to access the column.
552
+ st.session_state.loaded_df[col].astype(float)
553
+ numeric_cols.append(col)
554
+ except Exception:
555
+ # If conversion fails, skip the column.
556
+ pass
557
+
558
+ # st.write("Columns that can be converted to float:", numeric_cols)
559
+
560
+ if "variables" in st.session_state:
561
+ default_x1 = st.session_state.variables[0]
562
+ default_x2 = st.session_state.variables[1]
563
+ default_color = st.session_state.variables[2]
564
+ else:
565
+ default_x1 = st.session_state.df_cols[0]
566
+ default_x2 = st.session_state.df_cols[0]
567
+ default_color = st.session_state.df_cols[0]
568
+
569
+ col1, col2, col3 = st.columns(3)
570
+
571
+ with col1:
572
+ selected_x1 = st.selectbox(
573
+ "Correlated variables pair",
574
+ numeric_cols,
575
+ index=(
576
+ st.session_state.df_cols.index(default_x1)
577
+ if default_x1 in st.session_state.df_cols
578
+ else 0
579
+ ),
580
+ )
581
+ selected_x2 = st.selectbox(
582
+ "Correlated variables",
583
+ numeric_cols,
584
+ label_visibility="collapsed",
585
+ index=(
586
+ st.session_state.df_cols.index(default_x2)
587
+ if default_x2 in st.session_state.df_cols
588
+ else 0
589
+ ),
590
+ )
591
+ with col2:
592
+ selected_color = st.selectbox(
593
+ "Color",
594
+ st.session_state.df_cols,
595
+ index=(
596
+ st.session_state.df_cols.index(default_color)
597
+ if default_color in st.session_state.df_cols
598
+ else 0
599
+ ),
600
+ )
601
+ with col3:
602
+ selected_name = st.text_input(label="File", value="myfigure")
603
+ selected_format = st.selectbox(
604
+ label="Format",
605
+ label_visibility="collapsed",
606
+ options=["jpeg", "png", "svg", "webp"],
607
+ index=0
608
+ )
609
+
610
+ if selected_x1 and selected_x2 and selected_color:
611
+ pivot_df = st.session_state.loaded_df.drop_duplicates(
612
+ subset=[selected_x1]
613
+ )
614
+ pivot_df = pivot_df.dropna(subset=[selected_x1, selected_x2])
615
+ pivot_df = pivot_df.pivot(columns=selected_x1, values=selected_x2)
616
+
617
+ if pivot_df.shape[1] > 1:
618
+ __bary_visualisation(pivot_df,
619
+ selected_format,
620
+ selected_name,
621
+ selected_x1,
622
+ selected_x2,
623
+ color=selected_color)
624
+ else:
625
+ st.info(
626
+ f"Error: only 1 distinct value for {selected_x2}. To plot this graph, there must be at "
627
+ f"least 2 different values for {selected_x2} in the provided dataset."
628
+ f"Select more rows, or choose another correlated variable."
629
+ )
630
+
631
+
632
+ # -------------------- Main Application Entry -------------------- #
633
+ def main():
634
+ """
635
+ Main function to run the application.
636
+ """
637
+ try:
638
+ parser = argparse.ArgumentParser(description="Run the AGI Streamlit View with optional parameters.")
639
+ parser.add_argument(
640
+ "--active-app",
641
+ dest="active_app",
642
+ type=str,
643
+ help="Active app path (e.g. src/agilab/apps/builtin/flight_telemetry_project)",
644
+ required=True,
645
+ )
646
+ args, _ = parser.parse_known_args()
647
+
648
+ active_app = Path(args.active_app).expanduser()
649
+ if not active_app.exists():
650
+ st.error(f"Error: provided --active-app path not found: {active_app}")
651
+ sys.exit(1)
652
+
653
+ if "coltype" not in st.session_state:
654
+ st.session_state["coltype"] = var[0]
655
+
656
+ # Short app name (e.g., 'flight_telemetry_project')
657
+ app = active_app.name
658
+ st.session_state["apps_path"] = str(active_app.parent)
659
+ st.session_state["app"] = app
660
+
661
+ env = AgiEnv(
662
+ apps_path=active_app.parent,
663
+ app=app,
664
+ verbose=1,
665
+ )
666
+ env.init_done = True
667
+ st.session_state['env'] = env
668
+ st.session_state["IS_SOURCE_ENV"] = env.is_source_env
669
+ st.session_state["IS_WORKER_ENV"] = env.is_worker_env
670
+
671
+ if "TABLE_MAX_ROWS" not in st.session_state:
672
+ st.session_state["TABLE_MAX_ROWS"] = env.TABLE_MAX_ROWS
673
+ if "GUI_SAMPLING" not in st.session_state:
674
+ st.session_state["GUI_SAMPLING"] = env.GUI_SAMPLING
675
+
676
+ # Initialize session state
677
+ page(env)
678
+
679
+ except Exception as e:
680
+ st.error(f"An error occurred: {e}")
681
+ import traceback
682
+
683
+ st.caption("Full traceback")
684
+ st.code(traceback.format_exc(), language="text")
685
+
686
+
687
+ # -------------------- Main Entry Point -------------------- #
688
+ if __name__ == "__main__":
689
+ main()