CoolFace
Apppublic

hussain2010/Line_Graphs_Customization

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes
app.py1220 linesDownload Raw Back to root
1import streamlit as st2import pandas as pd3import matplotlib.pyplot as plt4import numpy as np5from io import BytesIO6import matplotlib as mpl7from scipy.signal import savgol_filter, butter, filtfilt, medfilt8from scipy.ndimage import gaussian_filter1d9from statsmodels.nonparametric.smoothers_lowess import lowess10 11# =========================================================12# GLOBAL DEFAULT FONT (does NOT remove font selector)13# =========================================================14mpl.rcParams["font.family"] = "Times New Roman"15mpl.rcParams["axes.unicode_minus"] = False16 17SPEED_OF_LIGHT = 299_792_458.0  # m/s18 19FREQ_UNITS = {20    "Hz": 1.0,21    "kHz": 1e3,22    "MHz": 1e6,23    "GHz": 1e9,24    "THz": 1e12,25}26 27LENGTH_UNITS = {28    "m": 1.0,29    "cm": 1e-2,30    "mm": 1e-3,31    "um": 1e-6,32    "nm": 1e-9,33    "pm": 1e-12,34}35 36 37# =========================================================38# Download function39# =========================================================40def download_button(fig, file_name, file_format, dpi):41    buffer = BytesIO()42    fig.savefig(buffer, format=file_format.lower(), dpi=dpi, bbox_inches="tight")43    buffer.seek(0)44 45    mime_map = {46        "png": "image/png",47        "jpg": "image/jpeg",48        "jpeg": "image/jpeg",49        "svg": "image/svg+xml",50        "pdf": "application/pdf",51        "eps": "application/postscript",52        "tif": "image/tiff",53        "tiff": "image/tiff",54    }55 56    st.download_button(57        label=f"Download as {file_format.upper()} ({dpi} DPI)",58        data=buffer,59        file_name=f"{file_name}_{dpi}dpi.{file_format.lower()}",60        mime=mime_map.get(file_format.lower(), "application/octet-stream"),61    )62 63 64# =========================================================65# Smoothing function (preserved; only safer edge handling)66# =========================================================67def apply_smoothing(data, method, **params):68    data = np.asarray(data, dtype=float)69 70    if method == "None":71        return data72 73    elif method == "Moving Average":74        window = int(params.get("window_size", 5))75        return pd.Series(data).rolling(window, center=True, min_periods=1).mean().to_numpy()76 77    elif method == "Gaussian":78        sigma = float(params.get("sigma", 2.0))79        return gaussian_filter1d(data, sigma=sigma)80 81    elif method == "Savitzky-Golay":82        window = int(params.get("window_size", 21))83        poly = int(params.get("poly_order", 3))84        window = _make_valid_odd_window(window, len(data), poly + 2)85        return savgol_filter(data, window, poly)86 87    elif method == "Median":88        kernel = int(params.get("kernel_size", 5))89        kernel = _make_valid_odd_window(kernel, len(data), 1)90        return medfilt(data, kernel_size=kernel)91 92    elif method == "Combined":93        kernel = int(params.get("kernel_size", 5))94        window = int(params.get("window_size", 21))95        poly = int(params.get("poly_order", 3))96        kernel = _make_valid_odd_window(kernel, len(data), 1)97        window = _make_valid_odd_window(window, len(data), poly + 2)98        temp = medfilt(data, kernel_size=kernel)99        return savgol_filter(temp, window, poly)100 101    elif method == "Exponential Moving Average":102        span = int(params.get("span", 10))103        return pd.Series(data).ewm(span=span).mean().to_numpy()104 105    elif method == "LOWESS":106        frac = float(params.get("frac", 0.1))107        return lowess(data, np.arange(len(data)), frac=frac, return_sorted=False)108 109    elif method == "Butterworth":110        order = int(params.get("order", 3))111        cutoff = float(params.get("cutoff", 0.05))112        b, a = butter(order, cutoff, btype="low")113        return filtfilt(b, a, data)114 115    elif method == "Fourier Transform":116        keep = float(params.get("keep_fraction", 0.1))117        keep = np.clip(keep, 0.0, 1.0)118        fft_vals = np.fft.fft(data)119        n = len(fft_vals)120        fft_vals[int(n * keep): int(n * (1 - keep))] = 0121        return np.real(np.fft.ifft(fft_vals))122 123    return data124 125 126def _make_valid_odd_window(window, data_len, minimum):127    window = max(int(window), int(minimum))128    if window % 2 == 0:129        window += 1130    if window > data_len:131        window = data_len if data_len % 2 == 1 else max(1, data_len - 1)132    if window < minimum:133        window = minimum if minimum % 2 == 1 else minimum + 1134    if window > data_len:135        window = data_len if data_len % 2 == 1 else max(1, data_len - 1)136    return max(window, 1)137 138 139# =========================================================140# Numeric cleaning / parsing helpers141# =========================================================142def clean_column(column):143    if pd.api.types.is_numeric_dtype(column):144        return pd.to_numeric(column, errors="coerce")145 146    cleaned = (147        column.astype(str)148        .str.replace(",", "", regex=False)149        .str.extract(r"([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)", expand=False)150    )151    return pd.to_numeric(cleaned, errors="coerce")152 153 154def parse_optional_float(text):155    if text is None:156        return None157    if isinstance(text, (int, float, np.integer, np.floating)):158        return float(text)159 160    text = str(text).strip()161    if text == "":162        return None163 164    try:165        return float(text)166    except ValueError:167        return None168 169 170# =========================================================171# Unit conversion helpers172# =========================================================173def linear_to_db(values, mode="Power (10log10)"):174    values = np.asarray(values, dtype=float)175    out = np.full(values.shape, np.nan, dtype=float)176    valid = values > 0177    factor = 10.0 if mode == "Power (10log10)" else 20.0178    out[valid] = factor * np.log10(values[valid])179    return out, np.count_nonzero(~valid)180 181 182 183def db_to_linear(values, mode="Power (10log10)"):184    values = np.asarray(values, dtype=float)185    factor = 10.0 if mode == "Power (10log10)" else 20.0186    return np.power(10.0, values / factor), 0187 188 189 190def convert_series(values, scale_option, config=None, axis_name="Axis"):191    values = np.asarray(values, dtype=float)192    config = config or {}193    warnings = []194 195    if scale_option == "None":196        return values, warnings197 198    if scale_option == "Hz → MHz":199        return values / 1e6, warnings200 201    if scale_option == "Hz → GHz":202        return values / 1e9, warnings203 204    if scale_option == "mm → cm":205        return values / 10.0, warnings206 207    if scale_option == "mm → m":208        return values / 1000.0, warnings209 210    if scale_option == "cm → mm":211        return values * 10.0, warnings212 213    if scale_option == "m → mm":214        return values * 1000.0, warnings215 216    if scale_option == "Frequency → Wavelength":217        in_unit = config.get("freq_input_unit", "GHz")218        out_unit = config.get("wave_output_unit", "nm")219        freq_hz = values * FREQ_UNITS[in_unit]220 221        out = np.full(freq_hz.shape, np.nan, dtype=float)222        valid = freq_hz != 0223        out[valid] = (SPEED_OF_LIGHT / freq_hz[valid]) / LENGTH_UNITS[out_unit]224        invalid_count = np.count_nonzero(~valid)225        if invalid_count:226            warnings.append(227                f"{axis_name}: {invalid_count} zero-value points were skipped during frequency-to-wavelength conversion."228            )229        return out, warnings230 231    if scale_option == "Wavelength → Frequency":232        in_unit = config.get("wave_input_unit", "nm")233        out_unit = config.get("freq_output_unit", "GHz")234        wavelength_m = values * LENGTH_UNITS[in_unit]235 236        out = np.full(wavelength_m.shape, np.nan, dtype=float)237        valid = wavelength_m != 0238        out[valid] = (SPEED_OF_LIGHT / wavelength_m[valid]) / FREQ_UNITS[out_unit]239        invalid_count = np.count_nonzero(~valid)240        if invalid_count:241            warnings.append(242                f"{axis_name}: {invalid_count} zero-value points were skipped during wavelength-to-frequency conversion."243            )244        return out, warnings245 246    if scale_option == "Linear → dB":247        mode = config.get("db_mode", "Power (10log10)")248        out, invalid_count = linear_to_db(values, mode=mode)249        if invalid_count:250            warnings.append(251                f"{axis_name}: {invalid_count} non-positive points were skipped during linear-to-dB conversion."252            )253        return out, warnings254 255    if scale_option == "dB → Linear":256        mode = config.get("db_mode", "Power (10log10)")257        out, _ = db_to_linear(values, mode=mode)258        return out, warnings259 260    return values, warnings261 262 263# =========================================================264# App config265# =========================================================266st.set_page_config(layout="wide")267st.title("Advanced CSV Data Visualization App")268 269uploaded_file = st.file_uploader("Upload your CSV file", type="csv")270 271if uploaded_file is not None:272    try:273        df = pd.read_csv(uploaded_file)274        df = df.apply(clean_column)275 276        if df.empty or df.dropna(how="all").empty:277            raise ValueError("No usable numeric data was found in the uploaded CSV file.")278 279        # =================================================280        # Sidebar281        # =================================================282        with st.sidebar:283            st.subheader("Graph Settings")284            col1, col2 = st.columns(2)285 286            # ---------------- X AXIS ----------------287            with col1:288                x_column = st.selectbox("X-axis Column:", options=df.columns, index=0)289                x_label = st.text_input("X-axis Label:", value="Frequency")290                x_unit = st.text_input("X-axis Unit:", value="GHz")291                scale_option = st.selectbox(292                    "X Scaling Option:",293                    [294                        "None",295                        "Hz → MHz",296                        "Hz → GHz",297                        "mm → cm",298                        "mm → m",299                        "cm → mm",300                        "m → mm",301                        "Frequency → Wavelength",302                        "Wavelength → Frequency",303                        "Linear → dB",304                        "dB → Linear",305                    ],306                    index=2,307                )308 309                x_conversion_config = {}310                if scale_option == "Frequency → Wavelength":311                    x_conversion_config["freq_input_unit"] = st.selectbox(312                        "X Input Frequency Unit:",313                        list(FREQ_UNITS.keys()),314                        index=3,315                        key="x_freq_input_unit",316                    )317                    x_conversion_config["wave_output_unit"] = st.selectbox(318                        "X Output Wavelength Unit:",319                        list(LENGTH_UNITS.keys()),320                        index=4,321                        key="x_wave_output_unit",322                    )323                elif scale_option == "Wavelength → Frequency":324                    x_conversion_config["wave_input_unit"] = st.selectbox(325                        "X Input Wavelength Unit:",326                        list(LENGTH_UNITS.keys()),327                        index=4,328                        key="x_wave_input_unit",329                    )330                    x_conversion_config["freq_output_unit"] = st.selectbox(331                        "X Output Frequency Unit:",332                        list(FREQ_UNITS.keys()),333                        index=3,334                        key="x_freq_output_unit",335                    )336                elif scale_option in ["Linear → dB", "dB → Linear"]:337                    x_conversion_config["db_mode"] = st.selectbox(338                        "X dB Conversion Mode:",339                        ["Power (10log10)", "Amplitude (20log10)"],340                        index=0,341                        key="x_db_mode",342                    )343 344                x_min_text = st.text_input("Lower X-axis Limit:", value="")345                x_max_text = st.text_input("Upper X-axis Limit:", value="")346                x_step = st.number_input("X-axis Step Size:", value=5.0, format="%f")347                x_tick_position = st.selectbox(348                    "X-axis Tick Position:", ["out", "in", "inout"], index=2349                )350                show_x_tick_marks = st.checkbox(351                    "Show X-axis division marks", value=True352                )353                show_x_tick_labels = st.checkbox(354                    "Show X-axis labels", value=True355                )356 357            # ---------------- Y AXIS ----------------358            with col2:359                default_y_columns = [col for col in df.columns if col != x_column]360 361                if "y_columns_state" not in st.session_state:362                    st.session_state.y_columns_state = default_y_columns363 364                st.session_state.y_columns_state = [365                    col for col in st.session_state.y_columns_state if col != x_column366                ]367                if not st.session_state.y_columns_state:368                    st.session_state.y_columns_state = default_y_columns369 370                y_columns = st.multiselect(371                    "Y-axis Column(s):",372                    options=[col for col in df.columns if col != x_column],373                    default=st.session_state.y_columns_state,374                )375                st.session_state.y_columns_state = y_columns376 377                if st.button("➕ Add all remaining columns"):378                    st.session_state.y_columns_state = [379                        col for col in df.columns if col != x_column380                    ]381                    st.rerun()382 383                if st.button("❌ Clear Y-axis selection"):384                    st.session_state.y_columns_state = []385                    st.rerun()386 387                y_label = st.text_input("Y-axis Label:", value="S21")388                y_unit = st.text_input("Y-axis Unit:", value="dB")389                y_scale_option = st.selectbox(390                    "Y Scaling Option:",391                    [392                        "None",393                        "Hz → MHz",394                        "Hz → GHz",395                        "mm → cm",396                        "mm → m",397                        "cm → mm",398                        "m → mm",399                        "Frequency → Wavelength",400                        "Wavelength → Frequency",401                        "Linear → dB",402                        "dB → Linear",403                    ],404                    index=0,405                )406 407                y_conversion_config = {}408                if y_scale_option == "Frequency → Wavelength":409                    y_conversion_config["freq_input_unit"] = st.selectbox(410                        "Y Input Frequency Unit:",411                        list(FREQ_UNITS.keys()),412                        index=3,413                        key="y_freq_input_unit",414                    )415                    y_conversion_config["wave_output_unit"] = st.selectbox(416                        "Y Output Wavelength Unit:",417                        list(LENGTH_UNITS.keys()),418                        index=4,419                        key="y_wave_output_unit",420                    )421                elif y_scale_option == "Wavelength → Frequency":422                    y_conversion_config["wave_input_unit"] = st.selectbox(423                        "Y Input Wavelength Unit:",424                        list(LENGTH_UNITS.keys()),425                        index=4,426                        key="y_wave_input_unit",427                    )428                    y_conversion_config["freq_output_unit"] = st.selectbox(429                        "Y Output Frequency Unit:",430                        list(FREQ_UNITS.keys()),431                        index=3,432                        key="y_freq_output_unit",433                    )434                elif y_scale_option in ["Linear → dB", "dB → Linear"]:435                    y_conversion_config["db_mode"] = st.selectbox(436                        "Y dB Conversion Mode:",437                        ["Power (10log10)", "Amplitude (20log10)"],438                        index=0,439                        key="y_db_mode",440                    )441 442                y_min_text = st.text_input("Lower Y-axis Limit:", value="")443                y_max_text = st.text_input("Upper Y-axis Limit:", value="")444                y_step = st.number_input("Y-axis Step Size:", value=10.0, format="%f")445                y_tick_position = st.selectbox(446                    "Y-axis Tick Position:", ["out", "in", "inout"], index=2447                )448                show_y_tick_marks = st.checkbox(449                    "Show Y-axis division marks", value=True450                )451                show_y_tick_labels = st.checkbox(452                    "Show Y-axis labels", value=True453                )454 455            # ---------------- TITLE & FONT ----------------456            st.subheader("Graph Font Settings")457            title = st.text_input("Graph Title:", value="MWPF, 100Km DM, S21, Far-field")458            font_theme = st.selectbox(459                "Graph Font Family (applies to all graph text):",460                ["Times New Roman", "Arial", "Courier New", "Helvetica", "Verdana"],461                index=0,462            )463            font_style_choice = st.selectbox(464                "Title Font Style:", ["Normal", "Italic", "Bold"], index=0465            )466            use_global_font_size = st.checkbox(467                "Use one font size for the whole graph", value=False468            )469 470            if use_global_font_size:471                whole_graph_font_size = st.slider(472                    "Whole Graph Font Size:", 6, 30, 14473                )474                title_font_size = whole_graph_font_size475                x_label_font_size = whole_graph_font_size476                y_label_font_size = whole_graph_font_size477                x_tick_font_size = whole_graph_font_size478                y_tick_font_size = whole_graph_font_size479                legend_font_size = whole_graph_font_size480            else:481                title_font_size = st.slider("Title Font Size:", 10, 30, 14)482                x_label_font_size = st.slider(483                    "X-axis Label Font Size:", 8, 24, 14484                )485                y_label_font_size = st.slider(486                    "Y-axis Label Font Size:", 8, 24, 14487                )488                x_tick_font_size = st.slider(489                    "X-axis Tick Font Size:", 6, 22, 12490                )491                y_tick_font_size = st.slider(492                    "Y-axis Tick Font Size:", 6, 22, 12493                )494                legend_font_size = st.slider("Legend Font Size:", 6, 20, 10)495 496            # ---------------- GRID ----------------497            st.subheader("Grid Settings")498            show_grid = st.checkbox("Show Grid", value=True)499            show_minor_grid = st.checkbox("Show Sub-grid (Minor Grid)", value=False)500            grid_direction = st.selectbox("Grid Direction:", ["x", "y", "both"], index=2)501            grid_line_style = st.selectbox(502                "Grid Line Style:", ["-", "--", "-.", ":", "None"], index=0503            )504            grid_color = st.color_picker("Grid Line Color:", "#DDDDDD")505            grid_line_width = st.slider("Grid Line Width:", 0.5, 2.5, 1.0)506 507            # ---------------- SMOOTHING ----------------508            st.subheader("Advanced Smoothing Settings")509 510            smoothing_method = st.selectbox(511                "Smoothing Method:",512                [513                    "None",514                    "Moving Average",515                    "Gaussian",516                    "Savitzky-Golay",517                    "Median",518                    "Combined",519                    "Exponential Moving Average",520                    "LOWESS",521                    "Butterworth",522                    "Fourier Transform",523                ],524            )525 526            smoothing_params = {}527 528            if smoothing_method == "Moving Average":529                smoothing_params["window_size"] = st.slider("Window Size", 3, 101, 5, 2)530 531            elif smoothing_method == "Gaussian":532                smoothing_params["sigma"] = st.slider("Sigma", 0.1, 10.0, 2.0, 0.1)533 534            elif smoothing_method == "Savitzky-Golay":535                smoothing_params["window_size"] = st.slider("Window Size", 5, 101, 21, 2)536                smoothing_params["poly_order"] = st.slider("Polynomial Order", 1, 5, 3)537 538            elif smoothing_method == "Median":539                smoothing_params["kernel_size"] = st.slider("Kernel Size", 3, 51, 5, 2)540 541            elif smoothing_method == "Combined":542                smoothing_params["kernel_size"] = st.slider("Median Kernel", 3, 51, 5, 2)543                smoothing_params["window_size"] = st.slider("SG Window", 5, 101, 21, 2)544                smoothing_params["poly_order"] = st.slider("Polynomial Order", 1, 5, 3)545 546            elif smoothing_method == "Exponential Moving Average":547                smoothing_params["span"] = st.slider("EMA Span", 1, 50, 10)548 549            elif smoothing_method == "LOWESS":550                smoothing_params["frac"] = st.slider(551                    "LOWESS Fraction", 0.01, 0.5, 0.1, 0.01552                )553 554            elif smoothing_method == "Butterworth":555                smoothing_params["order"] = st.slider("Filter Order", 1, 10, 3)556                smoothing_params["cutoff"] = st.slider(557                    "Cutoff Frequency", 0.01, 0.5, 0.05, 0.01558                )559 560            elif smoothing_method == "Fourier Transform":561                smoothing_params["keep_fraction"] = st.slider(562                    "Keep Fraction", 0.01, 1.0, 0.1, 0.01563                )564 565            # ---------------- MARKERS ----------------566            marker_legend_locations = [567                "upper right",568                "upper center",569                "upper left",570                "center right",571                "center",572                "center left",573                "lower right",574                "lower center",575                "lower left",576            ]577 578            marker_line_styles = {579                "Solid": "-",580                "Dashed": "--",581                "Dash-Dot": "-.",582                "Dotted": ":",583            }584 585            st.subheader("Vertical Marker Settings")586            marker_x_values = st.text_input(587                "Enter X-axis Values for Vertical Markers (comma-separated):", value=""588            )589            vmarker_color = st.color_picker("Vertical Marker Color:", "#FF0000")590            vmarker_line_style_name = st.selectbox(591                "Vertical Marker Line Style:",592                list(marker_line_styles.keys()),593                index=1,594            )595            vmarker_line_style = marker_line_styles[vmarker_line_style_name]596 597            vmarker_candidates = []598            if marker_x_values.strip():599                try:600                    vmarker_candidates = [601                        float(v.strip())602                        for v in marker_x_values.split(",")603                        if v.strip() != ""604                    ]605                except Exception:606                    vmarker_candidates = []607 608            selected_vmarkers = []609            if vmarker_candidates:610                selected_vmarkers = st.multiselect(611                    "Select Vertical Marker(s):",612                    options=vmarker_candidates,613                    default=vmarker_candidates,614                )615 616            show_vmarker_values = st.checkbox(617                "Show line values at selected vertical markers", value=True618            )619 620            vmarker_legend_enable = st.checkbox(621                "Show vertical marker legend (marker values)", value=True622            )623            vmarker_legend_location = st.selectbox(624                "Vertical Marker Legend Location:",625                marker_legend_locations,626                index=0,627            )628 629            vvalues_legend_enable = st.checkbox(630                "Show vertical marker intersection values in legend", value=True631            )632            vvalues_legend_location = st.selectbox(633                "Vertical Values Legend Location:",634                marker_legend_locations,635                index=2,636            )637 638            st.subheader("Horizontal Marker Settings")639            marker_y_values = st.text_input(640                "Enter Y-axis Values for Horizontal Markers (comma-separated):", value=""641            )642            hmarker_color = st.color_picker("Horizontal Marker Color:", "#0000FF")643            hmarker_line_style_name = st.selectbox(644                "Horizontal Marker Line Style:",645                list(marker_line_styles.keys()),646                index=1,647            )648            hmarker_line_style = marker_line_styles[hmarker_line_style_name]649 650            hmarker_candidates = []651            if marker_y_values.strip():652                try:653                    hmarker_candidates = [654                        float(v.strip())655                        for v in marker_y_values.split(",")656                        if v.strip() != ""657                    ]658                except Exception:659                    hmarker_candidates = []660 661            selected_hmarkers = []662            if hmarker_candidates:663                selected_hmarkers = st.multiselect(664                    "Select Horizontal Marker(s):",665                    options=hmarker_candidates,666                    default=hmarker_candidates,667                )668 669            show_hmarker_values = st.checkbox(670                "Show line values at selected horizontal markers", value=True671            )672 673            hmarker_legend_enable = st.checkbox(674                "Show horizontal marker legend (marker values)", value=True675            )676            hmarker_legend_location = st.selectbox(677                "Horizontal Marker Legend Location:",678                marker_legend_locations,679                index=6,680            )681 682            hvalues_legend_enable = st.checkbox(683                "Show horizontal marker intersection values in legend", value=True684            )685            hvalues_legend_location = st.selectbox(686                "Horizontal Values Legend Location:",687                marker_legend_locations,688                index=8,689            )690 691            # ---------------- DOWNLOAD ----------------692            dpi = st.selectbox("Select DPI for Download:", [100, 200, 300, 600], index=2)693            file_format = st.selectbox(694                "Select File Format for Download:",695                ["PNG", "JPG", "SVG", "PDF", "EPS", "TIFF"],696                index=0,697            )698 699            # ---------------- LEGEND ----------------700            st.subheader("Legend Customization")701            if use_global_font_size:702                st.caption(f"Legend font size follows the whole graph font size: {legend_font_size}")703            legend_font_weight = st.selectbox("Font Weight:", ["Normal", "Bold"], index=0)704            legend_bg_color = st.color_picker("Background Color:", "#FFFFFF")705            legend_border_color = st.color_picker("Border Color:", "#000000")706            legend_border_width = st.slider("Border Width:", 0.5, 2.0, 1.0)707            legend_alpha = st.slider(708                "Frame Alpha (0 = transparent, 1 = opaque):", 0.0, 1.0, 0.5709            )710            legend_title = st.text_input("Legend Title:", value="")711            legend_location = st.selectbox(712                "Legend Location:",713                marker_legend_locations,714                index=1,715            )716            legend_columns = st.selectbox("Legend Columns:", [1, 2, 3, 4, 5], index=1)717 718            # =================================================719            # LINE STYLE SETTINGS720            # =================================================721            st.subheader("Line Style Settings")722 723            unite_lines = st.checkbox(724                "Unite line formatting for all line graphs", value=False725            )726 727            unify_ls = False728            unify_ms = False729            unify_color = False730            unify_lw = False731 732            line_styles = {733                "Solid": "-",734                "Dashed": "--",735                "Dash-Dot": "-.",736                "Dotted": ":",737            }738 739            marker_styles = {740                "None": "",741                "Circle": "o",742                "Square": "s",743                "Star": "*",744                "Diamond": "D",745                "Triangle": "^",746                "Pentagon": "p",747                "Hexagon": "H",748                "Plus": "+",749                "X": "x",750            }751 752            auto_colors = plt.rcParams["axes.prop_cycle"].by_key()["color"]753            auto_line_styles = list(line_styles.values())754            auto_marker_styles = [m for m in marker_styles.values() if m != ""]755 756            if "auto_format_seed" not in st.session_state:757                st.session_state.auto_format_seed = int(np.random.randint(0, 1_000_000))758 759            unified_line_width = 1.0760 761            if unite_lines:762                st.markdown("**Apply unified formatting for:**")763                unify_ls = st.checkbox("Line Style", value=True)764                unify_ms = st.checkbox("Marker Style", value=False)765                unify_color = st.checkbox("Line Color", value=False)766                unify_lw = st.checkbox("Line Width", value=True)767 768                st.caption(769                    "Checked line color, style, and marker are auto-distributed across the selected graph lines. "770                    "Checked line width uses one identical width for all selected graph lines."771                )772 773                if unify_lw:774                    unified_line_width = st.slider(775                        "Select unified line width for all graph lines", 0.5, 3.0, 1.0776                    )777                    st.caption(778                        "This shared width overrides the individual line-width sliders below."779                    )780 781                if unify_color or unify_ls or unify_ms:782                    if st.button("Reshuffle auto formatting"):783                        st.session_state.auto_format_seed = int(784                            np.random.randint(0, 1_000_000)785                        )786 787            def build_shuffled_pool(pool, seed_offset):788                if not pool:789                    return []790                pool_array = np.array(pool, dtype=object)791                rng = np.random.default_rng(int(st.session_state.auto_format_seed) + seed_offset)792                return pool_array[rng.permutation(len(pool_array))].tolist()793 794            shuffled_colors = build_shuffled_pool(auto_colors, 11)795            shuffled_line_styles = build_shuffled_pool(auto_line_styles, 23)796            shuffled_marker_styles = build_shuffled_pool(auto_marker_styles, 37)797 798            style_settings = {}799 800            for i, y_column in enumerate(y_columns):801                with st.expander(f"Line Style for '{y_column}'", expanded=False):802                    default_color = auto_colors[i % len(auto_colors)]803 804                    color = st.color_picker(805                        f"Color for '{y_column}'",806                        default_color,807                        key=f"color_{y_column}",808                    )809 810                    line_style_name = st.selectbox(811                        "Line Style",812                        list(line_styles.keys()),813                        key=f"ls_{y_column}",814                    )815 816                    marker_style_name = st.selectbox(817                        "Marker Style",818                        list(marker_styles.keys()),819                        key=f"ms_{y_column}",820                    )821 822                    line_width = st.slider(823                        "Line Width", 0.5, 3.0, 1.0, key=f"lw_{y_column}"824                    )825 826                    applied_color = (827                        shuffled_colors[i % len(shuffled_colors)]828                        if unite_lines and unify_color and len(shuffled_colors) > 0829                        else color830                    )831                    applied_line_style = (832                        shuffled_line_styles[i % len(shuffled_line_styles)]833                        if unite_lines and unify_ls and len(shuffled_line_styles) > 0834                        else line_styles[line_style_name]835                    )836                    applied_marker_style = (837                        shuffled_marker_styles[i % len(shuffled_marker_styles)]838                        if unite_lines and unify_ms and len(shuffled_marker_styles) > 0839                        else marker_styles[marker_style_name]840                    )841                    applied_line_width = (842                        unified_line_width843                        if unite_lines and unify_lw844                        else line_width845                    )846 847                    style_settings[y_column] = {848                        "color": applied_color,849                        "line_style": applied_line_style,850                        "marker_style": applied_marker_style,851                        "line_width": applied_line_width,852                    }853 854 855        # =================================================856        # Plot857        # =================================================858        st.subheader("Graph Output")859 860        if st.button("Generate Graph"):861            if not y_columns:862                st.warning("Please select at least one Y-axis column.")863                st.stop()864 865            fig, ax = plt.subplots()866            messages = []867 868            x_min = parse_optional_float(x_min_text)869            x_max = parse_optional_float(x_max_text)870            y_min = parse_optional_float(y_min_text)871            y_max = parse_optional_float(y_max_text)872 873            if x_min_text.strip() and x_min is None:874                messages.append("X-axis lower limit could not be parsed and was ignored.")875            if x_max_text.strip() and x_max is None:876                messages.append("X-axis upper limit could not be parsed and was ignored.")877            if y_min_text.strip() and y_min is None:878                messages.append("Y-axis lower limit could not be parsed and was ignored.")879            if y_max_text.strip() and y_max is None:880                messages.append("Y-axis upper limit could not be parsed and was ignored.")881 882            x_raw = pd.to_numeric(df[x_column], errors="coerce").to_numpy(dtype=float)883            x_converted, x_warnings = convert_series(884                x_raw,885                scale_option,886                config=x_conversion_config,887                axis_name="X-axis",888            )889            messages.extend(x_warnings)890 891            plotted_series = {}892 893            for col in y_columns:894                y_raw = pd.to_numeric(df[col], errors="coerce").to_numpy(dtype=float)895                y_converted, y_warnings = convert_series(896                    y_raw,897                    y_scale_option,898                    config=y_conversion_config,899                    axis_name=f"Y-axis ({col})",900                )901                messages.extend(y_warnings)902 903                mask = np.isfinite(x_converted) & np.isfinite(y_converted)904                if np.count_nonzero(mask) < 2:905                    messages.append(906                        f"'{col}' was skipped because fewer than two valid points remained after conversion."907                    )908                    continue909 910                x_plot = x_converted[mask]911                y_plot = y_converted[mask]912 913                order = np.argsort(x_plot)914                x_plot = x_plot[order]915                y_plot = y_plot[order]916 917                y_smoothed = apply_smoothing(y_plot, smoothing_method, **smoothing_params)918 919                plotted_series[col] = {920                    "x": x_plot,921                    "y": y_smoothed,922                }923 924                ax.plot(925                    x_plot,926                    y_smoothed,927                    label=col,928                    linestyle=style_settings[col]["line_style"],929                    marker=style_settings[col]["marker_style"],930                    color=style_settings[col]["color"],931                    linewidth=style_settings[col]["line_width"],932                )933 934            if not plotted_series:935                st.warning("No plottable Y-series remained after conversion and cleaning.")936                st.stop()937 938            if x_min is not None and x_max is not None:939                ax.set_xlim(x_min, x_max)940                if x_step > 0:941                    ax.set_xticks(np.arange(x_min, x_max + x_step, x_step))942 943            if y_min is not None and y_max is not None:944                ax.set_ylim(y_min, y_max)945                if y_step > 0:946                    ax.set_yticks(np.arange(y_min, y_max + y_step, y_step))947 948            if font_style_choice == "Italic":949                title_fontstyle = "italic"950                title_fontweight = "normal"951            elif font_style_choice == "Bold":952                title_fontstyle = "normal"953                title_fontweight = "bold"954            else:955                title_fontstyle = "normal"956                title_fontweight = "normal"957 958            ax.set_xlabel(959                f"{x_label} ({x_unit})",960                fontsize=x_label_font_size,961                family=font_theme,962            )963            ax.set_ylabel(964                f"{y_label} ({y_unit})",965                fontsize=y_label_font_size,966                family=font_theme,967            )968            ax.set_title(969                title,970                fontsize=title_font_size,971                fontstyle=title_fontstyle,972                fontweight=title_fontweight,973                family=font_theme,974            )975 976            ax.tick_params(977                axis="x",978                direction=x_tick_position,979                labelsize=x_tick_font_size,980                bottom=show_x_tick_marks,981                labelbottom=show_x_tick_labels,982            )983            ax.tick_params(984                axis="y",985                direction=y_tick_position,986                labelsize=y_tick_font_size,987                left=show_y_tick_marks,988                labelleft=show_y_tick_labels,989            )990 991            for tick in ax.get_xticklabels() + ax.get_yticklabels():992                tick.set_fontfamily(font_theme)993 994            if show_grid and grid_line_style != "None":995                ax.grid(996                    True,997                    linestyle=grid_line_style,998                    linewidth=grid_line_width,999                    color=grid_color,1000                    axis=grid_direction,1001                )1002 1003            if show_minor_grid and grid_line_style != "None":1004                ax.minorticks_on()1005                ax.grid(1006                    which="minor",1007                    linestyle=grid_line_style,1008                    linewidth=grid_line_width,1009                    color=grid_color,1010                )1011 1012            # ==========================1013            # MARKERS (Vertical + Horizontal) with legend-only values1014            # ==========================1015            vmarker_handles = []1016            hmarker_handles = []1017            vvalue_handles = []1018            hvalue_handles = []1019 1020            # --- Vertical Markers ---1021            if selected_vmarkers:1022                for j, xm in enumerate(selected_vmarkers, start=1):1023                    ax.axvline(1024                        xm,1025                        color=vmarker_color,1026                        linestyle=vmarker_line_style,1027                        linewidth=1.0,1028                    )1029 1030                    if vmarker_legend_enable:1031                        vmarker_handles.append(1032                            mpl.lines.Line2D(1033                                [],1034                                [],1035                                color=vmarker_color,1036                                linestyle=vmarker_line_style,1037                                linewidth=1.0,1038                                label=f"V{j}: x = {xm:g}",1039                            )1040                        )1041 1042                    if show_vmarker_values and vvalues_legend_enable:1043                        for col, series in plotted_series.items():1044                            x_m = series["x"]1045                            y_m = series["y"]1046 1047                            if xm < x_m[0] or xm > x_m[-1]:1048                                continue1049 1050                            ym = float(np.interp(xm, x_m, y_m))1051                            ax.scatter([xm], [ym], s=18, color=style_settings[col]["color"], zorder=5)1052 1053                            vvalue_handles.append(1054                                mpl.lines.Line2D(1055                                    [],1056                                    [],1057                                    color=style_settings[col]["color"],1058                                    marker="o",1059                                    linestyle="None",1060                                    markersize=5,1061                                    label=f"V{j} ({col}) = {ym:.6g}",1062                                )1063                            )1064 1065            # --- Horizontal Markers ---1066            if selected_hmarkers:1067                for j, ym in enumerate(selected_hmarkers, start=1):1068                    ax.axhline(1069                        ym,1070                        color=hmarker_color,1071                        linestyle=hmarker_line_style,1072                        linewidth=1.0,1073                    )1074 1075                    if hmarker_legend_enable:1076                        hmarker_handles.append(1077                            mpl.lines.Line2D(1078                                [],1079                                [],1080                                color=hmarker_color,1081                                linestyle=hmarker_line_style,1082                                linewidth=1.0,1083                                label=f"H{j}: y = {ym:g}",1084                            )1085                        )1086 1087                    if show_hmarker_values and hvalues_legend_enable:1088                        for col, series in plotted_series.items():1089                            x_m = series["x"]1090                            y_m = series["y"]1091 1092                            diff = y_m - ym1093                            sign = np.sign(diff)1094                            idx = np.where(np.diff(sign) != 0)[0]1095                            if len(idx) == 0:1096                                continue1097 1098                            i0 = int(idx[0])1099                            x0, x1 = x_m[i0], x_m[i0 + 1]1100                            y0, y1 = y_m[i0], y_m[i0 + 1]1101                            if y1 == y0:1102                                continue1103 1104                            xm = float(x0 + (ym - y0) * (x1 - x0) / (y1 - y0))1105 1106                            ax.scatter([xm], [ym], s=18, color=style_settings[col]["color"], zorder=5)1107 1108                            hvalue_handles.append(1109                                mpl.lines.Line2D(1110                                    [],1111                                    [],1112                                    color=style_settings[col]["color"],1113                                    marker="o",1114                                    linestyle="None",1115                                    markersize=5,1116                                    label=f"H{j} ({col}) x = {xm:.6g}",1117                                )1118                            )1119 1120            # ==========================1121            # LEGENDS1122            # ==========================1123            def style_legend(legend_obj):1124                if legend_obj is None:1125                    return1126                legend_obj.get_frame().set_linewidth(legend_border_width)1127                if legend_obj.get_title() is not None:1128                    legend_obj.get_title().set_fontfamily(font_theme)1129                    legend_obj.get_title().set_fontweight(legend_font_weight.lower())1130                for text in legend_obj.get_texts():1131                    text.set_fontfamily(font_theme)1132                    text.set_fontweight(legend_font_weight.lower())1133 1134            marker_legend_font_size = (1135                legend_font_size if use_global_font_size else max(6, legend_font_size - 1)1136            )1137            marker_value_font_size = (1138                legend_font_size if use_global_font_size else max(6, legend_font_size - 2)1139            )1140 1141            main_leg = ax.legend(1142                title=legend_title,1143                fontsize=legend_font_size,1144                loc=legend_location,1145                frameon=True,1146                facecolor=legend_bg_color,1147                edgecolor=legend_border_color,1148                framealpha=legend_alpha,1149                ncol=legend_columns,1150            )1151            style_legend(main_leg)1152            ax.add_artist(main_leg)1153 1154            if vmarker_legend_enable and len(vmarker_handles) > 0:1155                vleg = ax.legend(1156                    handles=vmarker_handles,1157                    title="Vertical Markers",1158                    fontsize=marker_legend_font_size,1159                    loc=vmarker_legend_location,1160                    frameon=True,1161                    facecolor=legend_bg_color,1162                    edgecolor=legend_border_color,1163                    framealpha=legend_alpha,1164                )1165                style_legend(vleg)1166                ax.add_artist(vleg)1167 1168            if hmarker_legend_enable and len(hmarker_handles) > 0:1169                hleg = ax.legend(1170                    handles=hmarker_handles,1171                    title="Horizontal Markers",1172                    fontsize=marker_legend_font_size,1173                    loc=hmarker_legend_location,1174                    frameon=True,1175                    facecolor=legend_bg_color,1176                    edgecolor=legend_border_color,1177                    framealpha=legend_alpha,1178                )1179                style_legend(hleg)1180                ax.add_artist(hleg)1181 1182            if show_vmarker_values and vvalues_legend_enable and len(vvalue_handles) > 0:1183                vvleg = ax.legend(1184                    handles=vvalue_handles,1185                    title="Vertical Marker Values",1186                    fontsize=marker_value_font_size,1187                    loc=vvalues_legend_location,1188                    frameon=True,1189                    facecolor=legend_bg_color,1190                    edgecolor=legend_border_color,1191                    framealpha=legend_alpha,1192                )1193                style_legend(vvleg)1194                ax.add_artist(vvleg)1195 1196            if show_hmarker_values and hvalues_legend_enable and len(hvalue_handles) > 0:1197                hvleg = ax.legend(1198                    handles=hvalue_handles,1199                    title="Horizontal Marker Values",1200                    fontsize=marker_value_font_size,

Showing the first 1,200 of 1220 lines. Download the file for the rest.