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.
- agi_page_simplex_map-0.1.0.dist-info/METADATA +14 -0
- agi_page_simplex_map-0.1.0.dist-info/RECORD +8 -0
- agi_page_simplex_map-0.1.0.dist-info/WHEEL +5 -0
- agi_page_simplex_map-0.1.0.dist-info/entry_points.txt +2 -0
- agi_page_simplex_map-0.1.0.dist-info/top_level.txt +1 -0
- view_barycentric/__init__.py +7 -0
- view_barycentric/barycentric_graph.py +14 -0
- view_barycentric/view_barycentric.py +689 -0
|
@@ -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 @@
|
|
|
1
|
+
view_barycentric
|
|
@@ -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()
|