From 8a845bf156dbd7f29141d4d304aebf25c823a7fa Mon Sep 17 00:00:00 2001 From: atla8167 <athanasio.lakes@dsv.su.se> Date: Fri, 29 Nov 2024 14:59:34 +0200 Subject: [PATCH] Fixed Glacier Experiments not Viewed in Counterfactuals template. Enhanced home template --- base/handlers/__init__.py | 4 + .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 367 bytes .../ajaxChartsHandler.cpython-310.pyc | Bin 0 -> 3680 bytes ...ajaxCounterfactualsHandler.cpython-310.pyc | Bin 0 -> 10364 bytes .../ajaxHomeHandler.cpython-310.pyc | Bin 0 -> 5673 bytes .../ajaxTrainHandler.cpython-310.pyc | Bin 0 -> 7285 bytes base/handlers/ajaxChartsHandler.py | 219 +++ base/handlers/ajaxCounterfactualsHandler.py | 768 +++++++++ base/handlers/ajaxHomeHandler.py | 437 +++++ base/handlers/ajaxTrainHandler.py | 464 +++++ base/static/css/sb-admin-2.css | 478 +++++- base/static/img/digital_features.png | Bin 0 -> 12071 bytes base/static/img/su_logo.png | Bin 0 -> 40692 bytes base/static/img/undraw_posting_photo.svg | 1 - base/static/img/undraw_profile.svg | 38 - base/static/img/undraw_profile_1.svg | 38 - base/static/img/undraw_profile_2.svg | 44 - base/static/img/undraw_profile_3.svg | 47 - base/static/img/undraw_rocket.svg | 39 - base/static/js/click_reset.js | 29 - base/static/js/counterfactuals.js | 1 + base/static/js/home.js | 613 +++++++ base/static/js/import.js | 21 - base/static/js/main.js | 38 +- base/static/js/methods.js | 108 +- base/static/js/radio_dataset.js | 98 -- base/static/js/radio_model.js | 82 +- base/static/js/radio_timeseries_dataset.js | 130 +- base/static/js/radio_uploaded_dataset.js | 118 -- base/static/js/train.js | 307 +++- base/templates/base/charts.html | 27 +- base/templates/base/counterfactuals.html | 91 +- base/templates/base/home.html | 512 ++++-- base/templates/base/train.html | 153 +- base/views.py | 1515 +---------------- 35 files changed, 4169 insertions(+), 2251 deletions(-) create mode 100644 base/handlers/__init__.py create mode 100644 base/handlers/__pycache__/__init__.cpython-310.pyc create mode 100644 base/handlers/__pycache__/ajaxChartsHandler.cpython-310.pyc create mode 100644 base/handlers/__pycache__/ajaxCounterfactualsHandler.cpython-310.pyc create mode 100644 base/handlers/__pycache__/ajaxHomeHandler.cpython-310.pyc create mode 100644 base/handlers/__pycache__/ajaxTrainHandler.cpython-310.pyc create mode 100644 base/handlers/ajaxChartsHandler.py create mode 100644 base/handlers/ajaxCounterfactualsHandler.py create mode 100644 base/handlers/ajaxHomeHandler.py create mode 100644 base/handlers/ajaxTrainHandler.py create mode 100644 base/static/img/digital_features.png create mode 100644 base/static/img/su_logo.png delete mode 100755 base/static/img/undraw_posting_photo.svg delete mode 100755 base/static/img/undraw_profile.svg delete mode 100755 base/static/img/undraw_profile_1.svg delete mode 100755 base/static/img/undraw_profile_2.svg delete mode 100755 base/static/img/undraw_profile_3.svg delete mode 100755 base/static/img/undraw_rocket.svg delete mode 100755 base/static/js/click_reset.js create mode 100755 base/static/js/home.js delete mode 100755 base/static/js/import.js delete mode 100755 base/static/js/radio_dataset.js delete mode 100644 base/static/js/radio_uploaded_dataset.js diff --git a/base/handlers/__init__.py b/base/handlers/__init__.py new file mode 100644 index 000000000..fdae05bc9 --- /dev/null +++ b/base/handlers/__init__.py @@ -0,0 +1,4 @@ +from .ajaxHomeHandler import handler as home_handler +from .ajaxCounterfactualsHandler import handler as counterfactuals_handler +from .ajaxChartsHandler import handler as charts_handler +from .ajaxTrainHandler import handler as train_handler diff --git a/base/handlers/__pycache__/__init__.cpython-310.pyc b/base/handlers/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..830ecc83b6554a47997e4a71383138e808712db6 GIT binary patch literal 367 zcmY+9u};J=42F{)(1KbxCM4dV3y}~bLP#*&#BxksB42B0A!$*PisNw@d8MvQY|KnJ zy<7#BV*kJOIdQryJ75&=;`Z^I>Zdn<BO-H6$6ipXSj837yomTl&q(hCy-Rv8=$Ldu zI$E-P_Qh#5=R;gWTPZYm`F?Q(Pq1Ee8W)?M@$U?2O&uAq;?y%YUEex14^X-ulnwZB zQvUBxL&xJ#4aQCEVL93fnS12kK+|q-%ba&x;Dec%wi(mUx$rv)1<z;&uAM>MYx#;R lu>y-C>}rLSP1`sr&tBe!<WhH4ukgx$+$NM#mhzO(`7f5(YMKB5 literal 0 HcmV?d00001 diff --git a/base/handlers/__pycache__/ajaxChartsHandler.cpython-310.pyc b/base/handlers/__pycache__/ajaxChartsHandler.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..08c8a3de6c6e7ca07e5ccb7c7c54f5f8070cc7a5 GIT binary patch literal 3680 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 literal 0 HcmV?d00001 diff --git a/base/handlers/__pycache__/ajaxCounterfactualsHandler.cpython-310.pyc b/base/handlers/__pycache__/ajaxCounterfactualsHandler.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b8666251ebd9e08c44d2b35dd5728488f74c8f42 GIT binary patch literal 10364 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 bNI9Llb3(&z?@=_V$Ouf1!fnvr_tgInC~@C8 literal 0 HcmV?d00001 diff --git a/base/handlers/__pycache__/ajaxHomeHandler.cpython-310.pyc b/base/handlers/__pycache__/ajaxHomeHandler.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d0d39195b61073b7003dc142b23404eaa47ebf03 GIT binary patch literal 5673 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 G%>M#l`UQ3X literal 0 HcmV?d00001 diff --git a/base/handlers/__pycache__/ajaxTrainHandler.cpython-310.pyc b/base/handlers/__pycache__/ajaxTrainHandler.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3f957594366efcb8bfd39684d3a924890d80e0c9 GIT binary patch literal 7285 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 literal 0 HcmV?d00001 diff --git a/base/handlers/ajaxChartsHandler.py b/base/handlers/ajaxChartsHandler.py new file mode 100644 index 000000000..df690c95c --- /dev/null +++ b/base/handlers/ajaxChartsHandler.py @@ -0,0 +1,219 @@ +import base.pipeline as pipeline +import os +import pandas as pd +import joblib +from dict_and_html import * +from .. import methods +from ..methods import PIPELINE_PATH +import base.pipeline as pipeline +import json +from django.shortcuts import HttpResponse + +def handler(action, request): + status = 200 + if action == "pre_trained": + # load pre trained models + pre_trained_model_name = request.POST.get("pre_trained") + request.session["model_name"] = pre_trained_model_name + # dataframe name + df_name = request.session.get("df_name") + + if df_name == "upload": + df_name = request.session.get("df_name_upload_base_name") + + model_name_path = os.path.join( + PIPELINE_PATH + f"{df_name}" + "/trained_models/" + pre_trained_model_name + ) + + model_name_dir_path = os.path.join(PIPELINE_PATH + f"{df_name}") + + # get the type of the file + datasets_types_PipelineJSON_path = os.path.join( + PIPELINE_PATH + "/dataset_types_pipeline.json" + ) + datasets_types_pipeline = pipeline.PipelineJSON( + datasets_types_PipelineJSON_path + ) + dataset_type = datasets_types_pipeline.read_from_json([df_name]) + + if type(dataset_type) is list: + dataset_type = dataset_type[0] + + if "url" in request.POST: + url = request.POST.get("url") + + if url == "counterfactuals": + # only TSNE + tsne = joblib.load(model_name_path + "/tsne.sav") + + # Assuming you already have your fig object created, you can update it like this: + # Improved and modern t-SNE visualization + tsne.update_layout( + # Modern Legend Design + legend=dict( + x=0.9, + y=0.95, + xanchor="right", + yanchor="top", + bgcolor="rgba(255,255,255,0.8)", # Light semi-transparent white background + bordercolor="rgba(0,0,0,0.1)", # Light border for contrast + borderwidth=1, + font=dict(size=12, color="#444"), # Subtle grey for legend text + ), + # Tight Margins to Focus on the Plot + margin=dict( + l=10, r=10, t=30, b=10 + ), # Very slim margins for a modern look + # Axis Design: Minimalist and Clean + xaxis=dict( + title_text="", # No axis labels for a clean design + tickfont=dict( + size=10, color="#aaa" + ), # Light grey for tick labels + showline=True, + linecolor="rgba(0,0,0,0.2)", # Subtle line color for axis lines + zeroline=False, # No zero line for a sleek look + showgrid=False, # Hide grid lines for a minimal appearance + ticks="outside", # Small ticks outside the axis + ticklen=3, # Short tick marks for subtlety + ), + yaxis=dict( + title_text="", # No axis labels + tickfont=dict(size=10, color="#aaa"), + showline=True, + linecolor="rgba(0,0,0,0.2)", + zeroline=False, + showgrid=False, + ticks="outside", + ticklen=3, + ), + # Sleek Background + plot_bgcolor="#fafafa", # Very light grey background for a smooth finish + paper_bgcolor="#ffffff", # Pure white paper background + # Modern Title with Elegant Style + title=dict( + text="t-SNE Visualization of Data", + font=dict( + size=16, color="#222", family="Helvetica, Arial, sans-serif" + ), # Classy font style + x=0.5, + xanchor="center", + yanchor="top", + pad=dict(t=15), # Padding to separate the title from the plot + ), + ) + + # Add hover effects for a smooth user experience + tsne.update_traces( + hoverinfo="text+name", + hoverlabel=dict(bgcolor="white", font_size=12, font_family="Arial"), + ) + + context = { + "tsne": tsne.to_html(), + } + else: + # load plots + pca = joblib.load(model_name_path + "/pca.sav") + classification_report = joblib.load( + model_name_path + "/classification_report.sav" + ) + # tsne = joblib.load(model_name_path + "/tsne.sav") + + # pipeline path + json_path = os.path.join(PIPELINE_PATH, f"{df_name}" + "/pipeline.json") + jsonFile = pipeline.PipelineJSON(json_path) + + # load pipeline data + # jsonFile = open(json_path, "r") + # pipeline_data = json.load(jsonFile) # data becomes a dictionary + # classifier_data = pipeline_data["classifier"][pre_trained_model_name] + + classifier_data = jsonFile.read_from_json( + ["classifier", pre_trained_model_name] + ) + classifier_data_flattened = methods.flatten_dict(classifier_data) + classifier_data_df = pd.DataFrame([classifier_data_flattened]) + + if dataset_type == "tabular": + feature_importance = joblib.load( + model_name_path + "/feature_importance.sav" + ) + context = { + "dataset_type": dataset_type, + "pca": pca.to_html(), + "class_report": classification_report.to_html(), + "feature_importance": feature_importance.to_html(), + "classifier_data": classifier_data_df.to_html(), + } + elif dataset_type == "timeseries": + tsne = joblib.load(model_name_path + "/tsne.sav") + context = { + "dataset_type": dataset_type, + "pca": pca.to_html(), + "class_report": classification_report.to_html(), + "tsne": tsne.to_html(), + "classifier_data": classifier_data_df.to_html(), + } + elif action == "delete_pre_trained": + + df_name = request.session["df_name"] + model_name = request.POST.get("model_name") + model_name_path = os.path.join( + PIPELINE_PATH + f"{df_name}" + "/trained_models/" + model_name + ) + + print(model_name_path) + + excel_file_name_preprocessed_path = os.path.join( + PIPELINE_PATH, + f"{df_name}" + "/" + df_name + "_preprocessed" + ".csv", + ) + try: + # Check if the file exists + if os.path.exists(excel_file_name_preprocessed_path): + # Delete the file + os.remove(excel_file_name_preprocessed_path) + # print(f"File '{excel_file_name_preprocessed_path}' has been deleted successfully.") + else: + print(f"File '{excel_file_name_preprocessed_path}' does not exist.") + except Exception as e: + print(f"An error occurred while deleting the file: {e}") + + json_path = os.path.join(PIPELINE_PATH + f"{df_name}" + "/pipeline.json") + jsonFile = pipeline.PipelineJSON(json_path) + jsonFile.delete_key(["classifier", model_name]) + + methods.remove_dir_and_empty_parent(model_name_path) + # load paths + # absolute excel_file_preprocessed_path + + if not jsonFile.key_exists("classifier"): + # pre trained models do not exist + # check if dataset directory exists + df_dir = os.path.join(PIPELINE_PATH + f"{df_name}") + if not os.path.exists(df_dir): + df_name = None + + context = { + "df_name": df_name, + "available_pretrained_models_info": [], + } + else: + # if it exists + # check the section of "classifiers" + # folder path + available_pretrained_models = jsonFile.read_from_json( + ["classifier"] + ).keys() + + available_pretrained_models_info = ( + methods.create_tuple_of_models_text_value( + available_pretrained_models + ) + ) + context = { + "df_name": df_name, + "available_pretrained_models_info": available_pretrained_models_info, + } + return HttpResponse(json.dumps(context), status=status) \ No newline at end of file diff --git a/base/handlers/ajaxCounterfactualsHandler.py b/base/handlers/ajaxCounterfactualsHandler.py new file mode 100644 index 000000000..8ce97890d --- /dev/null +++ b/base/handlers/ajaxCounterfactualsHandler.py @@ -0,0 +1,768 @@ +import base.pipeline as pipeline +import pickle, os +import pandas as pd +import json +from sklearn.preprocessing import LabelEncoder +import joblib +from dict_and_html import * +from .. import methods +from ..methods import PIPELINE_PATH +import math +import numpy as np +from .. glacier.src.glacier_compute_counterfactuals import gc_compute_counterfactuals +import base.pipeline as pipeline +import concurrent.futures +import json +from django.shortcuts import HttpResponse + +def handler(action, request): + status = 200 + if action == "reset_graph": + model_name = request.session.get("model_name") + # dataframe name + excel_file_name = request.session.get("df_name") + # save the plots for future use + # folder path: pipelines/<dataset name>/trained_models/<model_name>/ + model_name_path = os.path.join( + PIPELINE_PATH + f"{excel_file_name}" + "/trained_models/" + model_name + ) + + model_name_dir_path = os.path.join(PIPELINE_PATH + f"{df_name}") + + tsne = joblib.load(model_name_dir_path + "/tsne.sav") + context = {"fig": tsne.to_html()} + elif action == "pre_trained": + # load pre trained models + pre_trained_model_name = request.POST.get("pre_trained") + request.session["model_name"] = pre_trained_model_name + # dataframe name + df_name = request.session.get("df_name") + + if df_name == "upload": + df_name = request.session.get("df_name_upload_base_name") + + model_name_path = os.path.join( + PIPELINE_PATH + f"{df_name}" + "/trained_models/" + pre_trained_model_name + ) + + model_name_dir_path = os.path.join(PIPELINE_PATH + f"{df_name}") + + # get the type of the file + datasets_types_PipelineJSON_path = os.path.join( + PIPELINE_PATH + "/dataset_types_pipeline.json" + ) + datasets_types_pipeline = pipeline.PipelineJSON( + datasets_types_PipelineJSON_path + ) + dataset_type = datasets_types_pipeline.read_from_json([df_name]) + + if type(dataset_type) is list: + dataset_type = dataset_type[0] + + if "url" in request.POST: + url = request.POST.get("url") + + if url == "counterfactuals": + # only TSNE + tsne = joblib.load(model_name_path + "/tsne.sav") + + # Assuming you already have your fig object created, you can update it like this: + # Improved and modern t-SNE visualization + tsne.update_layout( + # Modern Legend Design + legend=dict( + x=0.9, + y=0.95, + xanchor="right", + yanchor="top", + bgcolor="rgba(255,255,255,0.8)", # Light semi-transparent white background + bordercolor="rgba(0,0,0,0.1)", # Light border for contrast + borderwidth=1, + font=dict(size=12, color="#444"), # Subtle grey for legend text + ), + # Tight Margins to Focus on the Plot + margin=dict( + l=10, r=10, t=30, b=10 + ), # Very slim margins for a modern look + # Axis Design: Minimalist and Clean + xaxis=dict( + title_text="", # No axis labels for a clean design + tickfont=dict( + size=10, color="#aaa" + ), # Light grey for tick labels + showline=True, + linecolor="rgba(0,0,0,0.2)", # Subtle line color for axis lines + zeroline=False, # No zero line for a sleek look + showgrid=False, # Hide grid lines for a minimal appearance + ticks="outside", # Small ticks outside the axis + ticklen=3, # Short tick marks for subtlety + ), + yaxis=dict( + title_text="", # No axis labels + tickfont=dict(size=10, color="#aaa"), + showline=True, + linecolor="rgba(0,0,0,0.2)", + zeroline=False, + showgrid=False, + ticks="outside", + ticklen=3, + ), + # Sleek Background + plot_bgcolor="#fafafa", # Very light grey background for a smooth finish + paper_bgcolor="#ffffff", # Pure white paper background + # Modern Title with Elegant Style + title=dict( + text="t-SNE Visualization of Data", + font=dict( + size=16, color="#222", family="Helvetica, Arial, sans-serif" + ), # Classy font style + x=0.5, + xanchor="center", + yanchor="top", + pad=dict(t=15), # Padding to separate the title from the plot + ), + ) + + # Add hover effects for a smooth user experience + tsne.update_traces( + hoverinfo="text+name", + hoverlabel=dict(bgcolor="white", font_size=12, font_family="Arial"), + ) + + context = { + "tsne": tsne.to_html(), + } + else: + # load plots + pca = joblib.load(model_name_path + "/pca.sav") + classification_report = joblib.load( + model_name_path + "/classification_report.sav" + ) + # tsne = joblib.load(model_name_path + "/tsne.sav") + + # pipeline path + json_path = os.path.join(PIPELINE_PATH, f"{df_name}" + "/pipeline.json") + jsonFile = pipeline.PipelineJSON(json_path) + + # load pipeline data + # jsonFile = open(json_path, "r") + # pipeline_data = json.load(jsonFile) # data becomes a dictionary + # classifier_data = pipeline_data["classifier"][pre_trained_model_name] + + classifier_data = jsonFile.read_from_json( + ["classifier", pre_trained_model_name] + ) + classifier_data_flattened = methods.flatten_dict(classifier_data) + classifier_data_df = pd.DataFrame([classifier_data_flattened]) + + if dataset_type == "tabular": + feature_importance = joblib.load( + model_name_path + "/feature_importance.sav" + ) + context = { + "dataset_type": dataset_type, + "pca": pca.to_html(), + "class_report": classification_report.to_html(), + "feature_importance": feature_importance.to_html(), + "classifier_data": classifier_data_df.to_html(), + } + elif dataset_type == "timeseries": + tsne = joblib.load(model_name_path + "/tsne.sav") + context = { + "dataset_type": dataset_type, + "pca": pca.to_html(), + "class_report": classification_report.to_html(), + "tsne": tsne.to_html(), + "classifier_data": classifier_data_df.to_html(), + } + elif action == "click_graph": + # get df used name + df_name = request.session.get("df_name") + if df_name == "upload": + df_name = request.session.get("df_name_upload_base_name") + # get model_name + model_name = request.POST.get("model_name") + + # preprocessed_path + excel_file_name_preprocessed_path = os.path.join( + PIPELINE_PATH + f"{df_name}" + "/" + df_name + "_preprocessed" + ".csv" + ) + + excel_file_name_path = os.path.join( + PIPELINE_PATH + f"{df_name}" + "/" + df_name + ".csv" + ) + + model_name_path = os.path.join( + PIPELINE_PATH + f"{df_name}" + "/trained_models/" + model_name + ) + + # pipeline path + json_path = os.path.join(PIPELINE_PATH, f"{df_name}" + "/pipeline.json") + + # load pipeline data + # jsonFile = open(json_path, "r") + # pipeline_data = PipelineJSON.load(jsonFile) # data becomes a dictionary + # class_label = pipeline_data["classifier"][model_name]["class_label"] + jsonFile = pipeline.PipelineJSON(json_path) + class_label = jsonFile.read_from_json( + ["classifier", model_name, "class_label"] + ) + + df = pd.read_csv(excel_file_name_path) + + # Load your saved feature importance from a .sav file + feature_importance_df = pd.read_csv( + model_name_path + "/feature_importance_df.csv" + ) + # sorted_df = feature_importance_df.sort_values(by="importance", ascending=False) + + # x and y coordinates of the clicked point in tsne + x_coord = request.POST["x"] + y_coord = request.POST["y"] + + # tsne_projections + tsne_projections_path = os.path.join( + PIPELINE_PATH + + f"{df_name}/" + + f"trained_models/{model_name}" + + "/tsne_projections.json", + ) + + # tsne projections of all points (saved during generation of tsne) + projections = pd.read_json(tsne_projections_path) + projections = projections.values.tolist() + + # projections array is a list of pairs with the (x, y) + # [ [], [], [] ... ] + # coordinates for a point in tsne. These are actual absolute + # coordinates and not SVG. + # find the pair of the projection with x and y coordinates matching that of + # clicked point coordinates + for clicked_id, item in enumerate(projections): + if math.isclose(item[0], float(x_coord)) and math.isclose( + item[1], float(y_coord) + ): + break + + # save clicked point projections + request.session["clicked_point"] = item + # get clicked point row + row = df.iloc[[int(clicked_id)]] + request.session["cfrow_id"] = clicked_id + request.session["cfrow_og"] = row.to_html() + context = { + "row": row.to_html(index=False), + "feature_importance_dict": feature_importance_df.to_dict(orient="records"), + } + elif action == "cf": + # dataframe name + df_name = request.session.get("df_name") + if df_name == "upload": + df_name = request.session.get("df_name_upload_base_name") + + # preprocessed_path + excel_file_name_preprocessed_path = os.path.join( + PIPELINE_PATH + f"{df_name}" + "/" + df_name + "_preprocessed" + ".csv" + ) + + excel_file_name_path = os.path.join( + PIPELINE_PATH + f"{df_name}" + "/" + df_name + ".csv" + ) + # which model is being used during that session + model_name = request.POST.get("model_name") + # path of used model + model_name_path = os.path.join( + PIPELINE_PATH + f"{df_name}/" + "trained_models/" + f"{model_name}/" + ) + model_name_dir_path = os.path.join(PIPELINE_PATH + f"{df_name}") + + # read preprocessed data + if os.path.exists(excel_file_name_preprocessed_path): + df = pd.read_csv(excel_file_name_preprocessed_path) + else: + df = pd.read_csv(excel_file_name_path) + + datasets_types_PipelineJSON_path = os.path.join( + PIPELINE_PATH + "/dataset_types_pipeline.json" + ) + datasets_types_pipeline = pipeline.PipelineJSON( + datasets_types_PipelineJSON_path + ) + dataset_type = datasets_types_pipeline.read_from_json([df_name]) + + if type(dataset_type) is list: + dataset_type = dataset_type[0] + + df_id = request.session.get("cfrow_id") + if dataset_type == "tabular": + + # get row + features_to_vary = json.loads(request.POST.get("features_to_vary")) + + row = df.iloc[[int(df_id)]] + + # not preprocessed + notpre_df = pd.read_csv(excel_file_name_path) + notpre_row = notpre_df.iloc[[int(df_id)]] + + # if feature_to_vary has a categorical column then I cannot just + # pass that to dice since the trained model does not contain the + # categorical column but the one-hot-encoded sub-columns + features_to_vary = methods.update_column_list_with_one_hot_columns( + notpre_df, df, features_to_vary + ) + + # pipeline path + json_path = os.path.join(PIPELINE_PATH, f"{df_name}" + "/pipeline.json") + + # load pipeline data + jsonFile = pipeline.PipelineJSON(json_path) + class_label = jsonFile.read_from_json( + ["classifier", model_name, "class_label"] + ) # data becomes a dictionary + + # number of counterfactuals + # (TBD) input field value as parameter + # in ajax + num_counterfactuals = 5 + le = LabelEncoder() + notpre_df[class_label] = le.fit_transform(notpre_df[class_label]) + + continuous_features = methods.get_continuous_features(df) + non_continuous_features = methods.get_non_continuous_features(df) + + # load used classifier + clf = joblib.load(model_name_path + model_name + ".sav") + + try: + # Set up the executor to run the function in a separate thread + with concurrent.futures.ThreadPoolExecutor() as executor: + # Submit the function to the executor + future = executor.submit( + methods.counterfactuals, + row, + clf, + df, + class_label, + continuous_features, + num_counterfactuals, + features_to_vary, + ) + # Wait for the result with a timeout of 10 seconds + counterfactuals = future.result(timeout=10) + print("Counterfactuals computed successfully!") + except concurrent.futures.TimeoutError: + message = ( + "It seems like it took more than expected. Refresh and try again..." + ) + context = {"message": message} + + if counterfactuals: + cf_df = counterfactuals[0].final_cfs_df + counterfactuals[0].final_cfs_df.to_csv( + model_name_path + "counterfactuals.csv", index=False + ) + + # get coordinates of the clicked point (saved during 'click' event) + clicked_point = request.session.get("clicked_point") + clicked_point_df = pd.DataFrame( + { + "0": clicked_point[0], + "1": clicked_point[1], + f"{class_label}": row[class_label].astype(str), + } + ) + + # tSNE + cf_df = pd.read_csv(model_name_path + "counterfactuals.csv") + model_name_dir_path = os.path.join(PIPELINE_PATH + f"{df_name}") + tsne_path_to_augment = model_name_path + "tsne.sav" + + tsne = methods.generateAugmentedTSNE( + df, + cf_df, + num_counterfactuals, + clicked_point_df, + tsne_path_to_augment, + class_label, + ) + + tsne.update_layout( + # Modern Legend Design + legend=dict( + x=0.85, + y=0.95, + xanchor="right", + yanchor="top", + bgcolor="rgba(0,0,0,0.05)", # Transparent black background for a sleek look + bordercolor="rgba(0,0,0,0.1)", # Soft border for separation + borderwidth=1, + font=dict( + size=12, color="#333" + ), # Modern grey font color for text + ), + # Tight Margins for a Focused Plot Area + margin=dict( + l=20, r=20, t=40, b=40 + ), # Reduced margins for a cleaner look + # Axis Titles and Labels: Minimalist Design + xaxis=dict( + title_font=dict( + size=14, color="#555" + ), # Medium grey color for axis title + tickfont=dict( + size=11, color="#777" + ), # Light grey color for tick labels + showline=True, + linecolor="rgba(0,0,0,0.15)", # Subtle line color for axis lines + zeroline=False, # Hide the zero line for a cleaner design + showgrid=False, # No grid lines for a modern look + ), + yaxis=dict( + title_font=dict(size=14, color="#555"), + tickfont=dict(size=11, color="#777"), + showline=True, + linecolor="rgba(0,0,0,0.15)", + zeroline=False, + showgrid=False, + ), + # Sleek Background Design + plot_bgcolor="white", # Crisp white background for a modern touch + paper_bgcolor="white", # Ensure the entire background is uniform + # Title: Modern Font and Centered + title=dict( + text="t-SNE Visualization of Data", + font=dict( + size=18, color="#333", family="Arial, sans-serif" + ), # Modern font style + x=0.5, + xanchor="center", + yanchor="top", + pad=dict(t=10), # Padding to give the title breathing space + ), + ) + + pickle.dump(tsne, open(model_name_path + "tsne_cfs.sav", "wb")) + + context = { + "dataset_type": dataset_type, + "model_name": model_name, + "tsne": tsne.to_html(), + "num_counterfactuals": num_counterfactuals, + "default_counterfactual": "1", + "clicked_point": notpre_row.to_html(), + "counterfactual": cf_df.iloc[[1]].to_html(), + } + + else: + context = { + "dataset_type": dataset_type, + "model_name": model_name, + "message": "Please try again with different features.", + } + elif dataset_type == "timeseries": + model_name = request.POST["model_name"] + model_name_path = os.path.join( + PIPELINE_PATH + f"{df_name}/" + "trained_models/" + f"{model_name}/" + ) + path = model_name_path + if model_name == "glacier": + constraint = request.POST["constraint"] + path = os.path.join( + PIPELINE_PATH + + f"{df_name}/" + + "trained_models/" + + f"{model_name}/" + + f"{constraint}/" + ) + + X_test_path = os.path.join(model_name_path + "X_test.csv") + y_test_path = os.path.join(model_name_path + "y_test.npy") + y_pred_path = os.path.join(path + "y_pred.npy") + X_cf_path = os.path.join(path + "X_cf.npy") + cf_pred_path = os.path.join(path + "cf_pred.npy") + + X_test = pd.read_csv(X_test_path) + y_test = np.load(y_test_path) + y_pred = np.load(y_pred_path) + X_cf = np.load(X_cf_path) + cf_pred = np.load(cf_pred_path) + + if model_name != "glacier": + scaler = joblib.load(model_name_path + "/min_max_scaler.sav") + X_test = pd.DataFrame(scaler.inverse_transform(X_test)) + X_cf = scaler.inverse_transform(X_cf) + + fig = methods.ecg_plot_counterfactuals( + int(df_id), X_test, y_test, y_pred, X_cf, cf_pred + ) + + context = { + "df_name": df_name, + "fig": fig.to_html(), + "dataset_type": dataset_type, + } + elif action == "compute_cf": + model_name = request.POST.get("model_name") + if model_name == "glacier": + constraint_type = request.POST.get("constraint") + w_value = request.POST.get("w_value") + df_name = request.session.get("df_name") + + model_name_path = os.path.join( + PIPELINE_PATH + f"{df_name}/" + "trained_models/" + f"{model_name}/" + ) + model_name_path_constraint = model_name_path + f"{constraint_type}/" + if not os.path.exists(model_name_path_constraint): + os.makedirs(model_name_path_constraint) + + # https://github.com/wildboar-foundation/wildboar/blob/master/docs/guide/explain/counterfactuals.rst#id27 + classifier = joblib.load(model_name_path + "/classifier.sav") + + # pipeline path + json_path = os.path.join(PIPELINE_PATH, f"{df_name}" + "/pipeline.json") + # load pipeline data + jsonFile = pipeline.PipelineJSON(json_path) + autoencoder = jsonFile.read_from_json( + ["classifier", model_name, "autoencoder"] + ) + + experiment_dict = {"constraint": constraint_type, "w_value": w_value} + + # if "experiments" in pipeline_data["classifier"][model_name]: + # # if there exists key with value "experiments" + # keys = pipeline_data["classifier"][model_name]["experiments"].keys() + # last_key_int = int(list(keys)[-1]) + # last_key_int_incr_str = str(last_key_int + 1) + # else: + # last_key_int_incr_str = "0" + # experiment_key_dict = {"experiments": {last_key_int_incr_str: {}}} + # pipeline_data["classifier"][model_name].update(experiment_key_dict) + + # outter_dict = {last_key_int_incr_str: experiment_dict} + # pipeline_data["classifier"][model_name]["experiments"].update(outter_dict) + + if jsonFile.key_exists("experiments"): + keys = jsonFile.read_from_json( + ["classifier", model_name, "experiments"] + ).keys() + last_key_int = int(list(keys)[-1]) + last_key_int_incr_str = str(last_key_int + 1) + else: + last_key_int_incr_str = "0" + experiment_key_dict = {"experiments": {last_key_int_incr_str: {}}} + jsonFile.update_json( + ["classifier", model_name], experiment_key_dict + ) + + outter_dict = {last_key_int_incr_str: experiment_dict} + jsonFile.update_json( + ["classifier", model_name, "experiments"], outter_dict + ) + + if autoencoder == "Yes": + autoencoder = joblib.load(model_name_path + "/autoencoder.sav") + else: + autoencoder = None + + gc_compute_counterfactuals( + model_name_path, + model_name_path_constraint, + constraint_type, + [0.0001], + float(w_value), + 0.5, + classifier, + autoencoder, + ) + path = model_name_path_constraint + context = {"experiment_dict": experiment_dict} + elif action == "counterfactual_select": + + # if <select> element is used, and a specific counterfactual + # is inquired to be demonstrated: + df_name = request.session.get("df_name") + df_name = request.session.get("df_name") + if df_name == "upload": + df_name = request.session.get("df_name_upload_base_name") + + model_name = request.session.get("model_name") + model_name_path = os.path.join( + PIPELINE_PATH + f"{df_name}" + "/trained_models/" + model_name + ) + + excel_file_name_path = os.path.join( + PIPELINE_PATH + f"{df_name}" + "/" + df_name + ".csv" + ) + + # pipeline path + json_path = os.path.join(PIPELINE_PATH, f"{df_name}" + "/pipeline.json") + # load pipeline data + jsonFile = pipeline.PipelineJSON(json_path) + + class_label = jsonFile.read_from_json( + ["classifier", model_name, "class_label"] + ) + + # decode counterfactual to original values + preprocessing_list = jsonFile.read_from_json( + ["classifier", model_name, "preprocessing"] + ) + + df = pd.read_csv(excel_file_name_path) + cf_df = pd.read_csv(model_name_path + "/counterfactuals.csv") + cf_id = request.POST["cf_id"] + row = cf_df.iloc[[int(cf_id)]] + + if "id" in df.columns: + df = df.drop("id", axis=1) + + dec_row = methods.decode_cf( + df, row, class_label, model_name_path, preprocessing_list + ) + + fig = joblib.load(model_name_path + "/tsne_cfs.sav") + + # tsne stores data for each class in different data[] + # index. + # data[0] is class A + # data[1] is class B + # ... + # data[n-2] is counterfactuals + # data[n-1] is clicked point + + fig_data_array_length = len(fig.data) + for i in range(fig_data_array_length - 2): + fig.data[i].update( + opacity=0.3, + ) + + # last one, data[n-1], contains clicked point + l = fig.data[fig_data_array_length - 1] + clicked_id = -1 + for clicked_id, item in enumerate(list(zip(l.x, l.y))): + if math.isclose( + item[0], request.session.get("clicked_point")[0] + ) and math.isclose(item[1], request.session.get("clicked_point")[1]): + break + + # data[n-2] contains counterfactuals + fig.data[fig_data_array_length - 2].update( + selectedpoints=[int(cf_id)], + unselected=dict( + marker=dict( + opacity=0.3, + ) + ), + ) + + fig.data[fig_data_array_length - 1].update( + selectedpoints=[clicked_id], + unselected=dict( + marker=dict( + opacity=0.3, + ) + ), + ) + + if "id" in df.columns: + df = df.drop("id", axis=1) + + # order the columns + dec_row = dec_row[df.columns] + clicked_point_row_id = request.session.get("cfrow_id") + + # return only the differences + dec_row = dec_row.reset_index(drop=True) + df2 = df.iloc[[int(clicked_point_row_id)]].reset_index(drop=True) + difference = dec_row.loc[ + :, + [ + methods.compare_values(dec_row[col].iloc[0], df2[col].iloc[0]) + for col in dec_row.columns + ], + ] + + merged_df = pd.concat([df2[difference.columns], difference], ignore_index=True) + + context = { + "row": merged_df.to_html(index=False), + "fig": fig.to_html(), + } + elif action == "class_label_selection": + + df_name = request.session.get("df_name") + + if df_name == "upload": + df_name = request.session["df_name_upload_base_name"] + + datasets_types_PipelineJSON_path = os.path.join( + PIPELINE_PATH + "/dataset_types_pipeline.json" + ) + + dataset_type_json = pipeline.PipelineJSON(datasets_types_PipelineJSON_path) + + dataset_type = dataset_type_json.read_from_json([df_name]) + + if isinstance(dataset_type, list): + dataset_type = dataset_type[0] + + # preprocessed_path + excel_file_name_preprocessed_path = os.path.join( + PIPELINE_PATH + f"{df_name}" + "/" + df_name + "_preprocessed" + ".csv" + ) + + excel_file_name_path = os.path.join( + PIPELINE_PATH + f"{df_name}" + "/" + df_name + ".csv" + ) + + # which model is being used during that session + model_name = request.POST.get("model_name") + + model_name_path = os.path.join( + PIPELINE_PATH + f"{df_name}" + "/trained_models/" + model_name + ) + + X_test_path = os.path.join( + PIPELINE_PATH + + f"{df_name}" + + "/trained_models" + + f"/{model_name}" + + "/X_test.csv" + ) + y_test_path = os.path.join( + PIPELINE_PATH + + f"{df_name}" + + "/trained_models" + + f"/{model_name}" + + "/y_test.npy" + ) + + X_test = pd.read_csv(X_test_path) + y_test = np.load(y_test_path) + + if model_name != "glacier": + scaler = joblib.load(model_name_path + "/min_max_scaler.sav") + X_test = pd.DataFrame(scaler.inverse_transform(X_test)) + + if dataset_type == "timeseries": + class_label = request.POST.get("class_label") + cfrow_id = request.POST.get("cfrow_id") + + class_label = ( + int(class_label) + if class_label.isdigit() + else ( + float(class_label) + if class_label.replace(".", "", 1).isdigit() + else class_label + ) + ) + + fig, index = methods.get_ecg_entry( + X_test, y_test, int(cfrow_id), class_label + ) + request.session["cfrow_id"] = index + request.session["class_label"] = class_label + context = {"fig": fig.to_html(), "dataset_type": dataset_type} + return HttpResponse(json.dumps(context), status=status) \ No newline at end of file diff --git a/base/handlers/ajaxHomeHandler.py b/base/handlers/ajaxHomeHandler.py new file mode 100644 index 000000000..adcab037a --- /dev/null +++ b/base/handlers/ajaxHomeHandler.py @@ -0,0 +1,437 @@ +import base.pipeline as pipeline +import os +from dict_and_html import * +from .. import methods +from ..methods import PIPELINE_PATH +from django.core.files.storage import FileSystemStorage +import random +import base.pipeline as pipeline +import shutil +import json +from django.shortcuts import HttpResponse + +def handler(action, request): + status = 200 + if action == "upload_dataset": + + uploaded_file = request.FILES["excel_file"] # Get the file from request.FILES + dataset_type = request.POST.get("dataset_type") + + # action to add dataset when from radio button click + # add name of used dataframe in session for future use + request.session["df_name"] = "upload" + name = uploaded_file.name + + # Split the name and extension + base_name, extension = os.path.splitext(name) + request.session["df_name_upload_base_name"] = base_name + request.session["df_name_upload_extension"] = extension + + df_name = base_name + + df_name_path = os.path.join( + PIPELINE_PATH + f"{base_name}", + ) + + if not os.path.exists(df_name_path): + os.makedirs(df_name_path) + + fs = FileSystemStorage() # FileSystemStorage to save the file + + # Save the file with the new filename + fs = FileSystemStorage(location=df_name_path) + filename = fs.save(uploaded_file.name, uploaded_file) # Save file + + request.session["excel_file_name"] = df_name_path + + excel_file_name_path = os.path.join(PIPELINE_PATH + f"{base_name}" + "/" + name) + + df = methods.get_dataframe(excel_file_name_path) + + ## update the datasets_types json file + datasets_types_PipelineJSON_path = os.path.join( + PIPELINE_PATH + "dataset_types_pipeline.json" + ) + jsonFile = pipeline.PipelineJSON(datasets_types_PipelineJSON_path) + + # with open(datasets_types_PipelineJSON_path, "r") as jsonFile: + # datasets_types_PipelineJSON = pipeline.load( + # jsonFile + # ) # data becomes a dictionary + + jsonFile.append_to_json({df_name: [dataset_type, "uploaded"]}) + dataset_type = jsonFile.read_from_json([df_name])[0] + uploaded_files = jsonFile.get_keys_with_value("uploaded") + + # datasets_types_PipelineJSON[df_name] = dataset_type + # with open(datasets_types_PipelineJSON_path, "w") as file: + # pipeline.dump( + # datasets_types_PipelineJSON, file, indent=4 + # ) # Write with pretty print (indent=4) + + if df.columns.str.contains(" ").any(): + df.columns = df.columns.str.replace(" ", "_") + # if columns contain space + os.remove(excel_file_name_path) + df.to_csv(excel_file_name_path, index=None) + df = methods.get_dataframe(excel_file_name_path) + + if "id" in df.columns: + df.drop(["id"], axis=1, inplace=True) + df.to_csv(excel_file_name_path, index=False) + + # if dataset_type == "tabular": + # # tabular datasets + # features = df.columns + # feature1 = df.columns[3] + # feature2 = df.columns[2] + + # labels = list(df.select_dtypes(include=["object", "category"]).columns) + # # Find binary columns (columns with only two unique values, including numerics) + # binary_columns = [col for col in df.columns if df[col].nunique() == 2] + + # # Combine categorical and binary columns into one list + # labels = list(set(labels + binary_columns)) + + # label = random.choice(labels) + # fig = methods.stats( + # excel_file_name_path, + # dataset_type, + # None, + # None, + # feature1, + # feature2, + # label, + # df_name, + # ) + + # # tabular dataset + # request.session["data_to_display"] = df[:10].to_html() + # request.session["features"] = list(features) + # request.session["feature1"] = feature1 + # request.session["feature2"] = feature2 + # request.session["labels"] = list(labels) + # request.session["curlabel"] = label + # request.session["fig"] = fig + + # context = { + # "dataset_type": dataset_type, + # "data_to_display": df[:10].to_html(), + # "fig": fig, + # "features": list(features), # error if not a list + # "feature1": feature1, + # "feature2": feature2, + # "labels": list(labels), + # "curlabel": label, + # "df_name": request.session["df_name"], + # } + # elif dataset_type == "timeseries": + # fig, fig1 = methods.stats(excel_file_name_path, dataset_type) + # request.session["fig"] = fig + # request.session["fig1"] = fig1 + # context = { + # "dataset_type": dataset_type, + # "df_name": df_name, + # "fig": fig, + # "fig1": fig1, + # } + + context = {"dataset_type": dataset_type, "df_name": df_name} + context.update({"uploaded_files": uploaded_files}) + + if dataset_type == "timeseries": + target_labels = list(df.iloc[:, -1].unique()) + context.update({"target_labels": target_labels}) + + request.session["context"] = context + elif action == "delete_uploaded_file": + dataset_name = request.POST.get("dataset_name") + dataset_path = os.path.join(PIPELINE_PATH + f"/{dataset_name}") + + # pipeline path + datasets_types_pipeline_path = os.path.join( + PIPELINE_PATH + "/dataset_types_pipeline.json" + ) + # load pipeline data + datasets_types_pipeline = pipeline.PipelineJSON(datasets_types_pipeline_path) + datasets_types_pipeline.delete_key([dataset_name]) + + request.FILES["excel_file"] = None + request.session["df_name"] = None + + # check if there exist uploaded files + uploaded_files = datasets_types_pipeline.get_keys_with_value( + "uploaded" + ) + if uploaded_files == []: + uploaded_files = None + try: + shutil.rmtree(dataset_path) + except Exception as error: + print(error) + + context = {"uploaded_files": uploaded_files} + elif action == "dataset" or action == "uploaded_datasets": + + # action to add dataset when from radio button click + name = request.POST.get("df_name") + request.session["df_name"] = name + + # if name == "upload": + # name = request.session.get("df_name_upload_base_name") + + if action == "dataset" and name == "upload": + request.session["upload"] = 1 + context = {"upload": 1} + else: + + if name == "timeseries": + name = request.session.get("df_name") + + excel_file_name_path = os.path.join( + PIPELINE_PATH + f"{name}" + "/" + name + ".csv", + ) + + datasets_types_PipelineJSON_path = os.path.join( + PIPELINE_PATH + "/dataset_types_pipeline.json" + ) + datasets_types_PipelineJSON = pipeline.PipelineJSON( + datasets_types_PipelineJSON_path + ) + dataset_type = datasets_types_PipelineJSON.read_from_json([name]) + uploaded_files = datasets_types_PipelineJSON.get_keys_with_value( + "uploaded" + ) + + if request.POST.get("df_name") == "upload" or action == "uploaded_datasets": + if type(dataset_type) is list: + dataset_type = dataset_type[0] + + if request.POST.get("df_name") != "upload" or action == "uploaded_datasets": + if os.path.exists(excel_file_name_path): + df = methods.get_dataframe(excel_file_name_path) + df.columns = df.columns.str.replace(" ", "_") + request.session["excel_file_name"] = excel_file_name_path + + json_path = os.path.join( + PIPELINE_PATH + f"{name}" + "/pipeline.json" + ) + if not os.path.exists(json_path): + PipelineJSON = pipeline.PipelineJSON(json_path) + PipelineJSON.append_to_json({"name": name}) + + if "tabular" == dataset_type: + + if "id" in df.columns: + df.drop(["id"], axis=1, inplace=True) + df.to_csv(excel_file_name_path, index=False) + + # tabular datasets + features = df.columns + feature1 = df.columns[3] + feature2 = df.columns[2] + label = "" + + labels = list( + df.select_dtypes(include=["object", "category"]).columns + ) + # Find binary columns (columns with only two unique values, including numerics) + binary_columns = [ + col for col in df.columns if df[col].nunique() == 2 + ] + + # Combine categorical and binary columns into one list + labels = list(set(labels + binary_columns)) + label = random.choice(labels) + fig = methods.stats( + excel_file_name_path, + dataset_type, + feature1=feature1, + feature2=feature2, + label=label, + ) + + # tabular dataset + request.session["data_to_display"] = df[:10].to_html() + request.session["features"] = list(features) + request.session["feature1"] = feature1 + request.session["feature2"] = feature2 + request.session["labels"] = list(labels) + request.session["curlabel"] = label + request.session["fig"] = fig + + context = { + "dataset_type": dataset_type, + "data_to_display": df[:10].to_html(), + "fig": fig, + "features": list(features), # error if not a list + "feature1": feature1, + "feature2": feature2, + "labels": list(labels), + "curlabel": label, + "uploaded_files": list(uploaded_files), + } + elif dataset_type == "timeseries": + + json_path = os.path.join( + PIPELINE_PATH, f"{name}" + "/pipeline.json" + ) + jsonFile = pipeline.PipelineJSON(json_path) + + pos = jsonFile.read_from_json(["pos"]) + neg = jsonFile.read_from_json(["neg"]) + + fig, fig1 = methods.stats( + excel_file_name_path, + dataset_type, + int(pos), + int(neg), + None, + None, + None, + name=name, + ) + # timeseries + request.session["fig"] = fig + request.session["fig1"] = fig1 + context = { + "fig": fig, + "fig1": fig1, + "dataset_type": dataset_type, + } + else: + context = {"uploaded_files": list(uploaded_files)} + else: + context = {} + + if ( + action == "uploaded_datasets" + and "upload" in request.session + and request.session["upload"] == 1 + ): + request.session["upload"] = 1 + context.update({"upload": 1, "df_name": name}) + print(name) + else: + request.session["upload"] = 0 + elif action == "dataset_charts": + df_name = request.POST.get("df_name") + request.session["df_name"] = df_name + context = {} + elif action == "select_class_labels_for_uploaded_timeseries": + name = request.session["df_name"] + + if name == "upload": + name = request.session["df_name_upload_base_name"] + + pos = request.POST.get("positive_label") + neg = request.POST.get("negative_label") + + json_path = os.path.join(PIPELINE_PATH, f"{name}" + "/pipeline.json") + jsonFile = pipeline.PipelineJSON(json_path) + + jsonFile.append_to_json({"name": name}) + jsonFile.append_to_json({"pos": pos}) + jsonFile.append_to_json({"neg": neg}) + + context = {} + elif action == "timeseries-dataset": + + # action to add dataset when from radio button click + name = request.POST.get("timeseries_dataset") + + # add name of used dataframe in session for future use + request.session["df_name"] = name + excel_file_name_path = os.path.join( + PIPELINE_PATH + f"{name}" + "/" + name + ".csv", + ) + datasets_types_PipelineJSON_path = os.path.join( + PIPELINE_PATH + "/dataset_types_pipeline.json" + ) + datasets_types_PipelineJSON = pipeline.PipelineJSON( + datasets_types_PipelineJSON_path + ) + if os.path.exists(excel_file_name_path): + + dataset_type = datasets_types_PipelineJSON.read_from_json([name]) + + df = methods.get_dataframe(excel_file_name_path) + df.columns = df.columns.str.replace(" ", "_") + request.session["excel_file_name"] = excel_file_name_path + + # find the available pre trained datasets + # check the pipeline file + json_path = os.path.join(PIPELINE_PATH, f"{name}" + "/pipeline.json") + jsonFile = pipeline.PipelineJSON(json_path) + + preprocessing_info = {"name": name} + dataset_camel = methods.convert_to_camel_case(name) + if "Ecg" in dataset_camel: + dataset_camel = dataset_camel.replace("Ecg", "ECG") + experiment = methods.fetch_line_by_dataset( + PIPELINE_PATH + "/glacier_experiments.txt", + dataset_camel, + ) + if experiment is not None: + stripped_arguments = methods.extract_arguments_from_line(experiment) + + indices_to_keys = { + 1: "pos", + 2: "neg", + } + + # Create a dictionary by fetching items from the list at the specified indices + inner_dict = { + key: stripped_arguments[index] for index, key in indices_to_keys.items() + } + preprocessing_info.update(inner_dict) + jsonFile.append_to_json(preprocessing_info) + + pos = inner_dict["pos"] + neg = inner_dict["neg"] + fig, fig1 = methods.stats( + excel_file_name_path, dataset_type, int(pos), int(neg), name=name + ) + # timeseries + request.session["fig"] = fig + request.session["fig1"] = fig1 + context = {"fig": fig, "fig1": fig1, "dataset_type": dataset_type} + else: + context = {} + elif action == "stat": + + name = request.session.get("df_name") + datasets_types_PipelineJSON_path = os.path.join( + PIPELINE_PATH + "/dataset_types_pipeline.json" + ) + jsonFile = pipeline.PipelineJSON(datasets_types_PipelineJSON_path) + dataset_type = jsonFile.read_from_json([name]) + + if type(dataset_type) is list: + dataset_type = dataset_type[0] + + file_path = os.path.join( + PIPELINE_PATH + f"{name}" + "/" + name + ".csv", + ) + if dataset_type == "tabular": + feature1 = request.POST.get("feature1") + feature2 = request.POST.get("feature2") + label = request.POST.get("label") + else: + feature1 = request.POST.get("feature1") + feature2 = [] + label = [] + + fig = methods.stats( + file_path, + dataset_type, + None, + None, + feature1=feature1, + feature2=feature2, + label=label, + ) + context = { + "fig": fig, + } + return HttpResponse(json.dumps(context), status=status) \ No newline at end of file diff --git a/base/handlers/ajaxTrainHandler.py b/base/handlers/ajaxTrainHandler.py new file mode 100644 index 000000000..38dc1d782 --- /dev/null +++ b/base/handlers/ajaxTrainHandler.py @@ -0,0 +1,464 @@ +import base.pipeline as pipeline +import pickle, os +import pandas as pd +import json +from sklearn.preprocessing import LabelEncoder +from dict_and_html import * +from .. import methods +from ..methods import PIPELINE_PATH +import numpy as np +from collections import defaultdict +import base.pipeline as pipeline +import json +from django.shortcuts import HttpResponse + +def handler(action, request): + status = 200 + if action == "train": + # train a new model + # parameters sent via ajax + model_name = request.POST.get("model_name") + df_name = request.session.get("df_name") + + # dataframe name + if df_name == "upload": + df_name = request.session.get("df_name_upload_base_name") + + request.session["model_name"] = model_name + test_set_ratio = "" + if "test_set_ratio" in request.POST: + test_set_ratio = request.POST.get("test_set_ratio") + + datasets_types_PipelineJSON_path = os.path.join( + PIPELINE_PATH + "/dataset_types_pipeline.json" + ) + jsonFile = pipeline.PipelineJSON(datasets_types_PipelineJSON_path) + dataset_type = jsonFile.read_from_json([df_name]) + + if type(dataset_type) is list: + dataset_type = dataset_type[0] + + if "array_preprocessing" in request.POST: + array_preprocessing = request.POST.get("array_preprocessing") + + if dataset_type == "tabular": + class_label = request.POST.get("class_label") + preprocessing_info = { + "preprocessing": array_preprocessing, + "test_set_ratio": test_set_ratio, + "explainability": {"technique": "dice"}, + "class_label": class_label, + } + elif dataset_type == "timeseries": + if model_name != "glacier": + preprocessing_info = { + "preprocessing": array_preprocessing, + "test_set_ratio": test_set_ratio, + "explainability": {"technique": model_name}, + } + else: + # Path to the Bash script + autoencoder = request.POST.get("autoencoder") + preprocessing_info = { + "autoencoder": autoencoder, + "explainability": {"technique": model_name}, + } + + # absolute excel_file_name_path + excel_file_name_path = os.path.join( + PIPELINE_PATH + f"{df_name}" + "/" + df_name + ".csv" + ) + + # load paths + # absolute excel_file_preprocessed_path + excel_file_name_preprocessed_path = os.path.join( + PIPELINE_PATH, + f"{df_name}" + "/" + df_name + "_preprocessed" + ".csv", + ) + + json_path = os.path.join(PIPELINE_PATH + f"{df_name}" + "/pipeline.json") + jsonFile = pipeline.PipelineJSON(json_path) + # save the plots for future use + # folder path: pipelines/<dataset name>/trained_models/<model_name>/ + + model_name_path = os.path.join( + PIPELINE_PATH + f"{df_name}" + "/trained_models/" + model_name + ) + + model_name_dir_path = os.path.join(PIPELINE_PATH + f"{df_name}") + + if os.path.exists(excel_file_name_preprocessed_path) == True: + # if preprocessed_file exists + # delete it and do preprocessing again + # maybe should optimize it for cases + # where the preprocessing is the same with + # the one applited on the existing file + os.remove(excel_file_name_preprocessed_path) + + # generate filename + idx = excel_file_name_path.index(".") + excel_file_name_preprocessed = ( + df_name[:idx] + "_preprocessed" + excel_file_name_path[idx:] + ) + + # save file for preprocessing + preprocess_df = pd.read_csv(excel_file_name_path) + request.session["excel_file_name_preprocessed"] = excel_file_name_preprocessed + + # make the dir + if not os.path.exists(model_name_path): + os.makedirs(model_name_path) + + try: + if dataset_type == "tabular": + le = LabelEncoder() + preprocess_df[class_label] = le.fit_transform( + preprocess_df[class_label] + ) + + if "array_preprocessing" in request.POST: + preprocess_df = methods.preprocess( + preprocess_df, + array_preprocessing, + excel_file_name_path, + dataset_type, + model_name_path, + class_label, + ) + elif dataset_type == "timeseries": + + pos = jsonFile.read_from_json(["pos"]) + neg = jsonFile.read_from_json(["neg"]) + pos_label, neg_label = 1, 0 + + if pos != pos_label: + preprocess_df.iloc[:, -1] = preprocess_df.iloc[:, -1].apply( + lambda x: pos_label if x == int(pos) else x + ) + if neg != neg_label: + preprocess_df.iloc[:, -1] = preprocess_df.iloc[:, -1].apply( + lambda x: neg_label if x == int(neg) else x + ) + if "array_preprocessing" in request.POST: + preprocess_df = methods.preprocess( + preprocess_df, + array_preprocessing, + excel_file_name_path, + dataset_type, + model_name_path, + ) + + pca = methods.generatePCA(preprocess_df) + + # TSNE + if dataset_type == "tabular": + tsne, projections = methods.generateTSNE( + preprocess_df, dataset_type, class_label + ) + else: + tsne, projections = methods.generateTSNE(preprocess_df, dataset_type) + + if dataset_type == "tabular": + # training + feature_importance, classification_report, importance_dict = ( + methods.training( + preprocess_df, + model_name, + float(test_set_ratio), + class_label, + dataset_type, + df_name, + model_name_path, + ) + ) + + # feature importance on the original categorical columns (if they exist) + df = pd.read_csv(excel_file_name_path) + df = df.drop(class_label, axis=1) + + # Initialize a dictionary to hold aggregated feature importances + categorical_columns = methods.get_categorical_features(df) + + if categorical_columns != []: + aggregated_importance = {} + encoded_columns = methods.update_column_list_with_one_hot_columns( + df, preprocess_df, df.columns + ) + + feature_mapping = defaultdict(list) + for col in encoded_columns: + for original_col in categorical_columns: + if col.startswith(original_col + "_"): + feature_mapping[original_col].append(col) + break + else: + feature_mapping[col].append( + col + ) # If no match, map to itself + + # Aggregate the feature importances + for original_feature, encoded_columns in feature_mapping.items(): + if encoded_columns: # Check if encoded_columns is not empty + if original_feature not in encoded_columns: + aggregated_importance[original_feature] = np.sum( + [ + importance_dict.get(col, 0) + for col in encoded_columns + ] + ) + else: + aggregated_importance[original_feature] = ( + importance_dict.get(original_feature, 0) + ) + + importance_df = pd.DataFrame( + { + "feature": list(aggregated_importance.keys()), + "importance": list(aggregated_importance.values()), + } + ) + + importance_df.to_csv( + model_name_path + "/feature_importance_df.csv", index=None + ) + else: + # if no categorical columns + # Combine feature names with their respective importance values + feature_importance_df = pd.DataFrame( + { + "feature": importance_dict.keys(), + "importance": importance_dict.values(), + } + ) + + feature_importance_df.to_csv( + model_name_path + "/feature_importance_df.csv", index=None + ) + + # save some files + pickle.dump( + classification_report, + open(model_name_path + "/classification_report.sav", "wb"), + ) + pickle.dump( + feature_importance, + open(model_name_path + "/feature_importance.sav", "wb"), + ) + pickle.dump(le, open(model_name_path + "/label_encoder.sav", "wb")) + + context = { + "dataset_type": dataset_type, + "pca": pca.to_html(), + "class_report": classification_report.to_html(), + "feature_importance": feature_importance.to_html(), + } + elif dataset_type == "timeseries": + + path = model_name_path + dataset_camel = methods.convert_to_camel_case(df_name) + if "Ecg" in dataset_camel: + dataset_camel = dataset_camel.replace("Ecg", "ECG") + + experiment = methods.fetch_line_by_dataset( + PIPELINE_PATH + "/glacier_experiments.txt", + dataset_camel, + ) + + if experiment is not None: + stripped_arguments = methods.extract_arguments_from_line(experiment) + + if model_name == "glacier": + classification_report = methods.training( + preprocess_df, + model_name, + float(test_set_ratio) if test_set_ratio != "" else 0, + "", + dataset_type, + df_name, + path, + autoencoder, + stripped_arguments, + ) + else: + classification_report = methods.training( + preprocess_df, + model_name, + float(test_set_ratio) if test_set_ratio != "" else 0, + "", + dataset_type, + df_name, + path, + ) + + pickle.dump( + classification_report, + open(path + "/classification_report.sav", "wb"), + ) + + context = { + "dataset_type": dataset_type, + "pca": pca.to_html(), + "tsne": tsne.to_html(), + "class_report": classification_report.to_html(), + } + + # save the plots + pickle.dump(tsne, open(model_name_path + "/tsne.sav", "wb")) + pickle.dump(pca, open(model_name_path + "/pca.sav", "wb")) + + # save projections file for future use + with open(model_name_path + "/tsne_projections.json", "w") as f: + json.dump(projections.tolist(), f, indent=2) + + if jsonFile.key_exists("classifier"): + temp_json = {model_name: preprocessing_info} + jsonFile.update_json(["classifier"], temp_json) + else: + temp_jason = { + "preprocessed_name": df_name + "_preprocessed.csv", + "classifier": {model_name: preprocessing_info}, + } + jsonFile.append_to_json(temp_jason) + + classifier_data = jsonFile.read_from_json(["classifier", model_name]) + classifier_data_html = dict_and_html(classifier_data) + context.update({"classifier_data": classifier_data_html}) + preprocess_df.to_csv(excel_file_name_preprocessed_path, index=False) + status = 200 + + except FileNotFoundError as e: + methods.remove_dir_and_empty_parent(model_name_path) + context = methods.format_error_context( + e, "File error. Please check if all required files are available." + ) + status = 400 + + except PermissionError as e: + methods.remove_dir_and_empty_parent(model_name_path) + context = methods.format_error_context( + e, "Permission error. Ensure appropriate file permissions." + ) + status = 400 + + except KeyError as e: + methods.remove_dir_and_empty_parent(model_name_path) + context = methods.format_error_context( + e, f"Key error. Missing expected key {str(e)}. Verify dataset and configuration settings." + ) + status = 400 + + except ValueError as e: + methods.remove_dir_and_empty_parent(model_name_path) + context = methods.format_error_context( + e, "Data error. Please verify the data format and preprocessing steps." + ) + status = 400 + + except TypeError as e: + methods.remove_dir_and_empty_parent(model_name_path) + context = methods.format_error_context( + e, "Type error. Check for data type compatibility in operations." + ) + status = 400 + + except Exception as e: + methods.remove_dir_and_empty_parent(model_name_path) + context = methods.format_error_context( + e, "An unexpected error occurred. Please review the code and data." + ) + status = 400 + elif action == "delete_pre_trained": + + df_name = request.session["df_name"] + model_name = request.POST.get("model_name") + model_name_path = os.path.join( + PIPELINE_PATH + f"{df_name}" + "/trained_models/" + model_name + ) + + print(model_name_path) + + excel_file_name_preprocessed_path = os.path.join( + PIPELINE_PATH, + f"{df_name}" + "/" + df_name + "_preprocessed" + ".csv", + ) + try: + # Check if the file exists + if os.path.exists(excel_file_name_preprocessed_path): + # Delete the file + os.remove(excel_file_name_preprocessed_path) + # print(f"File '{excel_file_name_preprocessed_path}' has been deleted successfully.") + else: + print(f"File '{excel_file_name_preprocessed_path}' does not exist.") + except Exception as e: + print(f"An error occurred while deleting the file: {e}") + + json_path = os.path.join(PIPELINE_PATH + f"{df_name}" + "/pipeline.json") + jsonFile = pipeline.PipelineJSON(json_path) + jsonFile.delete_key(["classifier", model_name]) + + methods.remove_dir_and_empty_parent(model_name_path) + # load paths + # absolute excel_file_preprocessed_path + + if not jsonFile.key_exists("classifier"): + # pre trained models do not exist + # check if dataset directory exists + df_dir = os.path.join(PIPELINE_PATH + f"{df_name}") + if not os.path.exists(df_dir): + df_name = None + + context = { + "df_name": df_name, + "available_pretrained_models_info": [], + } + else: + # if it exists + # check the section of "classifiers" + # folder path + available_pretrained_models = jsonFile.read_from_json( + ["classifier"] + ).keys() + + available_pretrained_models_info = ( + methods.create_tuple_of_models_text_value( + available_pretrained_models + ) + ) + context = { + "df_name": df_name, + "available_pretrained_models_info": available_pretrained_models_info, + } + elif action == "discard_model": + name = request.session["df_name"] + model_name = request.session["model_name"] + model_name_path = os.path.join( + PIPELINE_PATH + f"{name}" + "/trained_models/" + model_name + ) + # should delete model folder + # should delete classifier from json + # should delete preprocessed path too + methods.remove_dir_and_empty_parent(model_name_path) + # load paths + # absolute excel_file_preprocessed_path + excel_file_name_preprocessed_path = os.path.join( + PIPELINE_PATH, + f"{name}" + "/" + name + "_preprocessed" + ".csv", + ) + try: + # Check if the file exists + if os.path.exists(excel_file_name_preprocessed_path): + # Delete the file + os.remove(excel_file_name_preprocessed_path) + # print(f"File '{excel_file_name_preprocessed_path}' has been deleted successfully.") + else: + print(f"File '{excel_file_name_preprocessed_path}' does not exist.") + except Exception as e: + print(f"An error occurred while deleting the file: {e}") + + json_path = os.path.join(PIPELINE_PATH + f"{name}" + "/pipeline.json") + jsonFile = pipeline.PipelineJSON(json_path) + jsonFile.delete_key(["classifier",model_name]) + + context = {} + + return HttpResponse(json.dumps(context), status=status) \ No newline at end of file diff --git a/base/static/css/sb-admin-2.css b/base/static/css/sb-admin-2.css index 440295341..119f169b8 100755 --- a/base/static/css/sb-admin-2.css +++ b/base/static/css/sb-admin-2.css @@ -4884,30 +4884,6 @@ input[type="button"].btn-block { max-height: 100%; } -.loader { - width: 48px; - height: 48px; - border: 5px solid #fff; - border-bottom-color: #ff3d00; - border-radius: 50%; - display: inline-block; - box-sizing: border-box; - animation: rotation 1s linear infinite; -} - -.loader_small { - width: 22px; - height: 22px; - border: 3px solid #fff; - margin-left: 5px; - border-bottom-color: #ff3d00; - border-radius: 50%; - display: inline-block; - box-sizing: border-box; - animation: rotation 1s linear infinite; - position: fixed; -} - @keyframes rotation { 0% { transform: rotate(0deg); @@ -12097,22 +12073,6 @@ button.btn-primary:hover { margin-top: 30px; } -/* Loader styling */ -.loader { - border: 4px solid #f3f3f3; - border-top: 4px solid #007bff; - border-radius: 50%; - width: 25px; - height: 25px; - animation: spin 1s linear infinite; -} - -/* Keyframes for loader spin */ -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - /* Preprocessing checkboxes styling */ .form-check-inline .form-check-label { margin-left: 5px; @@ -12529,7 +12489,7 @@ h6 { animation: fadeIn 0.8s ease forwards; } -/* Loader Style */ +/* Existing Loader Spinner */ .loader { display: inline-block; width: 1.5rem; @@ -12541,10 +12501,45 @@ h6 { margin-left: 8px; } +/* Keyframes for spinner animation */ @keyframes spin { - to { transform: rotate(360deg); } + to { + transform: rotate(360deg); + } } +/* Loader Overlay */ +.loader-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(255, 255, 255, 0.8); /* Semi-transparent white background */ + display: flex; + justify-content: center; + align-items: center; + z-index: 10; /* Ensure it overlays the content */ +} + +/* Spinner Loader */ +.spinner-border { + width: 3rem; + height: 3rem; + border: 4px solid rgba(0, 0, 0, 0.1); + border-top-color: #007bff; /* Customize color */ + border-radius: 50%; + animation: spin 0.6s linear infinite; +} + +/* Keyframes for spinner animation */ +@keyframes spin { + to { + transform: rotate(360deg); + } +} + + /* Enhanced style for the modal trigger button */ .info-button { background: none; @@ -12586,4 +12581,401 @@ table th, .sticky-top-table table td { /* Hover effect for rows */ .sticky-top-table table tbody tr:hover { background-color: #eaf1f8; /* Soft highlight on hover */ -} \ No newline at end of file +} + +/* Modal Styling */ +#deleteFileModal .modal-content { + border-radius: 4px; + padding: 0; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} +#deleteFileModal .modal-header { + padding: 0.5rem 1rem; + border-bottom: none; +} +#deleteFileModal .modal-title { + font-size: 1rem; + color: #d9534f; +} +#deleteFileModal .modal-body { + font-size: 0.9rem; + color: #444; +} + +/* Custom Buttons */ +.custom-btn-secondary, +.custom-btn-danger { + font-size: 0.85rem; + padding: 0.4rem 1rem; + border-radius: 2px; + cursor: pointer; + transition: background-color 0.2s; +} + +.custom-btn-secondary { + color: #555; + background-color: #f8f9fa; + border: 1px solid #ddd; +} + +.custom-btn-secondary:hover { + background-color: #e2e6ea; +} + +.custom-btn-danger { + color: #fff; + background-color: #d9534f; + border: 1px solid transparent; +} + +.custom-btn-danger:hover { + background-color: #c9302c; +} + +/* Delete icon next to file names */ +.delete-file-icon { + font-size: 1.2rem; + color: #bbb; + cursor: pointer; + transition: color 0.2s; +} +.delete-file-icon:hover { + color: #d9534f; +} + +.custom-alert { + display: flex; + align-items: center; + padding: 5px 10px; + border-radius: 8px; + background-color: #eafaf1; + color: #28a745; + font-size: 14px; + max-width: 250px; + opacity: 0; + transform: translateY(-10px); + transition: opacity 0.4s ease, transform 0.4s ease; +} + +.custom-alert.show { + opacity: 1; + transform: translateY(0); +} + +.loader i { + font-size: 1.2em; + color: #007bff; +} + +.card-header h6 { + font-size: 1rem; + font-weight: 600; + margin-right: auto; +} + +.card-footer { + font-size: 0.85rem; + color: #6c757d; +} + +/* Add to your CSS file */ +.blur-effect { + transition: filter 0.3s ease, opacity 0.3s ease; +} + +/* Ensure the modal respects the maximum height */ +#modelAnalysisModal .modal-content { + max-height: 80vh; /* Adjust the maximum height as needed */ + overflow-y: auto; /* Add vertical scrolling when content exceeds height */ +} + +/* Style for the modal body */ +#modelAnalysisModal .modal-body { + padding: 20px; /* Add some padding for better readability */ +} + +/* Optional: Keep the tabs navigation fixed at the top inside the modal */ +#modelAnalysisModal .nav-tabs { + position: sticky; + top: 0; + z-index: 1020; + background-color: #f8f9fa; /* Match with modal header background */ + border-bottom: 1px solid #dee2e6; +} + +/* Optional: Add smooth scrolling */ +#modelAnalysisModal .modal-content::-webkit-scrollbar { + width: 8px; +} + +#modelAnalysisModal .modal-content::-webkit-scrollbar-thumb { + background-color: #6c757d; /* Darker thumb for scrollbar */ + border-radius: 4px; +} + +#modelAnalysisModal .modal-content::-webkit-scrollbar-track { + background-color: #f8f9fa; /* Light track for scrollbar */ +} + +/* Make the modal footer fixed to the bottom of the modal */ +#modelAnalysisModal .modal-footer { + position: sticky; /* Keep it at the bottom of the modal body */ + bottom: 0; + z-index: 1050; /* Ensure it appears above the modal body content */ + background-color: #fff; /* Match the modal's background color */ + border-top: 1px solid #dee2e6; /* Optional: Add a top border */ + box-shadow: 0 -1px 5px rgba(0, 0, 0, 0.1); /* Optional: Add subtle shadow */ +} + +/* Adjust the modal body to account for the footer's height */ +#modelAnalysisModal .modal-body { + max-height: calc(80vh - 60px); /* Subtract the approximate footer height */ + overflow-y: auto; /* Enable scrolling if content exceeds height */ +} + + /* Minimal animations and transitions */ + .fade-in { + opacity: 0; + transform: translateY(20px); + transition: all 0.5s ease-in-out; + } + + .fade-in.visible { + opacity: 1; + transform: translateY(0); + } + + /* Button hover effect */ + .btn-outline-primary { + border: 2px solid #007bff; + color: #007bff; + background: none; + transition: all 0.3s ease-in-out; + } + + .btn-outline-primary:hover { + background: #007bff; + color: #fff; + transform: scale(1.05); + } + + /* Card hover effect */ + .feature-card { + transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out; + } + + .feature-card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); + } + + /* Typography tweaks */ + h1, h2, h3 { + font-weight: 600; + } + + p { + font-size: 1rem; + line-height: 1.6; + } + + .separator { + height: 2px; + background-color: #ddd; + width: 100px; + margin: 20px auto; + } + + .fade-in { + animation: fadeIn 1s ease-in-out; + } + + .btn-primary { + transition: background-color 0.3s ease, transform 0.2s ease; + } + + .btn-primary:hover { + background-color: #0056b3; + transform: scale(1.05); + } + + @keyframes fadeIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + .carousel-control-prev-icon, .carousel-control-next-icon { + width: 3rem; + height: 3rem; + } + + .carousel-indicators li { + width: 1rem; + height: 1rem; + margin: 0 0.5rem; + } + + #backToTop { + position: fixed; + bottom: 20px; + right: 20px; + display: none; + z-index: 1000; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); + } + + #backToTop:hover { + background-color: #007bff; + color: white; + } + + body.dark-mode { + background-color: #121212; + color: #ffffff; + } + + .dark-mode .bg-light { + background-color: #2a2a2a; + } + + .dark-mode .text-dark { + color: #ffffff; + } + + .dark-mode .btn-primary { + background-color: #0056b3; + border-color: #0056b3; + } +/* Background Enhancements */ +#home_intro { + overflow: hidden; + position: relative; + background: linear-gradient(145deg, #f3f4f6, #ffffff); +} + +#home_intro .background-shape { + position: absolute; + width: 180px; /* Reduced size */ + height: 180px; /* Reduced size */ + background: rgba(0, 123, 255, 0.2); + border-radius: 50%; + filter: blur(60px); + z-index: 0; + animation: float 5s ease-in-out infinite; +} + +#home_intro .background-shape.shape-1 { + top: -40px; + left: -40px; +} + +#home_intro .background-shape.shape-2 { + bottom: -40px; + right: -40px; + animation-delay: 2s; +} + +/* Keyframe Animation for Background Shapes */ +@keyframes float { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(15px); + } +} + +/* Logo Styling */ +#home_intro .logos .logo { + max-height: 60px; /* Smaller logo size */ + filter: drop-shadow(0 3px 5px rgba(0, 0, 0, 0.1)); + transition: transform 0.3s ease, filter 0.3s ease; +} + +#home_intro .logos .logo:hover { + transform: scale(1.1); + filter: drop-shadow(0 5px 7px rgba(0, 0, 0, 0.2)); +} + +/* Animation for Fading in */ +.fade-in { + animation: fadeIn 1s ease-in-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Responsive Styling */ +@media (max-width: 768px) { + #home_intro .logos { + flex-wrap: wrap; + } + + #home_intro .logos .logo { + margin-bottom: 8px; /* Reduced spacing */ + } +} + +/* Overall Styling */ +.collapse { + padding: 20px; + line-height: 1.6; + font-size: 16px; +} + +.collapse h4 { + font-weight: 600; + text-align: center; + margin-bottom: 20px; +} + +.collapse ul { + padding: 0; + margin: 20px 0; + list-style: none; +} + +.collapse ul li { + display: inline-block; + margin: 0 15px; + font-size: 16px; + font-weight: 500; + color: #495057; +} + +.collapse ul li i { + font-size: 20px; + vertical-align: middle; +} + +.collapse p { + text-align: justify; + margin: 10px 0; +} + +.collapse a.btn { + font-size: 14px; + padding: 10px 20px; + border: 1px solid #007bff; + color: #007bff; + transition: background-color 0.3s ease, color 0.3s ease; +} + +.collapse a.btn:hover { + background-color: #007bff; + color: white; +} diff --git a/base/static/img/digital_features.png b/base/static/img/digital_features.png new file mode 100644 index 0000000000000000000000000000000000000000..a7cdcbb847a444a52dbfa19a5393458e51cce2b3 GIT binary patch literal 12071 zcmch7cT^Ky*Kb1TB@{vFU_cP*1Of?71*w8GAs`|lgd&9An;<Q8MX6FOAfl8Yf^;HP zKtMqtNUsWnE?wY8pXYtP@4f52>wfG0ac8YbX3p7X@87O7bIyq~GSp_G=cWe$08F|% znkE1MNRWK(OG{1utsaZ6Apg;M=vaCK01Rh-pFlu%4krLW&F*Z5^g$Y2N1)xYQYd?O zJB(BS)`Lt90H~-3c%aZO7#~49jH9!gDrCLB86xOxuL`kHFpx3u(7-r3>jZgWOoI%~ z&_OO}xIIK&O;9BOL1uu(_@D#>u&!?2hyYc{AHE3k_3vhBh~OU*9~V`~mEQsdkp@PB z8tz^gL70?`Bw9vBPEZjpB@2_6hsj6?%E`#dNXsZl%gRa0!Vog@2w6qJzYhpmnwPx; z!bDT+Z&~C!Rfv<1j|W0p+TY(_${#A_?&T;g3x~s{W#pvg<Rr-ylHP%CKBxdmH*cYT zanQtgqrIFxe4O3g1b=fx*}41rs6xm{|ES>O?C@WF-Ms&{6WL<Yzk8%*rDT39^_R9- zkAD#S8)X2><KGDF(f`nS_<Fhi!D)|{#<*gz7&jkpGOg@COg)_3ecZjB-2V@(|LOiW z0c0;282lsSKVrdR{}JKsbIp&e@!z)lkJR2~fgTuX6O6aJuNN9~&5sOI2qO5~0*D)4 z&g47PZ~Li2WPjVj1EJxCLHS@b$)_shw-Y60;F5CkX0oyfIYk6iMqEY?AtUn_se!w_ zvqRv2B8AEzWMKbBN{%3Vln?6vN^FltIJkRZQDmi@u_#B3w1=A`MDQO|AvD}w-Mz?y z$>^Z}y<JyB!^q3s!P%AE;ccRQRZ#bu222(XgGtIs{iT_K0YcZ!+Xv-_#^`FQLdd#G zIXl~v<5CW$C=16(!Z3DFNqGl(j3f#}j#_zp1v^EU3`P;7g!!W`dAz1O+V{7ce~<qw zIqcohWFG$o4<@H1i-9SkCG8YZ3X=9RWNvm!7&}R{f*l5>Bqu9x=K%W`H?sA~`H6D< z?_7VQvL|yyE6Kv-6i_fpdj%*=QeFWAmsBF7l$1d!$UyB-a<Vc?=-*lJCt46X&fY%m zUV(q-l_|#a?<-el!9M|lK%xKgu{X*OV-NYe(fL1E<9{Ug@A3Xl7;@PBdyM>><NuQF z?e5^?kMhD?aU@&rU$w*NU#TG|Co3f<c+mibc6R%nEMoschjv1_Ibz6FOBy2m|E$jc z%(;Kz{Qs(s^zU-@m!qWrcOU-g`){R<to@%Ra=rY0`A?xH-~3bRF>Yi_d65e_R4hOb z0JvDIt9ivNAZK;Lzb5BqRp;h9d?`)yDn-HNt878I`vXk3r3~tH+#zQrMy{H&O4HLq zuCZKWr7F;&qctg@G;)Tp6jEIk<V(FQ7#%h5u_oZs!59mLx?3KF7X4D|hcbI)S_a<; z^}k(D#mf57HIV{F)d3I+KTi;RD*f4{MpANol46o-l5!H@05FZnh~Tiau-4T4^=|%l z#A7LHKLf5gCR@j+k{xJ6N<->zrs<91epfi|-453dS1@gJ0+vySO`8ZgQ+}tQd9+rq z4$7Q1&8?H+Co%_5z&vn@=K|<!m;%@{sU<vd;t&X1mj+TpaJ5wblh-|x*_ud2eud@` z6;k(b??(z*?%i*t1*g{~dmDxil&JS<)SA=M-1u|3Sy{V`MjFH)41~mTR<fx$Jk+SQ zWwNJplp?2WVl;T?LC0pbMq({k_U`B|v1CSy>v0uJX6h9<1Ty<-QdtlJF;O;bb--0_ zQEps`S%|~oK)#nXHQQQ~2v@i=4z70m(<EG~9HHuUaqob)o1OLH<F7zgIy3-ZtC4dq zd=%`ddG!Ev;G=dKDyX{ysP@J|83MkXPsYh}k-4=HDfZuJ<dg$5l-gM7W^AW%Io)pB zNmB(efIWCyEm-O6Z_2FU264fGr<4Bi+j*wEtnQI`Z?7#mP-X`DVCR#|T$6>C4y&`= zayTyP;qHTO2xkAG)y|ox!O08idFaudvfO~!_4GMb4bAKa2(nge76Q`@@R6AhIX&lC zA4ZYUvOcUiQCz2g3ZlKHxttDrmVndYl_b0eb=(s`0!j#P6?WPktPv*kHmna3g<u>G z*H$6<c?!PBHZ4a|d3de@ti$t}=?7B@kddl`qMo8PTrp>bEQrgPMmXz(p=Xd%`@@vG zAGjr)nY?l52Xy@WlKrJTfEx~?r|ZWp*z+55GIaaCGpAKbBO02Ta?im!a_`aS+6%Ih zt>{yRfZj79fE?-)t-8G><M-Tg@-dRUXg=EUoQ(>btVK;tVf1piFtpg?(YGC-hu>rx zi}U3b<#za!`CMjJ)?#$`{R<F?F%yLrqAe?-*C-yRbH;>5Q&aOfcS`@K1I<`YvYU(k z*x=Q$NBnjo*}kmB8f07Ibj*%(W<Pnw3qc^hI(#gwtZ}cOnC4tC^wiWW07rkiuRwuh zM$#a8l$Fugr|0mrnZQp#2Z}*_Ymtv}_wX{o$@Vs>k&BYC(`2L?wccOSg5^$+y=TbJ zfwkMoyz9ef%f4%A#jwx0d%RBHK=r8=cD|pWW?H^zYM}eHSIkf~tc)j#TCng-X;RfS zZQGHjR|qU6tAl5d;D)5!D?iO=@+<K!gD!;bPcFP2g6382v9|ioG-pp5r9V?CTiMC_ z(UO!2AtZ&YgsMTuvukr5m7oDWC0-(H&zWYmO>n$bcqzk)vWK{p5O23*xN1accv6LF z%nK{JUo`tP9_Kt_gIBq=b4IFC_i|>Vz6$KC$N-PlQsNHwEh37-W5anW!0}&2%J;ND zTnEeX{a2C^*Da_)^?*5i%n3`0Dv_duz<sbCp%}h|<e5FW>^sW2FJP;0YwF6P;;ifs zhRK{z`eEyOUfkyzWhZh3^TArEvx8qI9#H$0hEWHwDKooY$qwgNzdRT5A_BCbs&E)K zjhKGVvE~iM-|O~rK&iGW2?Yn+(l{0RB1Wxou|RD=tgSkmjR;5)&|I{Yy=EF-A=Xoe zH?lD+B?Ka+)stu<-kec!leASINcvfgQWp+2X#+g*`~~xq-FV06A-v0HJt4NpMv7g% zG(Abbr_6IS`4Vow<xGOIb*2$y>3SrxV|rPJQr*$#Uf+cQs{JmmyQQA)-oCNWucbI0 zGO%t@``*a>B7bv&H-1IOIu!46RlKWNL%f3e`{$?>X^sUeCAR%04a;T_zb9@mD~~`` z;7(Gd{)JdXH<SfB+Cl+oKRd(rm;ru}$?!3k{emqiZYmN3iHS7L`Tm6%@##u0QP8!N zE2WXT9yDfeG-*dt+ye6=gglvO!Xl3Yq8l0kB=eVGMdh~qI0N<eO=!{gYRgYvbIVjY zkx7@MLQ>L!Q?HFuxWE`*w(VMf&mgxm==7CUn~*$h=}rWeVtm%DT<^Uyp4#2<EW9mc z@R?-Yz_W2HYp-4UnW-D)W0p7b^refu$`TG|FU)V&XYR2*lzwyqe|KipO6fy!iQdTT zcjn%&=QQZLck33hmW4UxXehVt+*Iatt*?X7TjxkWzu<6J?gL{|)E(vXOITLeMNStP z$P)MG!HUUgPdWjCPHKWG5$9C8VHJZP7c+EBrg=#riXMD+eutpNE71A!vU2vKQO9%2 zl+BOtpkwE?xOMaVwc8V-V-v#fp(~Z2t;R83_JOr151l{piRDb1t!fZw(WrV@?MH&< z4_n7_rUm?xY9g@+F_m9H8ka@e^cYY1#SdmNZyfaIWo>x!k2GK~Cr!+|!6E0D92y3} zgc3Sykcu`8`?-(w@%1W2p7_04B2@tAEy;H-hQ1e<6MBCbwmSfXO3UKMfR+sVP#8s; zU%YJD3q182;=;Xc<kexPAoQI+ar~aRem=!Xv_Y&Mnk%LB=$923lOHX)ouF(g27xRc zbJiqnK?ghwxb_d&HrPBeHB?nd3BKLQefUpdgm%K6h*hYBVmv=Vv7!%Eo2GxtQ3QG` zTWyajx8L#F6my;Mt2pzVx+^y611w<SV1bmJM9*urrRdAPGL}c&b4%b|@etm4PYzZ+ z8uG(*;h0XTSJoyKtRp>bI4$RTsw=sauqu9tp~hh2t2Pb0pO(nO+RUYHmN<R|e-y=w zs$RqtsxY=8u9jXjH81ugRPMX2>CIW0_eQ+}nKXl+`@PM@MjHf2_OnT_vL4_EN{s#I z9(fd*jbnOWRJfW2D~+^{#YIg(>v)ED`B?Fc?&nO;>uNm)e_Z@JnS6ReboyZ)Xi=c$ zO8e`~R8j}OTl;{ldcOZP;fER<V=_N5Jkuh!m-}{DSe-T7RpmzPoR3tij8$9Bi&yd0 z295G{>IuD^(YW9n;Wd^@4Es}kC)!oNK7#N}QdOBNV?rGELY((~JMQ}Gc5BQxKfE=2 zq%>0rxZrzvl7r0AsNBxcnb&GHneNktAh!G@+6*ap0Vv`Q@QzRX`|}5mbJtfKiObQQ z`lm=N^}IXM3SNAB4<%|a^7=dyZN9ASFTvc1!^x+>i6+mRDH8?SX<Zjz*yM(<-uh|e zBK{yX=;@rmCEv?o*T0M1EcCO!pW7uk>kOGORU|)Ru}hKIpWuZh++KbDw_7IsBkQ`& zqI+`Oj<ufd)4_%JE{*^XD4Q&q+ol`{7TaY}@;NUmmi$>+ou!WHp2#m|(e53bsord0 zwSF{PF^7NGnlyi1ukV4_dz$&Z(XZD`YT)biIvREI>-6MDna1mCN*94&m2Mo8ZM*M3 zwSj@RBXY-8*_E$*2%qGZW5dyS`9{y&8S9Mamc;E-=3OfZ)X<go#)=U%uS|TV=o9VF z@vBJ(bUu?nc;-e=kWH(hNq$^z-6+SHiyXW196DfnYh{9t5ERjq8Oyg1<qJ5cJX0lF zU4+JxE<X>-homvr(;flomlPzT*IaEc>`vSP9a)#=UpW}44GNm5QZXt}yNQ?y1*8Y| z4Eng9opmE+-uD+D_gC7`C6&~_z}++Qho56<1`ROuge)wCxqd!ml@MQ|VdWpB8Ur*l zcu-c$b+^iz{Roe;I(^Sr!~ewQ&ASi9RQiq8XZ(`e_Al=gJ33M)hVuD+JJy_Tq~GIV zBr%e}q!hjmp&b{&8AJFzh9m0<Mit~tSR`Jco{s2v*W>BoM{UWTdJ5T+kM$G~wp+?+ z%7Wi#N0Oar!YFf?BvSS>xuVA;32@{gF_L##4xn_lMy>;$-17bDsv^GdQ1L>5D-TS6 zOJMryv>82L>|-r7RN|5HNy<cpV&9RUIIKOb%>p0efeYejX7QZ*VVcuDDd<oO$oIZf zKet*ge8(To$x}<a!(W9i^6dOV73@Bk&f}$F!>&lKx}RDd$}b%>X~1(Qq%K&BG^!EL zYn@j&)deFP$VJ=Y%wwK!Rs>GhZ;j(v;jsKitqhx&(ndzHj6>=-R4e*Jz9CLjmJ>2Q zw}O2`&U^5^IS^UO*HHda7M<^PX@zLkU-LGMcro}JLL`fBqi!*}A??=eyE-&ixt-7+ zf76;-k)mCC@OzD{giV3AvCd~UtL*7P{y9K+YwcUW9gzvvoeVpu$UQB~T0+pmWK+_D zv*EJAw9K^B^xX_@-Si;PlxOm2a_)$yOG~Hp2M2q%&agyPt;m9gS4*;YJTI+J5fS#R z#p+PJ5I_XobN742-9S?YKR;`ASMy-bH{aY=F|)@l#Wpu=#i3`F+v{nA)zoirIm-B@ z9yGG9uUF8Wa+Y8hm;9#d2{%kTMw!&cfm|u4U9;M=gMzs+xgSs5T`5Z}v5EKmaPBA< zc2(=hW7>&D_8TGKOkSOZUSAsYIfff&HsG4eYOc4}&X}ny-E85RW_NsG%G))fSER&k z`-<70!h-!IYrfHa;l&X%H88fI<Q(~Bt8T6_=jT;dXyCU6WbBzz&|B^<3q}ru^Pl2K zfbXctgj+9hvfP~JL08lzZl<X%%ZF&UW`M@*8drn9$t-mpQU2)JTnS;HNR~8K?u5HF zF5U0{{xj4SCfG|A1YFS0Bb>I4g>I;R*mZVAPfsMvUdy&*P+M6IrqI`IMvc4?5<)d3 ztm<cG${9L7$!`Rc#^OAJN0Ws@Y3!v*!4^wajIdh+LKw^bmmwhvQ&jwVT<D?!@JBVV z$h4rdW54FG{IF6Vjd0@5^u<o3DP0Eo(0ZC7s{iZFp6a%u4<YosRwAq6?`+i%7htnH zZDBQy?2kAD>S>;UPlLjVkX{GLY4||DigiNL@XMrazwS<brE);N$e~<sP16j^+<PK@ zmeE<@cXZFtBW~OU^Vii9qv#=xAB=^}AnynTp~-u&YHN3qbm{<+jTAc?dp_F|^g^;n z;?S2QqSi+TW#F$fM3L*#o$!w3Pr7D(e#1Y4W-CjU_*!gom^<7LTCYwA6d`lr9Al=7 zn;^-7Pj%CkIb~G7{M@5vuFfa-FK_yb99J-``Y554A28^Q0D;7``6I|2Q9e>GjOiDE zXFMmtS1vv`fvaTM^^?@EJ#7bg0^fsHK5>PIcvWLO|8{6Z&nty9yl4ZV<DZra{wOHw z36)fld0-*EfilOfB<<`E&l4&ZIG;7d_LgBhD_EWF1#F#RgJ3mH_>IfUBV|!5App`U z%N#Qjr1TDs9Iln;s)nU_2&7cEB8aX%nz6`*oLe~cgs3MI2`ezw3|s42&Nn$nbVr4+ zuH91VIDCjM(JSkI#jD%5?d@BOF7fg)smJLAh_b4gP9%e6%lvVyL+U1KVd&RIZTA@R z4L_FC*53&dt5%14zAPPX!RDAoPXhTp;%^#MVs&=1O@f(uWItYoN`H7w^CGS(rZQVM z>Xwl8T`51AR2(i!mZ$k^%>tuf=$Q9~4dmf#⋙zTs5<)wT-iCOWrhSzNlkfJE|}7 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 literal 0 HcmV?d00001 diff --git a/base/static/img/su_logo.png b/base/static/img/su_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c433f410a1d25340e90b542a997b743219238ff9 GIT binary patch literal 40692 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%0DGx%S}Q zLk~Sc1<9F$D8}qsMD;Z8NcC|=QN1T3)>Pl!^TB<w{6(X~y4br8wjl#*={_mQOCSmT z!A2eP)HKS*T{p5<vIs_5a}AWX%e_ntjX~$?H%7koi0Lf=%+DNcgOUcX^Z@!@O9<UV z_4}zt5w{CpiumBwL2}O1f@Qw39Z!%J?k8Th3EkZjSWaeX&kTGF9xJXyN>3iT>#RfJ z#+?)Quw=J%L@O@Zb&N(&9m{{MNu)&$_{-DLr^)(DXbQvVJ=tA$zg|f7FLBY=+Y{I6 z6A8MT*fr>^bl;Rx*4s!m94+lP%ZL>{cn305_}bM4jeE}3HQn<h)&Kf$tm3)NCOB;U z_|c8(TR>D8&RtJIj=yC%X}E!0?w`*Ec}ATq%K_4&P<A$n9fN@A@FPc}ivPsJ{3+T} zb7A{T8VEu%82Z~hBcMyRABq)RJMp%E8=aRB$83{&xG@Hw02Jt5_yrztmBJ@DO&SxJ z#Q#q2?ifWjANLb`F&}GuitUs(-dft6H(`lD;j`HX4(Hp?$JqU7TOy%0H;#}$=Fn~1 z;?LV~zW3Kz)pKa?IJ+<*9|WI~Li1LZGDHJ}<U4dexwag~a$3*!*?U)2?3)0m9>g%b zZZ^o{xaml&=3I|~*$<9x1~%P@9v#Q4CmN5|BoRqHF>_(pA}HWC6ylBBuiRSacOsb! z-qC7zW@~+~J?K8}X?)+&`$;HmhYsFK93np3orB;+GrM>Sxl@?d&_3=pw4+{O2{oVr zZptd}*k>}fmz51Bu)_QrvoN#>U8?b2Brx(zq`G`-U|*bOBeiUvIUY)FNgarU*Gx!I zwI|^|?>o}zs`bnFxWngXu6k+Z=P00u!1C$9`4e*H29ORp*q89>en00ZJohvWIoiz; zXX4M5HS-~^y}SG=63ylJ*9LGA6Yi90@3!Qso6EE&QUXh*V384Z$E3NjW5dvjfRx8V z9`fmq!4K_>wsTAw;6WhNCO)4yQT?rtCZRyXuhT`TIkBnZ!tyxm^j6aJu{KtebMSD6 z{#A$c@7}0$in=zl`G<ygi{PHT7YJ&biH5QL?EUg*-fF|swn>cYOTXJbm1#YoRmX*u z@-U<kGdNqJHi=G82A_*%7lTRkbOb74VFv!yvHoYdTOiLa$#%0Qm4E>P%F|kBi$#0b zDimsmD>tH&7t<Kz+Q?1hd*vE`vTkO09QQ*dDQL|)BACLQzorzA=En7WqypYgV_L4V z!tkPziDu$E+;Vq{k7K!b=Pvv1pg?7KyfCyUV21t4;RZNda%YsA$M8XAmRjUbeuIh@ zi&xoD-%&DlmDM4L8vMWSC)acSV*`uEUu3+IiNYeN8qNCn;UtP6`nQa7e^rv!FZD8i zgBShpB}2PhdVn(?Gwx1@Od&;K-#(=OPVHj{yNGuyYlZ^_Y|gWd!1UlenW7PnYeRds zTt0MXgPSi!RQ;`fNRXXUpOp237?dql0j3|c4RiEnfUX%9=2o9<9<&w=wAf_Wevg(v zwnX#pY=MZc1i8CyD)kcB)?El(7->@Z15o?S{5@3Q7idlR^5rmEWi#)R<$C9)a=kOD zwXq?=&W4Jrs_3m#Mj#2NSd$-`o!QZ6wV)@#oN*;G1}LkgpP>$d+ERvSp`qj27Uajt z=$vXDa}v(~%kMIFhCRd_wub+$o&rA8D>yBQkCeLl+}jZD3y2-_YAgl}g@%cZHa^#j zd7X7B(TirINXQxrz?~7VEjZe7Gx1@5>vP0oD?E?EeYLb6H@trmZcTl?J@pjXd)iwv zQBipZsujNFHLAb^MF~DY3r9BD)%lu{Ww=w*Ga=@wWxwZS3+ce+2U#37=%AYLo_&s8 z+R!yE_NoN6vjvJvWwTf8@vRD{RgVVUuHI^&Hh+t`B7jpwpwN~3&?DemK;_!v#d>>s z7&0+crwBb-4L*yWi9x@|oz+NV7`m^a!H4S)cC8xQI>kV1m=eo4{WlXk5KkP*XZ-O7 z>Pr_0K|MXnzr-ZP57nHgBm(UaezIh^#_1d~xQ9PW)iVd`y3rQ>=d|*%BByd#o12zM z`a<Q+9VCWn2wa@OdBJ<Je&5IJTUmw7<D)*o?JYsy%+vwCPqll|CYO?o`sB{hCSb*j z_V#@=b|+Jg`<B*<a&nZE*;|i9x<<DJ^3GItHh9ip=fSOLtCiB&tf8<9BDYPN(}&nl zhq-B)Naub8hak|PXEosRI|%%l-G>6mKu34-;=o-cdbs@i1US`p+E`Y2L4fPhK|k3A z44)7LQk)C<Ug3+cL>kq1V20)aE7fS{R$;H4Rq2PU%RD{-Ltt0iv*Lvh<tA$Bq|7si z5vm$o#MzD*23#~EdC4goxo6Ux8px47lhR0lmPMn|Tee{-AlCB5QeV$~OaB8*3<LhN z2kVy{rQ7G#X<+~+x%Ml44TbsL9-X{I$xh~XH5c77q%CnBGAd%lBQR=VHm0N`2K{N+ z2piLLla8WH<Dtg&pv<BcwVmS~|Ai@9uMS{X73$(>7UHj0Ov+Dxxi6q<9k6&IySQZ- zrIWmMh35Erge2GyCo*J=GDsp4Z+~_bCGT)CGOMLkMdw|&W8(~oX+nFHad>~#MA0D2 zKpw}?kx<XLR{k}ac5+eID(?&4d@CqZO4bG`SfvNDk(suKQny4^E&vhUp`GhNxq|bM zMjAqCsEPM!ox|<SS6kV&ebm5(pRFhH{4&<-lz&GPhW<|kZ)YizVK~JCE7XZZKOtT8 zW>!<n_x#9n+h3P-Z3#{*^ei&^>thX4=g@Y=j~bfE?|4*}r<=<hy2wg1n8ADK1`Zg3 zhXbysDF^B4lk-u9vc80A>N<pLu64Y>IS7`xB)a1L-}@AZ_?H-~9%g03mEz$b2A3kg zF^&<N;2(sla=KGj%ou;t{~rK5K*Yb-QM!aqr^n5v>)OC>!0q$K{@R5g(6|#SAWnvJ zWTxHI{v+eU6E7@b);ChHjK=VH@VO>R5;{YPJ9ciD978?;7K|wP9*Niwg_z=#ibNcq zp^S&@>|cv6@9PlPrhk#*78GihV6gd)w;G=#dPZp(q-K*e7Fqgo?wEL56n!S4EV0dj z5M=~wy|CO$ClDpx!vn1)z(EwAGe%H#mu&#Wexuz>I}*Z!&ck7h^Kw&3tXMB?I|}Ob zn}&{j(Db?)QBE_GFbtuyMiTix(?0Rpc%sC}SNbFsjpltde_{BCgYe3XSXA*yjqWPD z7<=s!7wHK^#slhqXA$$~upm5gP39hWj&uL(`_=TN1wFLegRCvxu5)sVog>&gjf?LV zXd4P_lqSqnKKlFR2>Pw&$nl(_#*yu%8zAT^G`2BCzsId4Si8OdYu*!3gTjKNbIIPq zQo9oF+AvM>tzEo$-K<l2Q04?T0fx_O!lFl2j2iBoQ3EOE?f<-lY@E*G!K`N%>)})o zBn1I29gusQZZq{Xh%PF|1R?4mwp$xUBv9Owb|ik7$Bs!kVPKq$TJEkz0F6=^^t*p= z9k5@B8Gd$NueP;rj5j!XwSq=YSSoL(qkxmlBh8{iIUtD4tlc63!2)<7b~~+A9Jlc> z^n?`F_J27hZ}Z9jwrc~s-|8oUFLB`pU8-<q9=pSr;*+k2KtFMmyl0rD{_vkW<eoZw zynZ<_7jh3x3!xQLDTzd;3V^dTgB7oFybhyva68=?h^|NaOdZGSthNW{lId&~<Hxru zuI_!pzCs^patG*3@_yEuy?i2EFnl(MhPy2A+{nEYp=s@I@`HpXv9v`TejJAGWZTI7 zn>{_kvm0K5L{{>HLoN<iBmuG{S=WSIH48z|X(6^JkuvdU$+03&U-Oiq26p}$2?x5j zo`_3frlAzuTT&?iy<<yKcH^72!s|5KMuGXUBIBOZ8Uecm*Z{xxiCu5Rr4?JJxU*Z> z*s0S>RD>x(&!7Wk;zDT*f4g}8NYBYK!+ptc!}f@fG$I!|O=7J7y@Y_rQXcIRFXtH% zXZt_Gb%<7t1E;!m^(lnPxZf-(e?_uF`*GLt7KwbgpXf{vL({+j000yv-66=dLI$m} zq56J875Y(F)+YW;GROB<0%|piR^`V%uL2wPSC<=`*imOPzd}hb?0Jl68CP35x*Yv& zWkk%)>^+(f0pX4SLXSpZ%9gt9&hJU`p~eqSi<`?0_q|eSiKH@y9-gzdEJ|3ikOZ!k zem=i%uh9FFsXYOwywLyPO~2vSO{ZF`wUQG;synit(1wgAy?r@=ql^sKGW>^gvP5h* zc;ZDV(!98km?-~mI>ESULbiv`F~J4R;Oj{ui2ap7AZ31%?w@Kgur4;NFuo^Z;zKY? zuRWUqPDN;T&3QIV$P}@dS<|VQJ51f0#)bDi@%=nE0QlRDhBI-ihVLcp3q%q8g)Eo{ zz$yFEDsM1ucH<nx3>vUR31ggohOTo@4%%HZOH@rMK5IlYR2-Du<E%(mwq{ABS=_5^ zSi@YK6h!C-YIN3~{zPqybnhMlz}N+V(-;bu%4w1$^ptdx8ogX}&S-2C7l!;ssNP;t z2X>zTMF28oay&YgVmcERrV{HM_GI>9!EnlK7^y|3`LIA;;2YD$TtFB;k6IG-im(Zh zM9U<a38j9Ko>9N*!kQZdkX6r{>_bhQr}Wl<?P|M;m+ewDByeo-@ML*5OO9r=*s?y} zfj&l%C;eONZX9Scf6&^`K?B_&eg!@s7qrP5FE-t(YaS^BuGYv*{$LpbJf=a$28q&- zt9Nr#M6^A|C^$@=3gd?zB#(Xlam({m&wLth$Hw8Q#_fPBCb&Z}VUV*9bwQ|D7xiWY z0u1TjqB$exi_z^v(x3FfHKhL(r6eA;DpimycG7@-qR_y+fWunHNvx-G`H0f`W54A> z3(s9Oujyru3B_;v$MLi-Yn}x5`D<D1<`|_xRhZPf1z)J!2I=Z8q3_RGpc$>Dvw3j} zs?nwyrvaKC^EooQU*9qTo*FWxiS3M&omJF)K<SrvW6e%*yb1K$RJgmltG^u|y3>Al z3|}+x<FkLb2<41_bVkhr!K+{K{7k(pN10r;OAdcN8qW|Un+~ESM2pH7`}z0s-|-{( zmpbAS!|qv00{RfSrJn6As)9XZhC7vAnIZa5RN_Rds%m(r<WdEZON$svFTQ+MuNYcZ z7>g=LW6OUzH5EN;TcMzF3QJAxC)U>py1lX``&j<6<h-P!Qm+{^xKo{yI!P|-mM2^$ zxpgkYoG&@K#yyr?(7jM}RJ6^S8%f;zg?XjP!~~-;m-X@7Mapf(xq68p8->Ym6P!cH zRrDq3sgPC;MhcUHZOV@7srtEXQ0I&s?`*V8-!z@C0a~1+1JY}~{>pW~g<A8idhj}{ z<`zk&<7Iezw;k9k>q5+l4dFjy<QZ*30G~uM&vaz)`>X^=H7;j^o-5T&w)&U29&+!L z@ad;MZ_je4I%mZgk4X5X(UUL_fn<Zfo#sSVRa>@d4K{zE(pJ1W5NAl=r_EgY^yw~n zl6MaTDVx-I+cXDZ_+k&mMb%q_58@^^y;JUkVFGxxUDZguMlwJJ{MFhc71siT4uF}= z{*JMoQons+*iWwGid{InWc#Hgx4NW4LF7N#;GWkicSTusTf5l$@O<5ysL88&tE_Cn z&L-!f>TsJs(^@4q+u@9UpO{Ki;$n5n<^duYLY0`Qm_kAKv)2B?RqHuVB;)L}Gm^Tw zwQI~71S2=;&m?B!Wa&lG3((t4oz_zVI`ZVy+9-LzUtNo2B9`nO?-4_?6=ORJRnW|< z7C5kA$7mHu>kt19a*?WvWABmzT3crt@9L@I=tYd2WXP9AxNroffF@UqsfUk{XbOU| zO?unR2_}QJkU3<+fi&ocPeu}ys=NbGL}59Q8mP5zKZ9`mgw(OBS#)Z>2R(1BN(qv_ z!_LUZAxgDr?>L^XSwsJi^~1SLaB*jx6pAz}N5)$3U%j*dw=bB^M<T%BB5g7)u+#_B zpn-4^pjg7Q#@Bp(?u23yn&3j}uH7GQFFUaOs4yQXOI^kzD$2Z+7A(HNkDhdBKjsOJ zwA(ZG@&3<u@v5D6-bacP)mmY<H7_y!!NDMm<=pV;s<XG*J?AP5iNs!bjQ+oAZ|$yl zFP7sFV1cX?Mor?Kuxp2S!cX-To;zy^JZvD?^N6}9G@qLvlR9%a00Dlt@xkgJ=2jGy zSoZjHH*kN{0Yq1<?0ho+!&w?Jd#)(XP0oFMJ*rK+3XG9CqehKfN=>}UTC(yY-n>R2 zopJZuS2aulAhgcKqiNeU+n158PMqt6rhcDev%B>HQU-w4PlFC)znSl;XgF_9fm7Hz zQ(O`xzw5oVQ$LS@gfyjwPJ^N>QPgvJZv08r-IJ;>GR4^8R65!BEkyFmjjL^8>pasv zF%lS&KRGNZnoMk!VQV-dmSCZDo->}^obZ$De5(6j8smA6u&KYD+i~&|^>!*!)RpN$ z@sGI0prh)UHuY6D5wTXVV(wy=)g5GG($~zX)s3FlP!=m+grP6u!Oq>Z8Ig6_xUyd} zoY+R;5fY=zJB0+Hv7qmWlf*+H7RyJ^qD1j<opFfGq8oqD!4SC>@LF%XR(x@)Y0)O1 zvGon<6~i3{)VRV**qXR{a~_lkS$3znl#H+iB4W+O!T5l?Q>`WGkQi1vz43#=vIDhr zREjyRhZw;su&saVr^xM@LL+$oX!_V8F>`0f!G%mLGR#ggBxNbLM=@*T9EqoPJ*vX) z7}lPe!MlA#mHJZpofyoXkwE842>3^*K7&Ld>QAM*>tB&5z8*muI;;DdrKJW=LFpUr zXy3N$Ny8oW>43}Pf=HJxt6@A`zsgi5<&LD}{R80I{R)1^o07<^uNt~};RCR&qyGHf zzCYGAs)5mR&N$m6XQW`H3_tFAJIw#DcrUYCNz*&hBQj08b72}9LulzF=3C)3vCMll zhvvOpd1ON)_5cwQut>>ysl{uPrJ$W�z8EF&lRPkRx2Sj~+=i))MjdYbz8ET#31+ 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%0D;Bf^yz0FjcL5D|&0l;_xtvZV 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 literal 0 HcmV?d00001 diff --git a/base/static/img/undraw_posting_photo.svg b/base/static/img/undraw_posting_photo.svg deleted file mode 100755 index fc0d549c1..000000000 --- a/base/static/img/undraw_posting_photo.svg +++ /dev/null @@ -1 +0,0 @@ -<svg id="fe93ff64-a18b-49f4-bb52-e425cf20d0d6" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="1050" height="594.02" viewBox="0 0 1050 594.02"><title>posting photo</title><ellipse cx="525" cy="561.02" rx="525" ry="33" fill="#4e73df" opacity="0.1"/><polygon points="497.09 549.99 318.9 547.71 319.43 543.14 328.04 467.75 484.53 467.75 496.04 543.14 496.92 548.85 497.09 549.99" fill="#d0d2d5"/><polygon points="496.92 548.85 408 548.85 318.9 547.71 319.43 543.14 496.04 543.14 496.92 548.85" opacity="0.1"/><rect x="289.2" y="544.28" width="236.45" height="5.71" fill="#d0d2d5"/><path d="M826.24,167.93A14.87,14.87,0,0,0,811.44,153H151.12a14.87,14.87,0,0,0-14.8,14.94V568.2H826.24Z" transform="translate(-75 -152.99)" fill="#3f3d56"/><path d="M136.32,564.2v46.88a14.8,14.8,0,0,0,14.8,14.8H811.44a14.8,14.8,0,0,0,14.8-14.8V564.2Z" transform="translate(-75 -152.99)" fill="#d0d2d5"/><rect x="89.88" y="25.13" width="636.23" height="359.81" fill="#fff"/><path d="M484.71,608.09a15.43,15.43,0,0,0,12.13-5.88v0a16.06,16.06,0,0,0,1.2-1.76L489.57,599l9.15.07a15.44,15.44,0,0,0,.29-12.22l-12.27,6.36,11.32-8.32a15.42,15.42,0,1,0-25.47,17.26v0A15.43,15.43,0,0,0,484.71,608.09Z" transform="translate(-75 -152.99)" fill="#4e73df"/><polygon points="425.13 472.89 496.22 544.28 485.31 472.89 425.13 472.89" opacity="0.1"/><path d="M709.94,364.1a1.48,1.48,0,0,0,0-.21,55.29,55.29,0,0,0-2.66-14.57c-.09-.27-.17-.54-.27-.8a55.77,55.77,0,0,0-21.32-28,55.47,55.47,0,0,0-72.69,9A78.52,78.52,0,0,0,608.57,314a248.45,248.45,0,0,1-44,1.64,177.65,177.65,0,0,0,27.91,10.14l-.34,1.27a178.73,178.73,0,0,1-31.19-11.67l-3-1.46,3.36.22a249.73,249.73,0,0,0,46.82-1.35,79.17,79.17,0,0,0-13.8-21.9c-25.18-2.54-50.17-7.82-73.48-18.3l.54-1.19c22.7,10.2,47,15.45,71.61,18a78.63,78.63,0,0,0-125,13.28A108.05,108.05,0,0,0,441.16,242a251.7,251.7,0,0,1-41.45,12.56,250.58,250.58,0,0,1-64.81,5.14,177.9,177.9,0,0,0,27.9,10.14l-.34,1.26a179,179,0,0,1-31.19-11.66l-3-1.47,3.35.22A248.9,248.9,0,0,0,440.24,241c-1.29-1.42-2.63-2.81-4-4.17-43.06.87-89.95.45-132.4-15A108.28,108.28,0,0,0,252.44,314c0,20.32,5.58,48.27,15.3,76.31A325.56,325.56,0,0,0,283,427.06c3,6,6.12,11.9,9.44,17.52h0a198.58,198.58,0,0,0,13.16,19.71c.86,1.13,1.73,2.24,2.6,3.32a120.36,120.36,0,0,0,16.42,17h0q1.82,1.52,3.67,2.9A69.49,69.49,0,0,0,338.82,494a48.34,48.34,0,0,0,19.81,5.38c.55,0,1.09,0,1.64,0v.23h294v-.23h.22a14.74,14.74,0,0,0,5-.88c10.4-3.69,20-18.5,27.93-37.21,1.76-4.15,3.44-8.49,5-12.95,1.41-3.93,2.75-8,4-12a371.64,371.64,0,0,0,9.25-36.12c2.8-13.88,4.35-26,4.35-33.68C710,365.76,710,364.93,709.94,364.1Z" transform="translate(-75 -152.99)" fill="#4e73df" opacity="0.1"/><path d="M434.91,235.5a107.89,107.89,0,0,0-129.62-14.61C346.83,235.77,392.68,236.32,434.91,235.5Z" transform="translate(-75 -152.99)" fill="#4e73df" opacity="0.1"/><path d="M675.72,477.05l-45.37-78.59a1.45,1.45,0,0,0-2.51,0l-45.37,78.59a1.45,1.45,0,0,0,1.25,2.17h13.54a1.46,1.46,0,0,1,1.45,1.44v16.79a1.44,1.44,0,0,0,1.44,1.45h17.66a1.45,1.45,0,0,0,1.45-1.45v-8.1a1.44,1.44,0,0,1,1.44-1.45h16.79a1.45,1.45,0,0,1,1.45,1.45v8.1a1.45,1.45,0,0,0,1.45,1.45H658a1.44,1.44,0,0,0,1.37-1,1.34,1.34,0,0,0,.08-.46V480.66a1.45,1.45,0,0,1,1.45-1.44h13.53A1.45,1.45,0,0,0,675.72,477.05Zm-63.26,8.69a1.4,1.4,0,0,1-1,.43h-6.37a1.45,1.45,0,0,1-1.45-1.45,1.47,1.47,0,0,1,1.45-1.45h6.37a1.45,1.45,0,0,1,1.45,1.45A1.4,1.4,0,0,1,612.46,485.74Zm41.68,0a1.4,1.4,0,0,1-1,.43h-6.37a1.45,1.45,0,0,1-1-2.47,1.4,1.4,0,0,1,1-.43h6.37a1.45,1.45,0,0,1,1.45,1.45A1.4,1.4,0,0,1,654.14,485.74Z" transform="translate(-75 -152.99)" fill="#4e73df" opacity="0.1"/><path d="M395.28,477.74l-45.37-78.59a1.45,1.45,0,0,0-2.5,0L308.21,467A119.89,119.89,0,0,0,324.63,484H331a1.45,1.45,0,0,1,1.45,1.45,1.47,1.47,0,0,1-1.45,1.45h-2.69a70.22,70.22,0,0,0,10.51,6.53V490a1.44,1.44,0,0,1,1.45-1.44h16.79A1.44,1.44,0,0,1,358.5,490v8.1a1.51,1.51,0,0,0,.13.61,1.44,1.44,0,0,0,1.32.84h17.66a1.41,1.41,0,0,0,1.15-.58,1.44,1.44,0,0,0,.29-.87V481.36a1.45,1.45,0,0,1,1.45-1.45H394A1.44,1.44,0,0,0,395.28,477.74Zm-30,6.65a1.43,1.43,0,0,1,1-.43h6.36a1.45,1.45,0,0,1,1.45,1.45,1.45,1.45,0,0,1-1.45,1.45h-6.36a1.45,1.45,0,0,1-1.45-1.45A1.44,1.44,0,0,1,365.29,484.39Zm-28.5-29.08a1.44,1.44,0,0,1-1.44-1.45v-11a1.44,1.44,0,0,1,1.44-1.45h23.74a1.45,1.45,0,0,1,1.44,1.45v11a1.45,1.45,0,0,1-1.44,1.45Z" transform="translate(-75 -152.99)" fill="#4e73df" opacity="0.1"/><path d="M574.63,336.57H518.48V324.42a2.9,2.9,0,0,0-2.9-2.9H499.09V308.58a1.82,1.82,0,0,0-1.83-1.82H476a1.83,1.83,0,0,0-1.83,1.82v12.94H454.81a2.9,2.9,0,0,0-2.9,2.9v12.14H398.37a.58.58,0,0,0-.59.59v161.2a.58.58,0,0,0,.59.59H434a.58.58,0,0,0,.59-.59V476.68a.6.6,0,0,1,.59-.6h15a.6.6,0,0,1,.59.6v21.68a.58.58,0,0,0,.59.59h70.32a.59.59,0,0,0,.59-.59V476.68a.59.59,0,0,1,.58-.6h15a.59.59,0,0,1,.59.6v21.68a.59.59,0,0,0,.59.59h35.59a.59.59,0,0,0,.59-.59V337.16A.59.59,0,0,0,574.63,336.57Zm-146.46,132a.58.58,0,0,1-.59.59h-22a.58.58,0,0,1-.59-.59v-7.5a.59.59,0,0,1,.59-.6h22a.59.59,0,0,1,.59.6Zm0-19.39a.59.59,0,0,1-.59.59h-22a.59.59,0,0,1-.59-.59v-7.51a.59.59,0,0,1,.59-.59h22a.59.59,0,0,1,.59.59Zm0-19.4a.58.58,0,0,1-.59.59h-22a.58.58,0,0,1-.59-.59v-7.5a.58.58,0,0,1,.59-.59h22a.58.58,0,0,1,.59.59Zm0-19.39a.58.58,0,0,1-.59.59h-22a.58.58,0,0,1-.59-.59v-7.5a.58.58,0,0,1,.59-.59h22a.58.58,0,0,1,.59.59Zm0-19.39a.59.59,0,0,1-.59.59h-22A.59.59,0,0,1,405,391v-7.51a.59.59,0,0,1,.59-.59h22a.59.59,0,0,1,.59.59Zm0-19.4a.58.58,0,0,1-.59.59h-22a.58.58,0,0,1-.59-.59v-7.5a.58.58,0,0,1,.59-.59h22a.58.58,0,0,1,.59.59Zm0-19.39a.58.58,0,0,1-.59.59h-22a.58.58,0,0,1-.59-.59v-7.5a.58.58,0,0,1,.59-.59h22a.58.58,0,0,1,.59.59Zm52.1,116.36a.58.58,0,0,1-.59.59h-22a.58.58,0,0,1-.59-.59v-7.5a.59.59,0,0,1,.59-.6h22a.59.59,0,0,1,.59.6Zm0-19.39a.59.59,0,0,1-.59.59h-22a.59.59,0,0,1-.59-.59v-7.51a.59.59,0,0,1,.59-.59h22a.59.59,0,0,1,.59.59Zm0-19.4a.58.58,0,0,1-.59.59h-22a.58.58,0,0,1-.59-.59v-7.5a.58.58,0,0,1,.59-.59h22a.58.58,0,0,1,.59.59Zm0-19.39a.58.58,0,0,1-.59.59h-22a.58.58,0,0,1-.59-.59v-7.5a.58.58,0,0,1,.59-.59h22a.58.58,0,0,1,.59.59Zm0-19.39a.59.59,0,0,1-.59.59h-22a.59.59,0,0,1-.59-.59v-7.51a.59.59,0,0,1,.59-.59h22a.59.59,0,0,1,.59.59Zm0-19.4a.58.58,0,0,1-.59.59h-22a.58.58,0,0,1-.59-.59v-7.5a.58.58,0,0,1,.59-.59h22a.58.58,0,0,1,.59.59Zm0-19.39a.58.58,0,0,1-.59.59h-22a.58.58,0,0,1-.59-.59v-7.5a.58.58,0,0,1,.59-.59h22a.58.58,0,0,1,.59.59Zm35.61,116.36a.59.59,0,0,1-.59.59h-22a.59.59,0,0,1-.59-.59v-7.5a.6.6,0,0,1,.59-.6h22a.6.6,0,0,1,.59.6Zm0-19.39a.6.6,0,0,1-.59.59h-22a.6.6,0,0,1-.59-.59v-7.51a.6.6,0,0,1,.59-.59h22a.6.6,0,0,1,.59.59Zm0-19.4a.59.59,0,0,1-.59.59h-22a.59.59,0,0,1-.59-.59v-7.5a.59.59,0,0,1,.59-.59h22a.59.59,0,0,1,.59.59Zm0-19.39a.59.59,0,0,1-.59.59h-22a.59.59,0,0,1-.59-.59v-7.5a.59.59,0,0,1,.59-.59h22a.59.59,0,0,1,.59.59Zm0-19.39a.6.6,0,0,1-.59.59h-22a.6.6,0,0,1-.59-.59v-7.51a.6.6,0,0,1,.59-.59h22a.6.6,0,0,1,.59.59Zm0-19.4a.59.59,0,0,1-.59.59h-22a.59.59,0,0,1-.59-.59v-7.5a.59.59,0,0,1,.59-.59h22a.59.59,0,0,1,.59.59Zm0-19.39a.59.59,0,0,1-.59.59h-22a.59.59,0,0,1-.59-.59v-7.5a.59.59,0,0,1,.59-.59h22a.59.59,0,0,1,.59.59ZM568,468.55a.59.59,0,0,1-.59.59h-22a.58.58,0,0,1-.59-.59v-7.5a.59.59,0,0,1,.59-.6h22a.6.6,0,0,1,.59.6Zm0-19.39a.6.6,0,0,1-.59.59h-22a.59.59,0,0,1-.59-.59v-7.51a.59.59,0,0,1,.59-.59h22a.6.6,0,0,1,.59.59Zm0-19.4a.59.59,0,0,1-.59.59h-22a.58.58,0,0,1-.59-.59v-7.5a.58.58,0,0,1,.59-.59h22a.59.59,0,0,1,.59.59Zm0-19.39a.59.59,0,0,1-.59.59h-22a.58.58,0,0,1-.59-.59v-7.5a.58.58,0,0,1,.59-.59h22a.59.59,0,0,1,.59.59ZM568,391a.6.6,0,0,1-.59.59h-22a.59.59,0,0,1-.59-.59v-7.51a.59.59,0,0,1,.59-.59h22a.6.6,0,0,1,.59.59Zm0-19.4a.59.59,0,0,1-.59.59h-22a.58.58,0,0,1-.59-.59v-7.5a.58.58,0,0,1,.59-.59h22a.59.59,0,0,1,.59.59Zm0-19.39a.59.59,0,0,1-.59.59h-22a.58.58,0,0,1-.59-.59v-7.5a.58.58,0,0,1,.59-.59h22a.59.59,0,0,1,.59.59Z" transform="translate(-75 -152.99)" fill="#4e73df" opacity="0.1"/><path d="M414.7,292.87a12.6,12.6,0,0,0-7.33.8,10.79,10.79,0,0,1-8.81,0,12.37,12.37,0,0,0-10.36.2,6.33,6.33,0,0,1-3,.75c-4.2,0-7.7-4.23-8.42-9.81a8.11,8.11,0,0,0,2.09-2.27c2.47-4,6.28-6.51,10.56-6.51s8.06,2.51,10.52,6.44a8.1,8.1,0,0,0,7,3.83h.11C410.4,286.28,413.3,289,414.7,292.87Z" transform="translate(-75 -152.99)" fill="#4e73df" opacity="0.1"/><path d="M427.5,275.35l-6.79,4.31,4.12-7.5a6.73,6.73,0,0,0-4.1-1.46h-.11a8.22,8.22,0,0,1-1.41-.1l-2.3,1.45,1-1.79a8.19,8.19,0,0,1-4-3.05l-4.12,2.61,2.6-4.73a12.05,12.05,0,0,0-9.22-4.67c-4.29,0-8.1,2.55-10.57,6.52a7.87,7.87,0,0,1-7,3.76h-.23c-4.72,0-8.56,5.36-8.56,12s3.84,12,8.56,12a6.48,6.48,0,0,0,3-.74,12.3,12.3,0,0,1,10.36-.2,10.9,10.9,0,0,0,8.81,0,12.35,12.35,0,0,1,10.27.19,6.31,6.31,0,0,0,3,.73c4.72,0,8.56-5.36,8.56-12A15.22,15.22,0,0,0,427.5,275.35Z" transform="translate(-75 -152.99)" fill="#4e73df" opacity="0.1"/><rect x="505.46" y="102.97" width="371.54" height="447.42" rx="19.8" fill="#3f3d56"/><rect x="522" y="148.11" width="336" height="357.15" fill="#fff"/><circle cx="691.23" cy="528.8" r="13.08" fill="#fff"/><path d="M766.23,288.17a6,6,0,1,1,6-6A6,6,0,0,1,766.23,288.17Z" transform="translate(-75 -152.99)" fill="#fff"/><path d="M766.23,276.58a5.55,5.55,0,1,1-5.54,5.55,5.55,5.55,0,0,1,5.54-5.55m0-1a6.55,6.55,0,1,0,6.54,6.55,6.54,6.54,0,0,0-6.54-6.55Z" transform="translate(-75 -152.99)" fill="#fff"/><path d="M899.2,486.3s0-.08,0-.12a32.12,32.12,0,0,0-1.55-8.47l-.15-.46A32.51,32.51,0,0,0,885.09,461a32.23,32.23,0,0,0-42.25,5.22,47.14,47.14,0,0,0-2.57-9,144.23,144.23,0,0,1-25.59,1A102.72,102.72,0,0,0,830.9,464l-.2.74A103.56,103.56,0,0,1,812.57,458l-1.76-.85,2,.13a144.61,144.61,0,0,0,27.22-.78,46.08,46.08,0,0,0-8-12.73c-14.64-1.48-29.17-4.55-42.72-10.64l.31-.7c13.2,5.94,27.35,9,41.63,10.49a45.71,45.71,0,0,0-72.66,7.72A62.74,62.74,0,0,0,743,415.31a147.66,147.66,0,0,1-24.1,7.3,145.91,145.91,0,0,1-37.68,3,102.72,102.72,0,0,0,16.22,5.89l-.2.73a102.73,102.73,0,0,1-18.13-6.78l-1.76-.85,2,.13a144.71,144.71,0,0,0,63.16-10c-.75-.83-1.53-1.63-2.33-2.42-25,.5-52.29.26-77-8.73a62.93,62.93,0,0,0-29.89,53.62c0,11.81,3.25,28.06,8.9,44.36A187.93,187.93,0,0,0,651,522.91c1.72,3.51,3.55,6.92,5.48,10.18h0a117,117,0,0,0,7.65,11.46c.5.65,1,1.3,1.52,1.93a69.63,69.63,0,0,0,9.54,9.87h0c.71.59,1.42,1.15,2.14,1.68a40.31,40.31,0,0,0,6.11,3.81A28,28,0,0,0,695,565c.31,0,.63,0,.95,0v.13H866.81V565h.12a8.76,8.76,0,0,0,2.89-.51c6-2.15,11.61-10.76,16.23-21.64,1-2.41,2-4.93,2.94-7.52.82-2.29,1.59-4.63,2.33-7a215.07,215.07,0,0,0,5.38-21,110.27,110.27,0,0,0,2.53-19.58C899.23,487.27,899.22,486.79,899.2,486.3Z" transform="translate(-75 -152.99)" fill="#4e73df" opacity="0.1"/><path d="M739.31,411.54A62.72,62.72,0,0,0,664,403.05C688.11,411.7,714.76,412,739.31,411.54Z" transform="translate(-75 -152.99)" fill="#4e73df" opacity="0.1"/><path d="M879.3,552l-26.37-45.69a.84.84,0,0,0-1.46,0L825.09,552a.84.84,0,0,0,.73,1.26h7.87a.85.85,0,0,1,.84.84v9.76a.85.85,0,0,0,.84.84h10.27a.85.85,0,0,0,.84-.84v-4.71a.83.83,0,0,1,.84-.84h9.76a.84.84,0,0,1,.84.84v4.71a.85.85,0,0,0,.84.84H869a.84.84,0,0,0,.8-.57.86.86,0,0,0,0-.27v-9.76a.84.84,0,0,1,.84-.84h7.87A.84.84,0,0,0,879.3,552Zm-36.77,5a.86.86,0,0,1-.6.25h-3.7a.85.85,0,0,1-.84-.84.81.81,0,0,1,.25-.6.84.84,0,0,1,.59-.25h3.7a.85.85,0,0,1,.85.85A.84.84,0,0,1,842.53,557Zm24.23,0a.86.86,0,0,1-.6.25h-3.7a.85.85,0,0,1,0-1.69h3.7a.85.85,0,0,1,.85.85A.84.84,0,0,1,866.76,557Z" transform="translate(-75 -152.99)" fill="#4e73df" opacity="0.1"/><path d="M716.27,552.37,689.9,506.68a.84.84,0,0,0-1.46,0l-22.78,39.46A69.45,69.45,0,0,0,675.2,556h3.7a.84.84,0,0,1,.6,1.43.81.81,0,0,1-.6.25h-1.56a41,41,0,0,0,6.11,3.8v-2a.84.84,0,0,1,.84-.84h9.76a.85.85,0,0,1,.84.84v4.71a.78.78,0,0,0,.08.35.83.83,0,0,0,.76.49H706a.84.84,0,0,0,.67-.34.86.86,0,0,0,.17-.5v-9.76a.84.84,0,0,1,.84-.84h7.87A.84.84,0,0,0,716.27,552.37Zm-17.43,3.86a.83.83,0,0,1,.59-.24h3.71a.83.83,0,0,1,.59,1.43.8.8,0,0,1-.59.25h-3.71a.85.85,0,0,1-.84-.84A.86.86,0,0,1,698.84,556.23Zm-16.57-16.9a.84.84,0,0,1-.84-.85v-6.39a.84.84,0,0,1,.84-.84h13.8a.85.85,0,0,1,.84.84v6.39a.85.85,0,0,1-.84.85Z" transform="translate(-75 -152.99)" fill="#4e73df" opacity="0.1"/><path d="M820.53,470.3H787.89v-7.05a1.69,1.69,0,0,0-1.69-1.69h-9.58V454a1.06,1.06,0,0,0-1.06-1.06H763.21a1.05,1.05,0,0,0-1.06,1.06v7.52H750.88a1.69,1.69,0,0,0-1.69,1.69v7.05H718.07a.35.35,0,0,0-.35.34v93.72a.35.35,0,0,0,.35.34h20.68a.34.34,0,0,0,.34-.34V551.75a.35.35,0,0,1,.35-.34h8.73a.35.35,0,0,1,.35.34v12.61a.34.34,0,0,0,.34.34h40.88a.34.34,0,0,0,.34-.34V551.75a.34.34,0,0,1,.34-.34h8.74a.34.34,0,0,1,.34.34v12.61a.35.35,0,0,0,.35.34h20.68a.34.34,0,0,0,.34-.34V470.64A.34.34,0,0,0,820.53,470.3ZM735.39,547a.34.34,0,0,1-.34.34H722.27a.34.34,0,0,1-.34-.34v-4.37a.34.34,0,0,1,.34-.34h12.78a.34.34,0,0,1,.34.34Zm0-11.28a.34.34,0,0,1-.34.34H722.27a.34.34,0,0,1-.34-.34v-4.36a.34.34,0,0,1,.34-.34h12.78a.34.34,0,0,1,.34.34Zm0-11.27a.34.34,0,0,1-.34.34H722.27a.34.34,0,0,1-.34-.34v-4.37a.34.34,0,0,1,.34-.34h12.78a.34.34,0,0,1,.34.34Zm0-11.28a.35.35,0,0,1-.34.35H722.27a.35.35,0,0,1-.34-.35v-4.36a.34.34,0,0,1,.34-.34h12.78a.34.34,0,0,1,.34.34Zm0-11.27a.34.34,0,0,1-.34.34H722.27a.34.34,0,0,1-.34-.34v-4.37a.34.34,0,0,1,.34-.34h12.78a.34.34,0,0,1,.34.34Zm0-11.27a.34.34,0,0,1-.34.34H722.27a.34.34,0,0,1-.34-.34v-4.37a.34.34,0,0,1,.34-.34h12.78a.34.34,0,0,1,.34.34Zm0-11.28a.34.34,0,0,1-.34.34H722.27a.34.34,0,0,1-.34-.34V475a.34.34,0,0,1,.34-.34h12.78a.34.34,0,0,1,.34.34ZM765.68,547a.34.34,0,0,1-.34.34H752.56a.34.34,0,0,1-.34-.34v-4.37a.34.34,0,0,1,.34-.34h12.78a.34.34,0,0,1,.34.34Zm0-11.28a.34.34,0,0,1-.34.34H752.56a.34.34,0,0,1-.34-.34v-4.36a.34.34,0,0,1,.34-.34h12.78a.34.34,0,0,1,.34.34Zm0-11.27a.34.34,0,0,1-.34.34H752.56a.34.34,0,0,1-.34-.34v-4.37a.34.34,0,0,1,.34-.34h12.78a.34.34,0,0,1,.34.34Zm0-11.28a.35.35,0,0,1-.34.35H752.56a.35.35,0,0,1-.34-.35v-4.36a.34.34,0,0,1,.34-.34h12.78a.34.34,0,0,1,.34.34Zm0-11.27a.34.34,0,0,1-.34.34H752.56a.34.34,0,0,1-.34-.34v-4.37a.34.34,0,0,1,.34-.34h12.78a.34.34,0,0,1,.34.34Zm0-11.27a.34.34,0,0,1-.34.34H752.56a.34.34,0,0,1-.34-.34v-4.37a.34.34,0,0,1,.34-.34h12.78a.34.34,0,0,1,.34.34Zm0-11.28a.34.34,0,0,1-.34.34H752.56a.34.34,0,0,1-.34-.34V475a.34.34,0,0,1,.34-.34h12.78a.34.34,0,0,1,.34.34ZM786.38,547a.34.34,0,0,1-.34.34H773.26a.34.34,0,0,1-.34-.34v-4.37a.34.34,0,0,1,.34-.34H786a.34.34,0,0,1,.34.34Zm0-11.28a.34.34,0,0,1-.34.34H773.26a.34.34,0,0,1-.34-.34v-4.36a.34.34,0,0,1,.34-.34H786a.34.34,0,0,1,.34.34Zm0-11.27a.34.34,0,0,1-.34.34H773.26a.34.34,0,0,1-.34-.34v-4.37a.34.34,0,0,1,.34-.34H786a.34.34,0,0,1,.34.34Zm0-11.28a.35.35,0,0,1-.34.35H773.26a.35.35,0,0,1-.34-.35v-4.36a.34.34,0,0,1,.34-.34H786a.34.34,0,0,1,.34.34Zm0-11.27a.34.34,0,0,1-.34.34H773.26a.34.34,0,0,1-.34-.34v-4.37a.34.34,0,0,1,.34-.34H786a.34.34,0,0,1,.34.34Zm0-11.27a.34.34,0,0,1-.34.34H773.26a.34.34,0,0,1-.34-.34v-4.37a.34.34,0,0,1,.34-.34H786a.34.34,0,0,1,.34.34Zm0-11.28a.34.34,0,0,1-.34.34H773.26a.34.34,0,0,1-.34-.34V475a.34.34,0,0,1,.34-.34H786a.34.34,0,0,1,.34.34ZM816.67,547a.35.35,0,0,1-.34.34H803.55a.34.34,0,0,1-.34-.34v-4.37a.34.34,0,0,1,.34-.34h12.78a.34.34,0,0,1,.34.34Zm0-11.28a.34.34,0,0,1-.34.34H803.55a.34.34,0,0,1-.34-.34v-4.36a.34.34,0,0,1,.34-.34h12.78a.34.34,0,0,1,.34.34Zm0-11.27a.34.34,0,0,1-.34.34H803.55a.34.34,0,0,1-.34-.34v-4.37a.34.34,0,0,1,.34-.34h12.78a.35.35,0,0,1,.34.34Zm0-11.28a.35.35,0,0,1-.34.35H803.55a.35.35,0,0,1-.34-.35v-4.36a.34.34,0,0,1,.34-.34h12.78a.34.34,0,0,1,.34.34Zm0-11.27a.34.34,0,0,1-.34.34H803.55a.34.34,0,0,1-.34-.34v-4.37a.34.34,0,0,1,.34-.34h12.78a.35.35,0,0,1,.34.34Zm0-11.27a.35.35,0,0,1-.34.34H803.55a.34.34,0,0,1-.34-.34v-4.37a.34.34,0,0,1,.34-.34h12.78a.34.34,0,0,1,.34.34Zm0-11.28a.34.34,0,0,1-.34.34H803.55a.34.34,0,0,1-.34-.34V475a.34.34,0,0,1,.34-.34h12.78a.34.34,0,0,1,.34.34Z" transform="translate(-75 -152.99)" fill="#4e73df" opacity="0.1"/><path d="M727.56,444.9a7.39,7.39,0,0,0-4.26.46,6.34,6.34,0,0,1-2.55.54,6.24,6.24,0,0,1-2.57-.55,7.18,7.18,0,0,0-6,.12,3.72,3.72,0,0,1-1.73.43c-2.44,0-4.47-2.46-4.89-5.7a4.82,4.82,0,0,0,1.22-1.32,6.86,6.86,0,0,1,12.25,0,4.68,4.68,0,0,0,4,2.23h.07C725.06,441.07,726.74,442.62,727.56,444.9Z" transform="translate(-75 -152.99)" fill="#4e73df" opacity="0.1"/><path d="M735,434.71l-4,2.51,2.4-4.36a3.92,3.92,0,0,0-2.39-.85H731a5.55,5.55,0,0,1-.82-.06l-1.34.84.58-1a4.72,4.72,0,0,1-2.34-1.77l-2.4,1.51,1.52-2.75a7,7,0,0,0-5.37-2.71,7.35,7.35,0,0,0-6.14,3.79,4.6,4.6,0,0,1-4.06,2.19h-.13c-2.75,0-5,3.11-5,7s2.23,7,5,7a3.73,3.73,0,0,0,1.73-.44,7.18,7.18,0,0,1,6-.11,6.41,6.41,0,0,0,2.57.55,6.34,6.34,0,0,0,2.55-.54,7.19,7.19,0,0,1,6,.11,3.64,3.64,0,0,0,1.71.43c2.75,0,5-3.12,5-7A8.86,8.86,0,0,0,735,434.71Z" transform="translate(-75 -152.99)" fill="#4e73df" opacity="0.1"/><rect x="988.64" y="269.17" width="5.36" height="44.17" rx="2.29" fill="#3f3d56"/><rect x="810.55" y="234.57" width="3.01" height="14.54" rx="1.5" fill="#3f3d56"/><rect x="810.44" y="261.19" width="3.39" height="25.31" rx="1.69" fill="#3f3d56"/><rect x="810.49" y="295.35" width="3.23" height="25.53" rx="1.61" fill="#3f3d56"/><rect x="812.25" y="186.51" width="179.29" height="364.37" rx="18.54" fill="#3f3d56"/><rect x="884.6" y="197.39" width="25.04" height="5.08" rx="2.54" fill="#e6e8ec"/><circle cx="916.3" cy="199.94" r="2.88" fill="#e6e8ec"/><path d="M1041.22,349H1020.7v2.47A11.73,11.73,0,0,1,1009,363.19H943.54a11.73,11.73,0,0,1-11.73-11.73V349H912.56a14.25,14.25,0,0,0-14.24,14.24V680.14a14.24,14.24,0,0,0,14.24,14.24h128.66a14.23,14.23,0,0,0,14.24-14.24V363.23A14.24,14.24,0,0,0,1041.22,349Z" transform="translate(-75 -152.99)" fill="#fff"/><path d="M1037.2,524.68v0a14.33,14.33,0,0,0-.7-3.83,1.72,1.72,0,0,0-.07-.21,14.56,14.56,0,0,0-24.65-5,20.46,20.46,0,0,0-1.16-4.07,65.66,65.66,0,0,1-11.55.43,47.79,47.79,0,0,0,7.32,2.66l-.09.33a46.82,46.82,0,0,1-8.18-3.06l-.79-.39.88.06a65.38,65.38,0,0,0,12.28-.35,20.79,20.79,0,0,0-3.62-5.75,61.91,61.91,0,0,1-19.27-4.79l.14-.32a60.89,60.89,0,0,0,18.78,4.73,20.63,20.63,0,0,0-32.78,3.49,28.35,28.35,0,0,0-7-15.94,66.32,66.32,0,0,1-27.87,4.64,46.76,46.76,0,0,0,7.32,2.66l-.09.33a46.82,46.82,0,0,1-8.18-3.06l-.79-.38.88.06a65.26,65.26,0,0,0,28.49-4.52c-.34-.37-.69-.73-1.05-1.09-11.29.23-23.59.12-34.72-3.94a28.4,28.4,0,0,0-13.48,24.19c0,5.33,1.46,12.66,4,20a83.69,83.69,0,0,0,4,9.64q1.17,2.38,2.47,4.6h0a54,54,0,0,0,3.45,5.17l.69.87a32,32,0,0,0,4.3,4.45h0c.32.27.64.52,1,.76a18.94,18.94,0,0,0,2.75,1.72,12.92,12.92,0,0,0,5.2,1.41h.43v.05h77.09v-.05h.06a3.92,3.92,0,0,0,1.3-.23c2.73-1,5.24-4.85,7.32-9.76.47-1.09.91-2.23,1.33-3.4s.72-2.08,1.05-3.15c1-3.2,1.82-6.49,2.43-9.47a50.32,50.32,0,0,0,1.14-8.83C1037.22,525.12,1037.21,524.9,1037.2,524.68Z" transform="translate(-75 -152.99)" fill="#4e73df" opacity="0.1"/><path d="M965.07,491a28.3,28.3,0,0,0-34-3.83C942,491,954,491.17,965.07,491Z" transform="translate(-75 -152.99)" fill="#4e73df" opacity="0.1"/><path d="M1028.23,554.3l-11.9-20.61a.38.38,0,0,0-.66,0l-11.9,20.61a.38.38,0,0,0,.33.57h3.55a.38.38,0,0,1,.38.38v4.4a.38.38,0,0,0,.38.38H1013a.38.38,0,0,0,.38-.38v-2.12a.38.38,0,0,1,.38-.38h4.4a.38.38,0,0,1,.38.38v2.12a.38.38,0,0,0,.38.38h4.63a.38.38,0,0,0,.36-.26.37.37,0,0,0,0-.12v-4.4a.38.38,0,0,1,.38-.38h3.55A.38.38,0,0,0,1028.23,554.3Zm-16.59,2.28a.39.39,0,0,1-.27.11h-1.67a.38.38,0,0,1-.38-.38.35.35,0,0,1,.11-.26.4.4,0,0,1,.27-.12h1.67a.38.38,0,0,1,.38.38A.37.37,0,0,1,1011.64,556.58Zm10.93,0a.37.37,0,0,1-.27.11h-1.67a.38.38,0,0,1-.38-.38.35.35,0,0,1,.11-.26.4.4,0,0,1,.27-.12h1.67a.38.38,0,0,1,.38.38A.37.37,0,0,1,1022.57,556.58Z" transform="translate(-75 -152.99)" fill="#4e73df" opacity="0.1"/><path d="M954.68,554.48l-11.9-20.61a.38.38,0,0,0-.66,0l-10.27,17.8a32,32,0,0,0,4.3,4.45h1.67a.38.38,0,1,1,0,.75h-.7a18.94,18.94,0,0,0,2.75,1.72v-.88a.38.38,0,0,1,.38-.38h4.41a.38.38,0,0,1,.37.38v2.12a.39.39,0,0,0,.38.38h4.64a.38.38,0,0,0,.3-.15.35.35,0,0,0,.07-.23v-4.4a.38.38,0,0,1,.38-.38h3.55A.38.38,0,0,0,954.68,554.48Zm-7.86,1.75a.35.35,0,0,1,.26-.11h1.67a.38.38,0,1,1,0,.75h-1.67a.38.38,0,0,1-.38-.38A.37.37,0,0,1,946.82,556.23Zm-7.48-7.63a.38.38,0,0,1-.38-.38v-2.88a.38.38,0,0,1,.38-.38h6.23a.38.38,0,0,1,.38.38v2.88a.38.38,0,0,1-.38.38Z" transform="translate(-75 -152.99)" fill="#4e73df" opacity="0.1"/><path d="M1001.72,517.46H987v-3.19a.76.76,0,0,0-.76-.76H981.9v-3.39a.48.48,0,0,0-.47-.48h-5.57a.48.48,0,0,0-.48.48v3.39h-5.09a.76.76,0,0,0-.76.76v3.19h-14a.15.15,0,0,0-.15.15v42.28a.16.16,0,0,0,.15.16h9.33a.16.16,0,0,0,.16-.16V554.2a.15.15,0,0,1,.15-.15h3.94a.16.16,0,0,1,.16.15v5.69a.16.16,0,0,0,.15.16h18.44a.16.16,0,0,0,.16-.16V554.2a.15.15,0,0,1,.15-.15h3.94a.16.16,0,0,1,.16.15v5.69a.16.16,0,0,0,.15.16h9.34a.16.16,0,0,0,.15-.16V517.61A.15.15,0,0,0,1001.72,517.46Zm-38.41,34.61a.16.16,0,0,1-.16.16h-5.76a.16.16,0,0,1-.16-.16v-2a.16.16,0,0,1,.16-.15h5.76a.16.16,0,0,1,.16.15Zm0-5.08a.16.16,0,0,1-.16.15h-5.76a.16.16,0,0,1-.16-.15v-2a.16.16,0,0,1,.16-.16h5.76a.16.16,0,0,1,.16.16Zm0-5.09a.16.16,0,0,1-.16.16h-5.76a.16.16,0,0,1-.16-.16v-2a.16.16,0,0,1,.16-.15h5.76a.16.16,0,0,1,.16.15Zm0-5.09a.16.16,0,0,1-.16.16h-5.76a.16.16,0,0,1-.16-.16v-2a.16.16,0,0,1,.16-.16h5.76a.16.16,0,0,1,.16.16Zm0-5.08a.16.16,0,0,1-.16.15h-5.76a.16.16,0,0,1-.16-.15v-2a.16.16,0,0,1,.16-.15h5.76a.16.16,0,0,1,.16.15Zm0-5.09a.16.16,0,0,1-.16.16h-5.76a.16.16,0,0,1-.16-.16v-2a.16.16,0,0,1,.16-.15h5.76a.16.16,0,0,1,.16.15Zm0-5.08a.16.16,0,0,1-.16.15h-5.76a.16.16,0,0,1-.16-.15v-2a.16.16,0,0,1,.16-.16h5.76a.16.16,0,0,1,.16.16ZM977,552.07a.16.16,0,0,1-.15.16h-5.77a.16.16,0,0,1-.15-.16v-2a.15.15,0,0,1,.15-.15h5.77a.15.15,0,0,1,.15.15Zm0-5.08a.15.15,0,0,1-.15.15h-5.77a.15.15,0,0,1-.15-.15v-2a.16.16,0,0,1,.15-.16h5.77a.16.16,0,0,1,.15.16Zm0-5.09a.16.16,0,0,1-.15.16h-5.77a.16.16,0,0,1-.15-.16v-2a.15.15,0,0,1,.15-.15h5.77a.15.15,0,0,1,.15.15Zm0-5.09a.16.16,0,0,1-.15.16h-5.77a.16.16,0,0,1-.15-.16v-2a.16.16,0,0,1,.15-.16h5.77a.16.16,0,0,1,.15.16Zm0-5.08a.15.15,0,0,1-.15.15h-5.77a.15.15,0,0,1-.15-.15v-2a.15.15,0,0,1,.15-.15h5.77a.15.15,0,0,1,.15.15Zm0-5.09a.16.16,0,0,1-.15.16h-5.77a.16.16,0,0,1-.15-.16v-2a.15.15,0,0,1,.15-.15h5.77a.15.15,0,0,1,.15.15Zm0-5.08a.15.15,0,0,1-.15.15h-5.77a.15.15,0,0,1-.15-.15v-2a.16.16,0,0,1,.15-.16h5.77a.16.16,0,0,1,.15.16Zm9.34,30.51a.16.16,0,0,1-.16.16h-5.76a.16.16,0,0,1-.16-.16v-2a.16.16,0,0,1,.16-.15h5.76a.16.16,0,0,1,.16.15Zm0-5.08a.16.16,0,0,1-.16.15h-5.76a.16.16,0,0,1-.16-.15v-2a.16.16,0,0,1,.16-.16h5.76a.16.16,0,0,1,.16.16Zm0-5.09a.16.16,0,0,1-.16.16h-5.76a.16.16,0,0,1-.16-.16v-2a.16.16,0,0,1,.16-.15h5.76a.16.16,0,0,1,.16.15Zm0-5.09a.16.16,0,0,1-.16.16h-5.76a.16.16,0,0,1-.16-.16v-2a.16.16,0,0,1,.16-.16h5.76a.16.16,0,0,1,.16.16Zm0-5.08a.16.16,0,0,1-.16.15h-5.76a.16.16,0,0,1-.16-.15v-2a.16.16,0,0,1,.16-.15h5.76a.16.16,0,0,1,.16.15Zm0-5.09a.16.16,0,0,1-.16.16h-5.76a.16.16,0,0,1-.16-.16v-2a.16.16,0,0,1,.16-.15h5.76a.16.16,0,0,1,.16.15Zm0-5.08a.16.16,0,0,1-.16.15h-5.76a.16.16,0,0,1-.16-.15v-2a.16.16,0,0,1,.16-.16h5.76a.16.16,0,0,1,.16.16ZM1000,552.07a.16.16,0,0,1-.15.16h-5.77a.16.16,0,0,1-.15-.16v-2a.15.15,0,0,1,.15-.15h5.77a.15.15,0,0,1,.15.15Zm0-5.08a.15.15,0,0,1-.15.15h-5.77a.15.15,0,0,1-.15-.15v-2a.16.16,0,0,1,.15-.16h5.77a.16.16,0,0,1,.15.16Zm0-5.09a.16.16,0,0,1-.15.16h-5.77a.16.16,0,0,1-.15-.16v-2a.15.15,0,0,1,.15-.15h5.77a.15.15,0,0,1,.15.15Zm0-5.09a.16.16,0,0,1-.15.16h-5.77a.16.16,0,0,1-.15-.16v-2a.16.16,0,0,1,.15-.16h5.77a.16.16,0,0,1,.15.16Zm0-5.08a.15.15,0,0,1-.15.15h-5.77a.15.15,0,0,1-.15-.15v-2a.15.15,0,0,1,.15-.15h5.77a.15.15,0,0,1,.15.15Zm0-5.09a.16.16,0,0,1-.15.16h-5.77a.16.16,0,0,1-.15-.16v-2a.15.15,0,0,1,.15-.15h5.77a.15.15,0,0,1,.15.15Zm0-5.08a.15.15,0,0,1-.15.15h-5.77a.15.15,0,0,1-.15-.15v-2a.16.16,0,0,1,.15-.16h5.77a.16.16,0,0,1,.15.16Z" transform="translate(-75 -152.99)" fill="#4e73df" opacity="0.1"/><path d="M959.77,506a3.33,3.33,0,0,0-1.92.21,2.82,2.82,0,0,1-1.15.24,2.72,2.72,0,0,1-1.16-.25,3.27,3.27,0,0,0-2.72.06,1.73,1.73,0,0,1-.78.19c-1.1,0-2-1.11-2.21-2.57a2.06,2.06,0,0,0,.55-.59,3.1,3.1,0,0,1,5.53,0,2.11,2.11,0,0,0,1.83,1h0A2.28,2.28,0,0,1,959.77,506Z" transform="translate(-75 -152.99)" fill="#4e73df" opacity="0.1"/><path d="M963.13,501.41l-1.78,1.12,1.08-2a1.75,1.75,0,0,0-1.08-.39h0a1.51,1.51,0,0,1-.37,0l-.61.38.26-.47a2.19,2.19,0,0,1-1.05-.8l-1.08.68.68-1.24a3.16,3.16,0,0,0-2.42-1.22A3.3,3.3,0,0,0,954,499.2a2.09,2.09,0,0,1-1.83,1h-.06c-1.24,0-2.25,1.41-2.25,3.14s1,3.14,2.25,3.14a1.64,1.64,0,0,0,.78-.19,3.23,3.23,0,0,1,2.72,0,2.9,2.9,0,0,0,2.31,0,3.24,3.24,0,0,1,2.69,0,1.6,1.6,0,0,0,.77.19c1.24,0,2.25-1.4,2.25-3.14A4,4,0,0,0,963.13,501.41Z" transform="translate(-75 -152.99)" fill="#4e73df" opacity="0.1"/><path d="M174.65,693.59a37,37,0,0,1-.8,7.76c-.1.48-.21.95-.32,1.41-2.84,11.39-10.85,19.72-20.41,20.25-.32,0-.64,0-1,0-10.11,0-18.66-8.72-21.48-20.73-.08-.32-.15-.64-.22-1a37,37,0,0,1-.8-7.76c0-16.27,10.07-29.45,22.5-29.45S174.65,677.32,174.65,693.59Z" transform="translate(-75 -152.99)" fill="#3f3d56"/><path d="M174.65,693.59a37,37,0,0,1-.8,7.76c-.1.48-.21.95-.32,1.41-.34,0-.67,0-1,0a45.76,45.76,0,0,1-7.36-1,44.92,44.92,0,0,1-6.56,1.5,45.87,45.87,0,0,1-5.14.48l-1.74,0a46.41,46.41,0,0,1-6.16-.41,45.17,45.17,0,0,1-9.67-2.4,45.56,45.56,0,0,1-5.22,1.4c-.08-.32-.15-.64-.22-1a37,37,0,0,1-.8-7.76c0-16.27,10.07-29.45,22.5-29.45S174.65,677.32,174.65,693.59Z" transform="translate(-75 -152.99)" opacity="0.1"/><path d="M222.65,638.72a45.6,45.6,0,0,0-4.9-20.61l-26.46,8.23,23.23-13.65a45.71,45.71,0,0,0-34.36-19.59,45.65,45.65,0,0,0-3.57-4.72l-38,11.83,31.17-18.33a45.73,45.73,0,0,0-72,24.39l32.55,37.47L95,618.2a45.74,45.74,0,0,0,40.93,80.7,45.92,45.92,0,0,0,29.28.81,45.74,45.74,0,0,0,55.62-44.66c0-1,0-2-.1-3A45.74,45.74,0,0,0,222.65,638.72Z" transform="translate(-75 -152.99)" fill="#4e73df"/><path d="M221.86,647.2a122.14,122.14,0,0,0-42.34-.54c-15.89,2.63-32.13,8.42-47.67,4.19-9.12-2.48-17-8.22-25.91-11.41a49.18,49.18,0,0,0-26.75-1.6,45.76,45.76,0,0,0,56.69,61.06,45.92,45.92,0,0,0,29.28.81,45.74,45.74,0,0,0,55.62-44.66c0-1,0-2-.1-3A46,46,0,0,0,221.86,647.2Z" transform="translate(-75 -152.99)" opacity="0.1"/><path d="M568.71,359.13l-19-.84c-4.35-.19-9.31-.13-12.21,3.11-3.09,3.45-2.28,8.87-.46,13.14s4.48,8.36,4.62,13c.21,7.3-5.78,13.08-11.21,18s-11.23,11-10.44,18.27c.62,5.67,5.14,10.05,9.6,13.59a128.25,128.25,0,0,0,21.85,14c4.43,2.24,9.15,4.26,14.12,4.31,4.36.05,8.58-1.42,12.68-2.9,7.13-2.56,14.45-5.33,19.86-10.62a39.92,39.92,0,0,0,6.9-9.54c9.35-17,11.84-38.26,4.24-56.13a31.55,31.55,0,0,0-7-10.66c-8.1-7.67-20.46-8-31.62-7.91a12.39,12.39,0,0,0-5.92,1.05c-1.77,1-3,3.23-2.18,5.08" transform="translate(-75 -152.99)" fill="#393859"/><path d="M544.39,594.37a411.28,411.28,0,0,0,2.24,60.27c.26,2.29.53,4.58,1,6.84,1.28,7,3.9,13.71,5.95,20.54s3.57,14,2.56,21.07q6.6-.69,13.22-1.13l.06-3.4c.12-6.52-2.44-12.88-1.79-19.37.62-6.14,2.6-12.14,2.51-18.31-.09-5.83-2-11.45-2.91-17.21a97.73,97.73,0,0,1-.82-11.6,146.49,146.49,0,0,1,.1-16c.84-10.82,4.06-21.6,2.57-32.35C560.92,587.48,552.5,590.65,544.39,594.37Z" transform="translate(-75 -152.99)" fill="#a0616a"/><path d="M564.36,726.39a3,3,0,0,0,1.47-.24A3.2,3.2,0,0,0,567,724a26.23,26.23,0,0,1,3.36-7.55c1.13-1.73,2.51-3.49,2.53-5.55,0-3.33-3.48-5.66-4.21-8.91a14.38,14.38,0,0,0-.62-3.08c-.82-1.75-3-2.35-4.88-2.74-2.56-.54-5.87-.76-7.14,1.52-1.08,1.94.12,4.3.28,6.51.2,3-1.58,5.76-3.5,8.07-2.25,2.72-8.16,9.23-4.56,13,1.35,1.42,4.29.94,6,1Z" transform="translate(-75 -152.99)" fill="#3f3d56"/><path d="M603,594.37a409.91,409.91,0,0,1-2.25,60.27c-.25,2.29-.52,4.58-.94,6.84-1.28,7-3.9,13.71-6,20.54s-3.56,14-2.55,21.07q-6.6-.69-13.22-1.13l-.06-3.4c-.12-6.52,2.43-12.88,1.78-19.37-.61-6.14-2.59-12.14-2.5-18.31.09-5.83,2-11.45,2.9-17.21a95.67,95.67,0,0,0,.83-11.6,146.49,146.49,0,0,0-.1-16c-.84-10.82-4.07-21.6-2.57-32.35C586.51,587.48,594.92,590.65,603,594.37Z" transform="translate(-75 -152.99)" fill="#a0616a"/><path d="M583.06,726.39a3,3,0,0,1-1.46-.24,3.2,3.2,0,0,1-1.21-2.15,26.23,26.23,0,0,0-3.36-7.55c-1.13-1.73-2.52-3.49-2.53-5.55,0-3.33,3.48-5.66,4.2-8.91a14.39,14.39,0,0,1,.63-3.08c.82-1.75,3-2.35,4.88-2.74,2.56-.54,5.86-.76,7.14,1.52,1.08,1.94-.13,4.3-.28,6.51-.21,3,1.58,5.76,3.5,8.07,2.25,2.72,8.16,9.23,4.56,13-1.35,1.42-4.29.94-6,1Z" transform="translate(-75 -152.99)" fill="#3f3d56"/><circle cx="492.31" cy="243.59" r="20.49" fill="#a0616a"/><path d="M555.7,415.05A7,7,0,0,1,556,418a5.74,5.74,0,0,1-1.72,2.66,30.58,30.58,0,0,1-10.15,6.77c-.4,1.29.35,2.62,1.08,3.74,2.46,3.79,5.06,7.47,7.67,11.16a74.72,74.72,0,0,0,17.43,18.77l6-7.54a48.75,48.75,0,0,0,3.48-4.73,47.18,47.18,0,0,0,3.12-6.39l7.33-17c-4.41.89-8.89-1.14-12.66-3.61a4.94,4.94,0,0,1-1.84-1.76,5,5,0,0,1-.4-2.18q-.18-5.3-.15-10.59a136.14,136.14,0,0,0-16-1.25c-2.08,0-3.48-.44-4,1.76S555.25,412.92,555.7,415.05Z" transform="translate(-75 -152.99)" fill="#a0616a"/><path d="M564.8,453.61a78.77,78.77,0,0,0-12-16,10.8,10.8,0,0,1-2.51-3.29,10,10,0,0,1-.56-3.18l-.36-5.29a1.23,1.23,0,0,0-.27-.82,1.19,1.19,0,0,0-.81-.23,6.77,6.77,0,0,0-3.71.54,10.4,10.4,0,0,0-2,1.84c-2.29,2.35-5.51,3.46-8.53,4.73a68.63,68.63,0,0,0-6.58,3.17c-1.69.93-3.43,2-4.19,3.8a9.39,9.39,0,0,0-.54,3.58L522,485a2.25,2.25,0,0,1-1.68,2.51,6.1,6.1,0,0,0-2.32,1.77,2.4,2.4,0,0,0-.09,2.75c.3.39.76.63,1.07,1,1,1.19.13,2.91-.54,4.29a9.32,9.32,0,0,0-1,5.81,5,5,0,0,0,3.92,4c.79-2.55-1-5.44,0-7.91a5.67,5.67,0,0,1,3.42-2.85,12.29,12.29,0,0,1,6.24-.68l1.2,4.37c4.31,15.74,1.32,32.65-2.61,48.49-.91,3.7-2.06,7.34-2.74,11.09-1,5.63-1,11.38-1,17.1l0,15.64c0,1.9.12,4.05,1.57,5.28s4.12,1.14,5.21,2.84c.63,1,.51,2.29,1.08,3.31a4.38,4.38,0,0,0,1.7,1.55,11.44,11.44,0,0,0,14.91-3.66c1.08-1.71,1.73-3.78,3.33-5,1.83-1.43,4.37-1.39,6.68-1.28l13.62.65a28.19,28.19,0,0,1,6.92.91,53.46,53.46,0,0,1,5.88,2.54,30.29,30.29,0,0,0,13.77,2.5,8,8,0,0,0,3.72-.9c1.3-.78,2.2-2.2,3.63-2.73,1.61-.6,3.68,0,4.93-1.17a4.47,4.47,0,0,0,1-3.17l.57-13.37c.6-13.79,1.13-28-3.42-41-1.11-3.17-2.51-6.24-3.36-9.49A93.86,93.86,0,0,1,606,520c-1.41-9.51-4.62-18.87-4-28.46,0-.73.64-.9,1.36-1,2.54-.25,5.31.22,7.12,2,1.61,1.6,2.18,4,2.36,6.23a7,7,0,0,1-.86,4.68l3.76-2.76c1.2-.88,2.54-2.09,2.3-3.56-.71-4.37-.47-9.26-1-13.65a199.36,199.36,0,0,0-3.59-22.16A58.25,58.25,0,0,1,612,455c-.36-2.92-.16-5.87-.35-8.81a33.18,33.18,0,0,0-3.14-12,12.26,12.26,0,0,0-2-3.18,13.94,13.94,0,0,0-3.88-2.68A79.93,79.93,0,0,0,583,421.2c-1.35-.29-2.9-.51-4,.33-1.4,1.08-1.24,3.25-.62,4.91s1.57,3.33,1.3,5.08-1.76,3.13-3,4.52c-3.17,3.71-4.61,8.71-8,12.25C567.23,449.92,565.13,451.37,564.8,453.61Z" transform="translate(-75 -152.99)" fill="#ff6f61"/><path d="M552.07,386.46c1.73,1.44,3.18,3.44,5.38,3.91,5.3,1.12,9.44-7.45,14.54-5.62,1.86.67,3,2.52,4.43,3.82a8.77,8.77,0,0,0,12-.87c2.68-3.13,2.71-7.65,2.61-11.77a8.85,8.85,0,0,0-.89-4.42,7.12,7.12,0,0,0-1.77-1.86c-6.05-4.73-14.22-5.41-21.88-5.9a16,16,0,0,0-5,.21c-2.28.58-4.16,2.12-6,3.62-3.44,2.86-11.81,8.3-12.86,12.91S548.67,383.64,552.07,386.46Z" transform="translate(-75 -152.99)" fill="#393859"/><rect x="453.12" y="295.07" width="74.28" height="80.43" rx="2.61" fill="#4e73df"/><rect x="459.52" y="302.01" width="61.48" height="48.65" rx="2.61" fill="#fff"/><path d="M588,480.46v0a5.8,5.8,0,0,0-.27-1.46l0-.07a5.53,5.53,0,0,0-9.36-1.9,7.35,7.35,0,0,0-.44-1.54,24.69,24.69,0,0,1-4.38.16,16.21,16.21,0,0,0,2.78,1l0,.13a18.07,18.07,0,0,1-3.1-1.16l-.3-.15.33,0a24.17,24.17,0,0,0,4.66-.13,7.72,7.72,0,0,0-1.37-2.18,23.49,23.49,0,0,1-7.32-1.82l.06-.12a23.14,23.14,0,0,0,7.13,1.79,7.83,7.83,0,0,0-12.45,1.33,10.74,10.74,0,0,0-2.67-6.05,25.14,25.14,0,0,1-10.58,1.76,18.48,18.48,0,0,0,2.78,1l0,.12a18.07,18.07,0,0,1-3.1-1.16l-.3-.14.33,0a24.79,24.79,0,0,0,10.82-1.72c-.13-.14-.26-.28-.4-.41-4.29.09-8.95,0-13.18-1.5a10.8,10.8,0,0,0-5.12,9.19,25.33,25.33,0,0,0,1.52,7.6,32.44,32.44,0,0,0,1.52,3.66c.3.6.61,1.18.94,1.74h0a20.87,20.87,0,0,0,1.31,2l.26.33a11.48,11.48,0,0,0,1.64,1.69h0a4.57,4.57,0,0,0,.36.29,7.58,7.58,0,0,0,1.05.66,4.87,4.87,0,0,0,2,.53h.17v0h29.27v0h0a1.62,1.62,0,0,0,.49-.08c1-.37,2-1.85,2.79-3.71.17-.41.34-.84.5-1.29s.27-.79.4-1.19c.38-1.22.69-2.47.92-3.6a18.77,18.77,0,0,0,.43-3.35Z" transform="translate(-75 -152.99)" fill="#4e73df" opacity="0.3"/><path d="M560.65,467.65a10.72,10.72,0,0,0-12.91-1.45C551.88,467.68,556.44,467.73,560.65,467.65Z" transform="translate(-75 -152.99)" fill="#4e73df" opacity="0.3"/><path d="M584.63,491.7l-4.52-7.82a.14.14,0,0,0-.25,0l-4.52,7.82a.15.15,0,0,0,.13.22h1.35a.14.14,0,0,1,.14.14v1.68a.14.14,0,0,0,.14.14h1.76a.15.15,0,0,0,.15-.14v-.81a.15.15,0,0,1,.14-.15h1.67a.15.15,0,0,1,.15.15v.81a.14.14,0,0,0,.14.14h1.76a.13.13,0,0,0,.13-.1.06.06,0,0,0,0,0v-1.68a.15.15,0,0,1,.15-.14h1.34A.15.15,0,0,0,584.63,491.7Zm-6.3.87a.13.13,0,0,1-.1,0h-.64a.14.14,0,0,1-.14-.14.15.15,0,0,1,0-.1.14.14,0,0,1,.1-.05h.64a.15.15,0,0,1,.14.15A.13.13,0,0,1,578.33,492.57Zm4.15,0a.13.13,0,0,1-.1,0h-.64a.14.14,0,0,1-.14-.14.15.15,0,0,1,0-.1.14.14,0,0,1,.1-.05h.64a.15.15,0,0,1,.14.15A.13.13,0,0,1,582.48,492.57Z" transform="translate(-75 -152.99)" fill="#4e73df" opacity="0.3"/><path d="M556.7,491.77,552.19,484a.14.14,0,0,0-.25,0L548,490.71a11.91,11.91,0,0,0,1.64,1.68h.63a.15.15,0,0,1,.15.15.14.14,0,0,1-.05.1.15.15,0,0,1-.1,0H550a8.55,8.55,0,0,0,1.05.65V493a.15.15,0,0,1,.14-.15h1.68a.15.15,0,0,1,.14.15v.8a.35.35,0,0,0,0,.06.17.17,0,0,0,.13.09h1.76a.14.14,0,0,0,.12-.06.12.12,0,0,0,0-.09v-1.67a.14.14,0,0,1,.14-.14h1.35A.14.14,0,0,0,556.7,491.77Zm-3,.66a.15.15,0,0,1,.1,0h.63a.15.15,0,0,1,.15.15.14.14,0,0,1-.05.1.15.15,0,0,1-.1,0h-.63a.15.15,0,0,1-.15-.14A.16.16,0,0,1,553.72,492.43Zm-2.84-2.89a.15.15,0,0,1-.15-.15V488.3a.15.15,0,0,1,.15-.14h2.36a.15.15,0,0,1,.15.14v1.09a.15.15,0,0,1-.15.15Z" transform="translate(-75 -152.99)" fill="#4e73df" opacity="0.3"/><path d="M574.56,477.71H569v-1.2a.29.29,0,0,0-.29-.29H567v-1.29a.18.18,0,0,0-.18-.18h-2.12a.18.18,0,0,0-.18.18v1.29h-1.93a.29.29,0,0,0-.29.29v1.2H557a.06.06,0,0,0-.06.06v16.06a.06.06,0,0,0,.06.05h3.54s.06,0,.06-.05v-2.16a.06.06,0,0,1,.06-.06h1.5a.06.06,0,0,1,.06.06v2.16s0,.05.06.05h7a.06.06,0,0,0,.06-.05v-2.16s0-.06,0-.06h1.5a.06.06,0,0,1,.06.06v2.16a.06.06,0,0,0,.06.05h3.54a.06.06,0,0,0,.06-.05V477.77A.06.06,0,0,0,574.56,477.71ZM560,490.86a.06.06,0,0,1-.06.06h-2.19a.06.06,0,0,1-.06-.06v-.75a.06.06,0,0,1,.06-.06h2.19a.06.06,0,0,1,.06.06Zm0-1.93a.06.06,0,0,1-.06.06h-2.19a.06.06,0,0,1-.06-.06v-.75a.06.06,0,0,1,.06-.06h2.19a.06.06,0,0,1,.06.06Zm0-1.93a.06.06,0,0,1-.06.05h-2.19a.06.06,0,0,1-.06-.05v-.75a.06.06,0,0,1,.06-.06h2.19a.06.06,0,0,1,.06.06Zm0-1.94a.06.06,0,0,1-.06.06h-2.19a.06.06,0,0,1-.06-.06v-.74a.06.06,0,0,1,.06-.06h2.19a.06.06,0,0,1,.06.06Zm0-1.93a.06.06,0,0,1-.06.06h-2.19a.06.06,0,0,1-.06-.06v-.74a.06.06,0,0,1,.06-.06h2.19a.06.06,0,0,1,.06.06Zm0-1.93a.06.06,0,0,1-.06.06h-2.19a.06.06,0,0,1-.06-.06v-.75a.06.06,0,0,1,.06-.05h2.19a.06.06,0,0,1,.06.05Zm0-1.93a.06.06,0,0,1-.06.06h-2.19a.06.06,0,0,1-.06-.06v-.75a.06.06,0,0,1,.06-.06h2.19a.06.06,0,0,1,.06.06Zm5.19,11.59a.06.06,0,0,1-.06.06h-2.19a.06.06,0,0,1-.06-.06v-.75a.06.06,0,0,1,.06-.06h2.19a.06.06,0,0,1,.06.06Zm0-1.93a.06.06,0,0,1-.06.06h-2.19a.06.06,0,0,1-.06-.06v-.75a.06.06,0,0,1,.06-.06h2.19a.06.06,0,0,1,.06.06Zm0-1.93a.06.06,0,0,1-.06.05h-2.19a.06.06,0,0,1-.06-.05v-.75a.06.06,0,0,1,.06-.06h2.19a.06.06,0,0,1,.06.06Zm0-1.94a.06.06,0,0,1-.06.06h-2.19a.06.06,0,0,1-.06-.06v-.74a.06.06,0,0,1,.06-.06h2.19a.06.06,0,0,1,.06.06Zm0-1.93a.06.06,0,0,1-.06.06h-2.19a.06.06,0,0,1-.06-.06v-.74a.06.06,0,0,1,.06-.06h2.19a.06.06,0,0,1,.06.06Zm0-1.93a.06.06,0,0,1-.06.06h-2.19a.06.06,0,0,1-.06-.06v-.75a.06.06,0,0,1,.06-.05h2.19a.06.06,0,0,1,.06.05Zm0-1.93a.06.06,0,0,1-.06.06h-2.19a.06.06,0,0,1-.06-.06v-.75a.06.06,0,0,1,.06-.06h2.19a.06.06,0,0,1,.06.06Zm3.54,11.59a.06.06,0,0,1-.06.06h-2.19a.06.06,0,0,1-.05-.06v-.75a.06.06,0,0,1,.05-.06h2.19a.06.06,0,0,1,.06.06Zm0-1.93a.06.06,0,0,1-.06.06h-2.19a.06.06,0,0,1-.05-.06v-.75a.06.06,0,0,1,.05-.06h2.19a.06.06,0,0,1,.06.06Zm0-1.93s0,.05-.06.05h-2.19a0,0,0,0,1-.05-.05v-.75a.06.06,0,0,1,.05-.06h2.19a.06.06,0,0,1,.06.06Zm0-1.94a.05.05,0,0,1-.06.06h-2.19s-.05,0-.05-.06v-.74s0-.06.05-.06h2.19a.05.05,0,0,1,.06.06Zm0-1.93a.05.05,0,0,1-.06.06h-2.19s-.05,0-.05-.06v-.74s0-.06.05-.06h2.19a.05.05,0,0,1,.06.06Zm0-1.93a.06.06,0,0,1-.06.06h-2.19a.06.06,0,0,1-.05-.06v-.75a0,0,0,0,1,.05-.05h2.19s.06,0,.06.05Zm0-1.93a.06.06,0,0,1-.06.06h-2.19a.06.06,0,0,1-.05-.06v-.75a.06.06,0,0,1,.05-.06h2.19a.06.06,0,0,1,.06.06Zm5.19,11.59a.06.06,0,0,1-.06.06h-2.19a.06.06,0,0,1-.06-.06v-.75a.06.06,0,0,1,.06-.06h2.19a.06.06,0,0,1,.06.06Zm0-1.93a.06.06,0,0,1-.06.06h-2.19a.06.06,0,0,1-.06-.06v-.75a.06.06,0,0,1,.06-.06h2.19a.06.06,0,0,1,.06.06Zm0-1.93a.06.06,0,0,1-.06.05h-2.19a.06.06,0,0,1-.06-.05v-.75a.06.06,0,0,1,.06-.06h2.19a.06.06,0,0,1,.06.06Zm0-1.94a.06.06,0,0,1-.06.06h-2.19a.06.06,0,0,1-.06-.06v-.74a.06.06,0,0,1,.06-.06h2.19a.06.06,0,0,1,.06.06Zm0-1.93a.06.06,0,0,1-.06.06h-2.19a.06.06,0,0,1-.06-.06v-.74a.06.06,0,0,1,.06-.06h2.19a.06.06,0,0,1,.06.06Zm0-1.93a.06.06,0,0,1-.06.06h-2.19a.06.06,0,0,1-.06-.06v-.75a.06.06,0,0,1,.06-.05h2.19a.06.06,0,0,1,.06.05Zm0-1.93a.06.06,0,0,1-.06.06h-2.19a.06.06,0,0,1-.06-.06v-.75a.06.06,0,0,1,.06-.06h2.19a.06.06,0,0,1,.06.06Z" transform="translate(-75 -152.99)" fill="#4e73df" opacity="0.3"/><path d="M558.64,473.36a1.38,1.38,0,0,0-.73.08,1,1,0,0,1-.88,0,1.23,1.23,0,0,0-1,0,.61.61,0,0,1-.3.08c-.42,0-.77-.43-.84-1a.77.77,0,0,0,.21-.23,1.28,1.28,0,0,1,1-.65,1.27,1.27,0,0,1,1,.65.81.81,0,0,0,.69.38h0A.87.87,0,0,1,558.64,473.36Z" transform="translate(-75 -152.99)" fill="#4e73df" opacity="0.1"/><path d="M559.91,471.62l-.67.43.41-.75a.7.7,0,0,0-.41-.14h-.15l-.23.14.09-.18a.79.79,0,0,1-.4-.3l-.41.26.26-.47a1.2,1.2,0,0,0-.92-.47,1.28,1.28,0,0,0-1.05.65.79.79,0,0,1-.69.38h0c-.47,0-.85.53-.85,1.19s.38,1.19.85,1.19a.73.73,0,0,0,.3-.07,1.2,1.2,0,0,1,1,0,1.12,1.12,0,0,0,.44.09,1.08,1.08,0,0,0,.44-.09,1.21,1.21,0,0,1,1,0,.73.73,0,0,0,.3.07c.47,0,.85-.53.85-1.19A1.52,1.52,0,0,0,559.91,471.62Z" transform="translate(-75 -152.99)" fill="#4e73df" opacity="0.1"/><ellipse cx="450.81" cy="350.66" rx="6.15" ry="10.25" fill="#a0616a"/><ellipse cx="531.76" cy="345.53" rx="6.15" ry="10.25" fill="#a0616a"/></svg> \ No newline at end of file diff --git a/base/static/img/undraw_profile.svg b/base/static/img/undraw_profile.svg deleted file mode 100755 index 980234178..000000000 --- a/base/static/img/undraw_profile.svg +++ /dev/null @@ -1,38 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 25.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - viewBox="0 0 108.3 108.3" style="enable-background:new 0 0 108.3 108.3;" xml:space="preserve"> -<style type="text/css"> - .st0{fill:#E6E6E6;} - .st1{fill:#FFB8B8;} - .st2{fill:#575A89;} - .st3{fill:#2F2E41;} -</style> -<g id="Group_45" transform="translate(-191 -152.079)"> - <g id="Group_30" transform="translate(282.246 224.353)"> - <path id="Path_944" class="st0" d="M17.1-18.1c0,10.5-3,20.8-8.8,29.6c-1.2,1.9-2.5,3.6-4,5.3c-3.4,4-7.3,7.4-11.6,10.3 - c-1.2,0.8-2.4,1.5-3.6,2.2c-6.5,3.6-13.7,5.8-21,6.5c-1.7,0.2-3.4,0.2-5.1,0.2c-4.7,0-9.4-0.6-14-1.8c-2.6-0.7-5.1-1.6-7.6-2.6 - c-1.3-0.5-2.5-1.1-3.7-1.8c-2.9-1.5-5.6-3.3-8.2-5.3c-1.2-0.9-2.3-1.9-3.4-2.9C-95.8,1.3-97.1-33-76.8-54.9s54.6-23.3,76.5-2.9 - C10.8-47.6,17.1-33.2,17.1-18.1L17.1-18.1z"/> - <path id="Path_945" class="st1" d="M-50.2-13.2c0,0,4.9,13.7,1.1,21.4s6,16.4,6,16.4s25.8-13.1,22.5-19.7s-8.8-15.3-7.7-20.8 - L-50.2-13.2z"/> - <ellipse id="Ellipse_185" class="st1" cx="-40.6" cy="-25.5" rx="17.5" ry="17.5"/> - <path id="Path_946" class="st2" d="M-51.1,34.2c-2.6-0.7-5.1-1.6-7.6-2.6l0.5-13.3l4.9-11c1.1,0.9,2.3,1.6,3.5,2.3 - c0.3,0.2,0.6,0.3,0.9,0.5c4.6,2.2,12.2,4.2,19.5-1.3c2.7-2.1,5-4.7,6.7-7.6L-8.8,9l0.7,8.4l0.8,9.8c-1.2,0.8-2.4,1.5-3.6,2.2 - c-6.5,3.6-13.7,5.8-21,6.5c-1.7,0.2-3.4,0.2-5.1,0.2C-41.8,36.1-46.5,35.4-51.1,34.2z"/> - <path id="Path_947" class="st2" d="M-47.7-0.9L-47.7-0.9l-0.7,7.2l-0.4,3.8l-0.5,5.6l-1.8,18.5c-2.6-0.7-5.1-1.6-7.6-2.6 - c-1.3-0.5-2.5-1.1-3.7-1.8c-2.9-1.5-5.6-3.3-8.2-5.3l-1.9-9l0.1-0.1L-47.7-0.9z"/> - <path id="Path_948" class="st2" d="M-10.9,29.3c-6.5,3.6-13.7,5.8-21,6.5c0.4-6.7,1-13.1,1.6-18.8c0.3-2.9,0.7-5.7,1.1-8.2 - c1.2-8,2.5-13.5,3.4-14.2l6.1,4L4.9,7.3l-0.5,9.5c-3.4,4-7.3,7.4-11.6,10.3C-8.5,27.9-9.7,28.7-10.9,29.3z"/> - <path id="Path_949" class="st2" d="M-70.5,24.6c-1.2-0.9-2.3-1.9-3.4-2.9l0.9-6.1l0.7-0.1l3.1-0.4l6.8,14.8 - C-65.2,28.3-67.9,26.6-70.5,24.6L-70.5,24.6z"/> - <path id="Path_950" class="st2" d="M8.3,11.5c-1.2,1.9-2.5,3.6-4,5.3c-3.4,4-7.3,7.4-11.6,10.3c-1.2,0.8-2.4,1.5-3.6,2.2l-0.6-2.8 - l3.5-9.1l4.2-11.1l8.8,1.1C6.1,8.7,7.2,10.1,8.3,11.5z"/> - <path id="Path_951" class="st3" d="M-23.9-41.4c-2.7-4.3-6.8-7.5-11.6-8.9l-3.6,2.9l1.4-3.3c-1.2-0.2-2.3-0.2-3.5-0.2l-3.2,4.1 - l1.3-4c-5.6,0.7-10.7,3.7-14,8.3c-4.1,5.9-4.8,14.1-0.8,20c1.1-3.4,2.4-6.6,3.5-9.9c0.9,0.1,1.7,0.1,2.6,0l1.3-3.1l0.4,3 - c4.2-0.4,10.3-1.2,14.3-1.9l-0.4-2.3l2.3,1.9c1.2-0.3,1.9-0.5,1.9-0.7c2.9,4.7,5.8,7.7,8.8,12.5C-22.1-29.8-20.2-35.3-23.9-41.4z" - /> - <ellipse id="Ellipse_186" class="st1" cx="-24.9" cy="-26.1" rx="1.2" ry="2.4"/> - </g> -</g> -</svg> diff --git a/base/static/img/undraw_profile_1.svg b/base/static/img/undraw_profile_1.svg deleted file mode 100755 index fcc91c706..000000000 --- a/base/static/img/undraw_profile_1.svg +++ /dev/null @@ -1,38 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 25.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<svg version="1.1" id="_x38_8ce59e9-c4b8-4d1d-9d7a-ce0190159aa8" - xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 231.8 231.8" - style="enable-background:new 0 0 231.8 231.8;" xml:space="preserve"> -<style type="text/css"> - .st0{opacity:0.5;} - .st1{fill:url(#SVGID_1_);} - .st2{fill:#F5F5F5;} - .st3{fill:#333333;} - .st4{fill:#4E73DF;} - .st5{opacity:0.1;enable-background:new ;} - .st6{fill:#BE7C5E;} -</style> -<g class="st0"> - - <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="115.89" y1="525.2" x2="115.89" y2="756.98" gradientTransform="matrix(1 0 0 -1 0 756.98)"> - <stop offset="0" style="stop-color:#808080;stop-opacity:0.25"/> - <stop offset="0.54" style="stop-color:#808080;stop-opacity:0.12"/> - <stop offset="1" style="stop-color:#808080;stop-opacity:0.1"/> - </linearGradient> - <circle class="st1" cx="115.9" cy="115.9" r="115.9"/> -</g> -<circle class="st2" cx="115.9" cy="115.3" r="113.4"/> -<path class="st3" d="M71.6,116.3c0,0-12.9,63.4-19.9,59.8c0,0,67.7,58.5,127.5,0c0,0-10.5-44.6-25.7-59.8H71.6z"/> -<path class="st4" d="M116.2,229c22.2,0,43.9-6.5,62.4-18.7c-4.2-22.8-20.1-24.1-20.1-24.1H70.8c0,0-15,1.2-19.7,22.2 - C70.1,221.9,92.9,229.1,116.2,229z"/> -<circle class="st3" cx="115" cy="112.8" r="50.3"/> -<path class="st5" d="M97.3,158.4h35.1l0,0v28.1c0,9.7-7.8,17.5-17.5,17.5l0,0c-9.7,0-17.5-7.9-17.5-17.5L97.3,158.4L97.3,158.4z"/> -<path class="st6" d="M100.7,157.1h28.4c1.9,0,3.4,1.5,3.4,3.3v0v24.7c0,9.7-7.8,17.5-17.5,17.5l0,0c-9.7,0-17.5-7.9-17.5-17.5v0 - v-24.7C97.4,158.6,98.9,157.1,100.7,157.1z"/> -<path class="st5" d="M97.4,171.6c11.3,4.2,23.8,4.3,35.1,0.1v-4.3H97.4V171.6z"/> -<circle class="st6" cx="115" cy="123.7" r="50.3"/> -<path class="st3" d="M66.9,104.6h95.9c0,0-8.2-38.7-44.4-36.2S66.9,104.6,66.9,104.6z"/> -<ellipse class="st6" cx="65.8" cy="121.5" rx="4.7" ry="8.8"/> -<ellipse class="st6" cx="164" cy="121.5" rx="4.7" ry="8.8"/> -<path class="st5" d="M66.9,105.9h95.9c0,0-8.2-38.7-44.4-36.2S66.9,105.9,66.9,105.9z"/> -</svg> diff --git a/base/static/img/undraw_profile_2.svg b/base/static/img/undraw_profile_2.svg deleted file mode 100755 index 488d1bd67..000000000 --- a/base/static/img/undraw_profile_2.svg +++ /dev/null @@ -1,44 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 25.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<svg version="1.1" id="_x38_8ce59e9-c4b8-4d1d-9d7a-ce0190159aa8" - xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 231.8 231.8" - style="enable-background:new 0 0 231.8 231.8;" xml:space="preserve"> -<style type="text/css"> - .st0{opacity:0.5;} - .st1{fill:url(#SVGID_1_);} - .st2{fill:#F5F5F5;} - .st3{fill:#4E73DF;} - .st4{fill:#72351C;} - .st5{opacity:0.1;enable-background:new ;} - .st6{fill:#FDA57D;} -</style> -<g class="st0"> - - <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="115.89" y1="526.22" x2="115.89" y2="758" gradientTransform="matrix(1 0 0 -1 0 758)"> - <stop offset="0" style="stop-color:#808080;stop-opacity:0.25"/> - <stop offset="0.54" style="stop-color:#808080;stop-opacity:0.12"/> - <stop offset="1" style="stop-color:#808080;stop-opacity:0.1"/> - </linearGradient> - <circle class="st1" cx="115.9" cy="115.9" r="115.9"/> -</g> -<circle class="st2" cx="116.1" cy="115.1" r="113.4"/> -<path class="st3" d="M116.2,229c22.2,0,43.9-6.5,62.4-18.7c-4.2-22.9-20.1-24.2-20.1-24.2H70.8c0,0-15,1.2-19.7,22.2 - C70.1,221.9,92.9,229.1,116.2,229z"/> -<circle class="st4" cx="115" cy="112.8" r="54.8"/> -<path class="st5" d="M97.3,158.4h35.1l0,0v28.1c0,9.7-7.8,17.6-17.5,17.6c0,0,0,0,0,0l0,0c-9.7,0-17.5-7.9-17.5-17.5L97.3,158.4 - L97.3,158.4z"/> -<path class="st6" d="M100.7,157.1h28.4c1.9,0,3.3,1.5,3.3,3.4v24.7c0,9.7-7.9,17.5-17.5,17.5l0,0c-9.7,0-17.5-7.9-17.5-17.5v-24.7 - C97.3,158.6,98.8,157.1,100.7,157.1L100.7,157.1z"/> -<path class="st5" d="M97.4,171.6c11.3,4.2,23.8,4.3,35.1,0.1v-4.3H97.4V171.6z"/> -<circle class="st6" cx="115" cy="123.7" r="50.3"/> -<path class="st5" d="M79.2,77.9c0,0,21.2,43,81,18l-13.9-21.8l-24.7-8.9L79.2,77.9z"/> -<path class="st4" d="M79.2,77.3c0,0,21.2,43,81,18l-13.9-21.8l-24.7-8.9L79.2,77.3z"/> -<path class="st4" d="M79,74.4c1.4-4.4,3.9-8.4,7.2-11.7c9.9-9.8,26.1-11.8,34.4-23c1.8,3.1,0.7,7.1-2.4,8.9 - c-0.2,0.1-0.4,0.2-0.6,0.3c8-0.1,17.2-0.8,21.7-7.3c2.3,5.3,1.3,11.4-2.5,15.7c7.1,0.3,14.6,5.1,15.1,12.2c0.3,4.7-2.6,9.1-6.5,11.9 - s-8.5,3.9-13.1,4.9C118.8,89.2,70.3,101.6,79,74.4z"/> -<path class="st4" d="M165.3,124.1H164L138,147.2c-25-11.7-43.3,0-43.3,0l-27.2-22.1l-2.7,0.3c0.8,27.8,23.9,49.6,51.7,48.9 - C143.6,173.5,165.3,151.3,165.3,124.1L165.3,124.1z M115,156.1c-9.8,0-17.7-2-17.7-4.4s7.9-4.4,17.7-4.4s17.7,2,17.7,4.4 - S124.7,156.1,115,156.1L115,156.1z"/> -<ellipse class="st6" cx="64.7" cy="123.6" rx="4.7" ry="8.8"/> -<ellipse class="st6" cx="165.3" cy="123.6" rx="4.7" ry="8.8"/> -</svg> diff --git a/base/static/img/undraw_profile_3.svg b/base/static/img/undraw_profile_3.svg deleted file mode 100755 index eecb335ba..000000000 --- a/base/static/img/undraw_profile_3.svg +++ /dev/null @@ -1,47 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 25.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<svg version="1.1" id="_x38_8ce59e9-c4b8-4d1d-9d7a-ce0190159aa8" - xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 231.8 231.8" - style="enable-background:new 0 0 231.8 231.8;" xml:space="preserve"> -<style type="text/css"> - .st0{opacity:0.5;} - .st1{fill:url(#SVGID_1_);} - .st2{fill:#F5F5F5;} - .st3{fill:#4E73DF;} - .st4{fill:#F55F44;} - .st5{opacity:0.1;enable-background:new ;} - .st6{fill:#FDA57D;} - .st7{fill:#333333;} -</style> -<g class="st0"> - - <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="115.89" y1="9.36" x2="115.89" y2="241.14" gradientTransform="matrix(1 0 0 -1 0 241.14)"> - <stop offset="0" style="stop-color:#808080;stop-opacity:0.25"/> - <stop offset="0.54" style="stop-color:#808080;stop-opacity:0.12"/> - <stop offset="1" style="stop-color:#808080;stop-opacity:0.1"/> - </linearGradient> - <circle class="st1" cx="115.9" cy="115.9" r="115.9"/> -</g> -<circle class="st2" cx="116.1" cy="115.1" r="113.4"/> -<path class="st3" d="M116.2,229c22.2,0,43.8-6.5,62.3-18.7c-4.2-22.8-20.1-24.2-20.1-24.2H70.8c0,0-15,1.2-19.7,22.2 - C70.1,221.9,92.9,229.1,116.2,229z"/> -<circle class="st4" cx="115" cy="112.8" r="54.8"/> -<path class="st5" d="M97.3,158.4h35.1l0,0v28.1c0,9.7-7.9,17.5-17.5,17.5l0,0l0,0c-9.7,0-17.5-7.9-17.5-17.5l0,0L97.3,158.4 - L97.3,158.4z"/> -<path class="st6" d="M100.7,157.1h28.4c1.9,0,3.4,1.5,3.4,3.4l0,0v24.7c0,9.7-7.9,17.5-17.5,17.5l0,0l0,0c-9.7,0-17.5-7.9-17.5-17.5 - l0,0v-24.7C97.4,158.6,98.8,157.1,100.7,157.1L100.7,157.1L100.7,157.1z"/> -<path class="st5" d="M97.4,171.6c11.3,4.2,23.8,4.3,35.1,0.1v-4.3H97.4V171.6z"/> -<circle class="st6" cx="115" cy="123.7" r="50.3"/> -<circle class="st4" cx="114.9" cy="57.1" r="20.2"/> -<circle class="st4" cx="114.9" cy="37.1" r="13.3"/> -<path class="st4" d="M106.2,68.2c-9.9-4.4-14.5-15.8-10.5-25.9c-0.1,0.3-0.3,0.6-0.4,0.9c-4.6,10.2,0,22.2,10.2,26.8 - s22.2,0,26.8-10.2c0.1-0.3,0.2-0.6,0.4-0.9C127.6,68.5,116,72.6,106.2,68.2z"/> -<path class="st5" d="M79.2,77.9c0,0,21.2,43,81,18l-13.9-21.8l-24.7-8.9L79.2,77.9z"/> -<path class="st4" d="M79.2,77.3c0,0,21.2,43,81,18l-13.9-21.8l-24.7-8.9L79.2,77.3z"/> -<path class="st7" d="M95.5,61.6c13-1,26.1-1,39.2,0C134.7,61.6,105.8,64.3,95.5,61.6z"/> -<path class="st4" d="M118,23c-1,0-2,0-3,0.2h0.8c7.3,0.2,13.1,6.4,12.8,13.7c-0.2,6.2-4.7,11.5-10.8,12.6 - c7.3,0.1,13.3-5.8,13.4-13.2C131.2,29.1,125.3,23.1,118,23L118,23z"/> -<ellipse class="st6" cx="64.7" cy="123.6" rx="4.7" ry="8.8"/> -<ellipse class="st6" cx="165.3" cy="123.6" rx="4.7" ry="8.8"/> -<polygon class="st4" points="76,78.6 85.8,73.5 88,81.6 82,85.7 "/> -</svg> diff --git a/base/static/img/undraw_rocket.svg b/base/static/img/undraw_rocket.svg deleted file mode 100755 index 45426141b..000000000 --- a/base/static/img/undraw_rocket.svg +++ /dev/null @@ -1,39 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 25.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<svg version="1.1" id="b759170a-51c3-4e2f-999d-77dec9fd6d11" - xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 650.9 610.5" - style="enable-background:new 0 0 650.9 610.5;" xml:space="preserve"> -<style type="text/css"> - .st0{fill:#AFC0E0;} - .st1{opacity:0.2;fill:#FFFFFF;enable-background:new ;} - .st2{opacity:0.1;enable-background:new ;} - .st3{fill:#E3E8F4;} - .st4{fill:#4E73DF;} -</style> -<path class="st0" d="M174,321c-2-1.6-4.2-3-6.6-4.2c-51.8-26.2-157,67.8-157,67.8L0,372.7c0,0,42.1-43.8,92.4-117.3 - c45.2-66.1,150.7-51.8,171.4-48.3c2.3,0.4,3.6,0.7,3.6,0.7C298.7,288.3,174,321,174,321z"/> -<path class="st1" d="M269.4,213.9c-0.6-2-1.3-4-2-6c0,0-1.2-0.2-3.6-0.7c-20.7-3.5-126.2-17.8-171.4,48.3C42.1,329,0,372.7,0,372.7 - l5.9,6.7c0,0,42.1-43.8,92.4-117.3C143.3,196.3,248,210.2,269.4,213.9z"/> -<path class="st0" d="M337.7,533.4c-79.2,40.8-127.8,77.1-127.8,77.1l-10.5-11.9c0,0,111.1-96.8,85.3-150.9c-0.5-1.2-1.2-2.3-1.9-3.4 - c0,0,47.9-119.6,123.9-78.5c0,0,0.1,1,0.2,2.9C407.8,387.8,409.7,496.3,337.7,533.4z"/> -<path class="st2" d="M174,321c-2-1.6-4.2-3-6.6-4.2c29.3-38.9,61.5-75.5,96.3-109.7c2.3,0.4,3.6,0.7,3.6,0.7 - C298.7,288.3,174,321,174,321z"/> -<path class="st2" d="M406.9,368.6c-38.6,29.6-79.4,56.1-122.3,79.1c-0.5-1.2-1.2-2.3-1.9-3.4c0,0,47.9-119.6,123.9-78.5 - C406.7,365.7,406.8,366.7,406.9,368.6z"/> -<path class="st3" d="M263.6,455.5c-20.3,10.4-41.6,20.5-64,30.2c-33.6,14.6-51.5-2.2-80.7-91.5c0,0,12.5-22.5,37.2-57 - c54.3-75.8,167.5-209.1,336.1-286.7C542.7,27.1,596.1,10.1,650.9,0c0,0-9.1,68.8-62,160.1S439.1,365.3,263.6,455.5z"/> -<circle class="st0" cx="435.6" cy="199.7" r="71.6"/> -<path class="st4" d="M469.2,237.9c-21,18.6-53.1,16.6-71.7-4.5c-7.8-8.8-12.2-20-12.7-31.8c-0.2-4.7,0.3-9.4,1.4-14 - c0.5-2,1.1-4.1,1.9-6c2.9-7.7,7.7-14.5,13.8-19.9c0.3-0.3,0.6-0.5,0.9-0.8c17.1-14.4,41.5-15.9,60.3-3.8c3.5,2.3,6.7,4.9,9.5,7.9 - l1,1.1C492.2,187.2,490.2,219.3,469.2,237.9C469.2,237.8,469.2,237.9,469.2,237.9z"/> -<path class="st0" d="M588.9,160.1c-83-35.2-96.8-109.6-96.8-109.6C542.7,27,596.1,10.1,650.9,0C650.9,0,641.8,68.8,588.9,160.1z"/> -<path class="st0" d="M263.6,455.5c-13.7,7.1-27.9,13.9-42.6,20.7c-7,3.2-14.1,6.4-21.4,9.5c-10.9,4.7-51.5-2.2-80.7-91.5 - c0,0,4.1-7.3,12.1-20c6.1-9.6,14.5-22.2,25.1-37c0,0,11,33.2,41.1,67.3C215.8,425.7,238.4,443,263.6,455.5z"/> -<path class="st3" d="M221,476.2c-7,3.2-14.1,6.4-21.4,9.5c-10.9,4.7-51.5-2.2-80.7-91.5c0,0,4.1-7.3,12.1-20 - C131,374.2,170.2,456.9,221,476.2z"/> -<path class="st1" d="M463.2,157l-0.1,0l-60.1,3.9c-0.3,0.3-0.6,0.5-0.9,0.8c-6.2,5.4-10.9,12.3-13.8,19.9l84.5-16.6L463.2,157z"/> -<path class="st1" d="M438.8,194.3l-53.9,7.3c-0.2-4.7,0.3-9.4,1.4-14l52.8,1.4L438.8,194.3z"/> -<path class="st1" d="M131.7,408.7c0,0,12.5-22.5,37.2-57C223.2,276,336.4,142.7,504.9,65c45.6-21.1,93.3-36.9,142.5-47.3 - C650.1,6.4,650.9,0,650.9,0c-54.8,10.1-108.2,27-158.7,50.5c-168.6,77.7-281.8,211-336.1,286.7c-24.7,34.4-37.2,57-37.2,57 - c11.5,35.3,26.6,57,40.5,70.3C149.4,451.4,139.7,433.3,131.7,408.7z"/> -</svg> diff --git a/base/static/js/click_reset.js b/base/static/js/click_reset.js deleted file mode 100755 index d46a1afca..000000000 --- a/base/static/js/click_reset.js +++ /dev/null @@ -1,29 +0,0 @@ -$(document).ready(function () { - //Highlight clicked row - document.getElementById('reset_div').addEventListener('click', function () { - // on click reset the graph - // if reset button exists, hide it - var csrftoken = jQuery("[name=csrfmiddlewaretoken]").val(); - $("#run_counterfactual_loader").show() - $("#reset").remove() - $.ajax({ - method: 'POST', - url: '', - headers: { 'X-CSRFToken': csrftoken }, - data: {'action': "reset_graph" }, - success: function (ret) { - $("#run_counterfactual_loader").hide() - if ($("#og_cf_row") && $("#og_cf_headers")) { - $("#og_cf_row").hide() - $("#og_cf_headers").hide() - } - ret = JSON.parse(ret) - fig = ret["fig"] - document.getElementById("tsne").innerHTML = ""; - $("#tsne").append(fig) - }, - error: function (ret) { - } - }); - }); -}); diff --git a/base/static/js/counterfactuals.js b/base/static/js/counterfactuals.js index 21a372534..e940a73df 100755 --- a/base/static/js/counterfactuals.js +++ b/base/static/js/counterfactuals.js @@ -308,6 +308,7 @@ $(document).ready(function () { }); data_to_pass = { 'action': "cf", "features_to_vary": JSON.stringify(features_to_vary), "model_name": model_name } } + // hide button and original point row // replace with loader $("#cfbtn_loader").show() diff --git a/base/static/js/home.js b/base/static/js/home.js new file mode 100755 index 000000000..01b18224b --- /dev/null +++ b/base/static/js/home.js @@ -0,0 +1,613 @@ +import { create_dataframe, create_selection, create_uploaded_file_radio, showSuccessMessage, showLoader, clearPreviousContent, resetContainers } from './methods.js'; + +$(document).ready(function () { + + // Add visibility to fade-in elements on scroll + const fadeElements = document.querySelectorAll('.fade-in'); + + const elementInView = (el, percentageScroll = 100) => { + const elementTop = el.getBoundingClientRect().top; + return elementTop <= (window.innerHeight || document.documentElement.clientHeight) * (percentageScroll / 100); + }; + + const displayFadeElement = (element) => { + element.classList.add('visible'); + }; + + const handleFadeAnimation = () => { + fadeElements.forEach((el) => { + if (elementInView(el)) { + displayFadeElement(el); + } + }); + }; + + window.addEventListener('scroll', () => { + handleFadeAnimation(); + }); + + // Initial check for elements already in view + handleFadeAnimation(); + + function fetchDatasetData(df_name) { + const csrftoken = jQuery("[name=csrfmiddlewaretoken]").val(); + showLoader(true); + + $.ajax({ + method: 'POST', + url: '', + headers: { 'X-CSRFToken': csrftoken }, + data: { 'action': "dataset", 'df_name': df_name }, + success: function (values) { + showLoader(false); + handleSuccessResponse(values); + $("#new_or_load").show(); + }, + error: function (ret) { + console.error("Failed to fetch dataset:", ret); + } + }); + } + + function handleSuccessResponse(values) { + if (!values) return; + + clearPreviousContent(); + const ret = JSON.parse(values); + const datasetType = ret["dataset_type"]; + + if (datasetType === "tabular") { + setupTabularDataset(ret); + } else if (datasetType === "timeseries") { + setupTimeseriesDataset(ret); + } + } + + function setupTabularDataset(ret) { + const { data_to_display: df, fig, features, feature1, feature2, labels, curlabel } = ret; + + const selection1 = create_selection(features, "feature1", null, feature1); + const selection2 = create_selection(features, "feature2", null, feature2); + const selection3 = create_selection(labels, "label", null, curlabel); + + const tb = create_dataframe(df, "df_container"); + + $("#model_container, #df, #df_stats").fadeIn(200); + $("#df_div").append(tb); + $("#selection").append(selection1, selection2, selection3); + + const figDiv = $("<div>", { id: 'stats_container', class: "plotly_fig" }).html(fig); + $("#stats_div").append(figDiv); + } + + function setupTimeseriesDataset(ret) { + const { fig, fig1 } = ret; + + const figDiv = $("<div>", { id: 'ts_confidence_container', class: "plotly_fig" }).html(fig); + const figDiv1 = $("<div>", { id: 'ts_stats_container', class: "plotly_fig" }).html(fig1); + + $("#ts_stats, #ts_confidence").fadeIn(200); + $("#ts_stats_div").append(figDiv); + $("#ts_confidence_div").append(figDiv1); + } + + $('.btn-dataset').click(function (e) { + const df_name = $(this).is('#upload') ? "upload" : $(this).attr('id'); + $("#new_or_load_cached").hide(); + resetContainers(); + $("#upload_col").toggle(df_name === "upload"); + $("#timeseries-datasets").toggle(df_name === "timeseries"); + + $(this).toggleClass("active").siblings().removeClass("active"); + $(this).addClass("active"); + + const timeseries_dataset = df_name === "timeseries" ? $("input:radio[name=timeseries_dataset]:checked").val() : ""; + if (timeseries_dataset || (df_name !== "timeseries")) { + fetchDatasetData(timeseries_dataset || df_name); + } + }); +}); + +document.getElementById("viewModelsButton").addEventListener("click", function () { + // Prompt or redirect the user to the pre-trained models section + window.location.href = "/charts.html"; // Replace with the actual URL +}); + +$(document).ready(function () { + $('#timeseries-datasets').change(function () { + if ($("input[name=timeseries_dataset]:checked").length > 0) { + + var timeseries_dataset = $("input:radio[name=timeseries_dataset]:checked").val(); + + $("#df_container").hide(); + $("#stats_container").hide(); + $("#figs").hide(); + + $("#ts_confidence_cached").hide() + $("#ts_stats_cached").hide() + + $("#ts_confidence").hide() + $("#ts_stats").hide() + var csrftoken = jQuery("[name=csrfmiddlewaretoken]").val(); + + $("#loader_ds").show(); + $("#loader_stats").show(); + + $("#new_or_load").hide(); + $("#new_or_load_cached").hide(); + + $.ajax({ + method: 'POST', + url: '', + headers: { 'X-CSRFToken': csrftoken, }, + data: { 'action': "timeseries-dataset", 'timeseries_dataset': timeseries_dataset }, + success: function (values) { + $("#loader_ds").hide(); + $("#loader_stats").hide(); + // fetch data + // remove data if already displayed + if (document.getElementById("df_container")) { + $("#pretrained_radio").remove(); + $("#df_container").remove(); + $("#stats_container").remove(); + $("#feature1").remove(); + $("#feature2").remove(); + $("#label").remove(); + } + + $("#new_or_load").show(); + + if (document.getElementById("ts_confidence_container")) { + $("#ts_confidence_container").remove(); + $("#ts_stats_container").remove(); + } + + var ret = JSON.parse(values) + var dataset_type = ret["dataset_type"] + + if (values) { + // timeseries + // var feature = ret["feature"] + var fig = ret["fig"] + var fig1 = ret["fig1"] + + var iDiv = document.createElement('div'); + iDiv.id = 'ts_confidence_container'; + iDiv.innerHTML = fig; + iDiv.setAttribute("class", "plotly_fig") + + var iDiv1 = document.createElement('div'); + iDiv1.id = 'ts_stats_container'; + iDiv1.innerHTML = fig1; + iDiv1.setAttribute("class", "plotly_fig") + + $("#ts_stats").show(); + $("#ts_confidence").show(); + + $("#ts_stats_div").append(iDiv); + $("#ts_confidence_div").append(iDiv1); + } + }, + error: function (ret) { + console.log("All bad") + } + + }); + } + }) +}); + +// $(document).ready(function () { +// $('#radio_buttons').change(function () { +// if ($("input[name=uploaded_file]:checked").length > 0) { +// var uploaded_dataset = $("input:radio[name=uploaded_file]:checked").val(); + +// if (document.getElementById("df_container")) { +// $("#df").hide(); +// $("#df_stats").hide(); +// } + +// if (document.getElementById("df_cached")) { +// $("#df_cached").hide() +// $("#df_stats_cached").hide() +// } + +// if (document.getElementById("ts_confidence")) { +// $("#ts_confidence").hide() +// $("#ts_stats").hide() +// } + +// if (document.getElementById("ts_confidence_cached")) { +// $("#ts_confidence_cached").hide() +// $("#ts_stats_cached").hide() +// } + +// $("#new_or_load").hide() +// var csrftoken = jQuery("[name=csrfmiddlewaretoken]").val(); + +// $("#loader_ds").show(); +// $("#loader_stats").show(); + +// $.ajax({ +// method: 'POST', +// url: '', +// headers: { 'X-CSRFToken': csrftoken, }, +// data: { 'action': "uploaded_datasets", 'df_name': uploaded_dataset }, +// success: function (values) { +// $("#loader_ds").hide(); +// $("#loader_stats").hide(); +// $("#new_or_load").show() +// // fetch data +// // remove data if already displayed +// if (document.getElementById("df_container")) { +// $("#pretrained_radio").remove(); +// $("#df_container").remove(); +// $("#stats_container").remove(); +// $("#feature1").remove(); +// $("#feature2").remove(); +// $("#label").remove(); +// } + +// if (document.getElementById("df_cached")) { +// $("#df_cached").remove() +// $("#df_stats_cached").remove() +// $("#ts_confidence_cached").remove() +// $("#ts_stats_cached").remove() +// } + +// if (document.getElementById("ts_confidence_container")) { +// $("#ts_confidence_container").remove(); +// $("#ts_stats_container").remove(); +// } + +// var ret = JSON.parse(values) +// var dataset_type = ret["dataset_type"] + +// if (values) { +// if (dataset_type == "tabular") { +// var df = ret["data_to_display"] +// var fig = ret["fig"] +// var features = ret["features"] +// var feature1 = ret["feature1"] +// var feature2 = ret["feature2"] + +// // cur labels +// var labels = ret["labels"] +// var curlabel = ret["curlabel"] + +// var selection1 = create_selection(features, "feature1", null, feature1) +// var selection2 = create_selection(features, "feature2", null, feature2) +// var selection3 = create_selection(labels, "label", null, curlabel) + +// // create table +// var tb = create_dataframe(df, "df_container") + +// $("#model_container").show() +// $("#df").show(); +// $("#df_stats").show(); + +// // append new data +// $("#df_div").append(tb); +// $("#selection").append(selection1); +// $("#selection").append(selection2); +// $("#selection").append(selection3); + +// // append fig +// var iDiv = document.createElement('div'); +// iDiv.id = 'stats_container'; +// iDiv.innerHTML = fig; +// iDiv.setAttribute("class", "plotly_fig") + +// $("#stats_div").append(iDiv); +// } else if (dataset_type == "timeseries") { + +// // timeseries +// // var feature = ret["feature"] +// var fig = ret["fig"] +// var fig1 = ret["fig1"] + +// var iDiv = document.createElement('div'); +// iDiv.id = 'ts_confidence_container'; +// iDiv.innerHTML = fig; +// iDiv.setAttribute("class", "plotly_fig") + +// var iDiv1 = document.createElement('div'); +// iDiv1.id = 'ts_stats_container'; +// iDiv1.innerHTML = fig1; +// iDiv1.setAttribute("class", "plotly_fig") + +// $("#ts_stats").show(); +// $("#ts_confidence").show(); + +// $("#ts_stats_div").append(iDiv); +// $("#ts_confidence_div").append(iDiv1); + +// } +// } +// }, +// error: function (ret) { +// } +// }); +// } +// }) +// }); + +// $('#upload_btn').click(function (event) { +// event.preventDefault(); // Prevent default form submission + +// var datasetType = $('input[name="dataset_type"]:checked').val(); +// var fileInput = $('#doc')[0].files[0]; +// var csrfToken = $('input[name="csrfmiddlewaretoken"]').val(); + +// if (!datasetType || !fileInput) { +// alert('Please select a dataset type and choose a file to upload.'); +// return; +// } + +// // Use FormData to handle file upload +// var formData = new FormData(); +// formData.append('action', 'upload_dataset'); +// formData.append('dataset_type', datasetType); +// formData.append('excel_file', fileInput); +// formData.append('csrfmiddlewaretoken', csrfToken); + +// $("#cfbtn_loader").show(); + +// $.ajax({ +// url: '', // Replace with your Django view URL for uploading +// type: 'POST', +// data: formData, +// processData: false, // Prevent jQuery from processing data +// contentType: false, // Prevent jQuery from setting content type +// success: function (response) { +// try { +// var ret = JSON.parse(response); +// var df_name = ret["df_name"]; +// var uploaded_files = ret["uploaded_files"]; +// var counter = uploaded_files.length - 1; + +// // Add uploaded file to the list +// alert("here") +// $("#radio_buttons").append(create_uploaded_file_radio(df_name, counter)); + +// // Check if target_labels exist in the response +// if (ret["target_labels"]) { +// populateLabelModal(ret["target_labels"]); // Populate the modal for label selection +// } + +// showSuccessMessage(); +// } catch (error) { +// console.error("Error processing response:", error); +// alert("An error occurred while processing the upload response."); +// } finally { +// $("#cfbtn_loader").hide(); +// } +// }, +// error: function (xhr, status, error) { +// console.error("Error uploading:", status, error); +// alert("An error occurred during upload. Please try again."); +// $("#cfbtn_loader").hide(); +// } +// }); +// }); + +// function populateLabelModal(targetLabels) { +// const positiveDropdown = $("#positive-label"); +// const negativeDropdown = $("#negative-label"); +// const errorContainer = $("#selection-error"); + +// // Populate dropdowns +// updateDropdownOptions(positiveDropdown, targetLabels, null); +// updateDropdownOptions(negativeDropdown, targetLabels, null); + +// // Reset error message +// errorContainer.addClass("d-none").text(""); + +// // Open the modal +// $("#labelSelectionModal").modal({ +// backdrop: 'static', // Prevent closing when clicking outside +// keyboard: false // Prevent closing with "Escape" +// }); + +// let selectedPositive = null; +// let selectedNegative = null; + +// // Handle changes in positive dropdown +// positiveDropdown.off("change").on("change", function () { +// selectedPositive = $(this).val(); +// updateDropdownOptions(negativeDropdown, targetLabels, selectedPositive, selectedNegative); +// validateSelection(selectedPositive, selectedNegative); +// }); + +// // Handle changes in negative dropdown +// negativeDropdown.off("change").on("change", function () { +// selectedNegative = $(this).val(); +// updateDropdownOptions(positiveDropdown, targetLabels, selectedNegative, selectedPositive); +// validateSelection(selectedPositive, selectedNegative); +// }); + +// $("#save-label-choices").click(function (event) { +// if (validateSelection(selectedPositive, selectedNegative, true)) { +// var csrfToken = $('input[name="csrfmiddlewaretoken"]').val(); +// var formData = new FormData(); + +// formData.append('action', 'select_class_labels_for_uploaded_timeseries'); +// formData.append('positive_label', selectedPositive); +// formData.append('negative_label', selectedNegative); +// formData.append('csrfToken', csrfToken); +// // Show loader +// $("#loader_ds").removeClass("d-none"); + +// // Disable the Save button to prevent duplicate submissions +// $("#save-label-choices").prop("disabled", true); + +// $.ajax({ +// url: '', // Replace with your Django view URL for uploading +// type: 'POST', +// headers: { 'X-CSRFToken': csrfToken, }, +// data: formData, +// processData: false, // Prevent jQuery from processing data +// contentType: false, // Prevent jQuery from setting content type +// success: function (response) { +// console.log('Labels saved successfully:', response); + +// // Hide loader +// $("#loader_ds").addClass("d-none"); + +// // Enable the Save button +// $("#save-label-choices").prop("disabled", false); + +// // Close the modal +// $("#labelSelectionModal").modal("hide"); + +// // Optionally update the UI with the response +// }, +// error: function (xhr) { +// const errorContainer = $("#selection-error"); +// const errorMessage = xhr.responseJSON?.message || 'An error occurred while saving labels.'; +// errorContainer.html(`<i class="fas fa-exclamation-triangle"></i> ${errorMessage}`) +// .removeClass("d-none"); + +// // Hide loader +// $("#loader_ds").addClass("d-none"); + +// // Enable the Save button +// $("#save-label-choices").prop("disabled", false); +// } +// }); +// } +// }); + + +// /** +// * Helper function to retrieve CSRF token. +// * Assumes the CSRF token is stored in a cookie named 'csrftoken'. +// * @returns {string} - CSRF token value. +// */ +// function getCSRFToken() { +// const name = 'csrftoken'; +// const cookies = document.cookie.split(';'); +// for (let cookie of cookies) { +// cookie = cookie.trim(); +// if (cookie.startsWith(name + '=')) { +// return cookie.substring(name.length + 1); +// } +// } +// return ''; +// } +// } + +// /** +// * Update dropdown options dynamically, excluding the currently selected value in the other dropdown. +// * @param {jQuery} dropdown - The dropdown to update. +// * @param {Array} options - The list of options to populate. +// * @param {string|null} exclude - The value to exclude from the dropdown options. +// * @param {string|null} currentValue - The current value of the dropdown being updated. +// */ +// function updateDropdownOptions(dropdown, options, exclude, currentValue = null) { +// dropdown.empty(); // Clear existing options + +// // Add default placeholder +// dropdown.append('<option value="" disabled>Select a label</option>'); + +// // Repopulate options, excluding the selected value from the other dropdown +// options.forEach(option => { +// if (option !== exclude) { +// dropdown.append( +// `<option value="${option}" ${option === currentValue ? "selected" : ""}>${option}</option>` +// ); +// } +// }); + +// // Reset dropdown if the current value is no longer valid +// if (exclude === currentValue) { +// dropdown.val(""); +// } +// } + +// /** +// * Validate the selected positive and negative labels. +// * @param {string|null} positive - The selected positive label. +// * @param {string|null} negative - The selected negative label. +// * @param {boolean} showError - Whether to show an error message on failure. +// * @returns {boolean} - Returns true if the selection is valid, otherwise false. +// */ +// function validateSelection(positive, negative, showError = false) { +// const errorContainer = $("#selection-error"); + +// if (!positive || !negative) { +// if (showError) { +// errorContainer.text("You must select both a positive and a negative label!").removeClass("d-none"); +// } +// return false; +// } + +// if (positive === negative) { +// if (showError) { +// errorContainer.text("Positive and Negative labels must be different!").removeClass("d-none"); +// } +// return false; +// } + +// // Clear error if valid +// errorContainer.addClass("d-none").text(""); +// return true; +// } + +document.getElementById("selection").addEventListener("change", function (e) { + var feature1 = document.getElementById("feature1").value + var feature2 = document.getElementById("feature2").value + var label = document.getElementById("label").value + var csrftoken = jQuery("[name=csrfmiddlewaretoken]").val(); + $("#stats_container").remove() + $('#loader_stats').show() + $.ajax({ + method: 'POST', + url: '', + headers: { 'X-CSRFToken': csrftoken, }, + data: { 'action': "stat", 'feature1': feature1, 'feature2': feature2, 'label': label }, + success: function (ret) { + $('#loader_stats').hide() + var ret = JSON.parse(ret) + var fig = ret["fig"] + var iDiv = document.createElement('div'); + iDiv.id = 'stats_container'; + iDiv.insertAdjacentHTML('beforeend', fig); + $("#stats_div").append(iDiv); + + }, + error: function (ret) { + } + }); +}); + +if (document.getElementById("selection_cached")) { + document.getElementById("selection_cached").addEventListener("change", function (e) { + + var feature1 = document.getElementById("feature1_cached").value + var feature2 = document.getElementById("feature2_cached").value + var label = document.getElementById("label_cached").value + var csrftoken = jQuery("[name=csrfmiddlewaretoken]").val(); + $("#stats_container_cached").html("") + $('#loader_stats_cached').show() + $.ajax({ + method: 'POST', + url: '', + headers: { 'X-CSRFToken': csrftoken, }, + data: { 'action': "stat", 'feature1': feature1, 'feature2': feature2, 'label': label }, + success: function (ret) { + $('#loader_stats_cached').hide() + var ret = JSON.parse(ret) + var fig = ret["fig"] + var iDiv = document.createElement('div'); + iDiv.id = 'stats_container_cached'; + iDiv.insertAdjacentHTML('beforeend', fig); + $("#stats_container_cached").html(fig) + // $("#stats_container_cached").append(iDiv); + + }, + error: function (ret) { + } + }); + }); +} \ No newline at end of file diff --git a/base/static/js/import.js b/base/static/js/import.js deleted file mode 100755 index 100d09d14..000000000 --- a/base/static/js/import.js +++ /dev/null @@ -1,21 +0,0 @@ -function enterdata() { - //Highlight clicked row - document.getElementById('upload_btn').addEventListener('click', function () { - var data1 = new FormData() - var csrftoken = jQuery("[name=csrfmiddlewaretoken]").val(); - data1.append('excel_file', $('#doc')[0].files[0]) - console.log(data1) - $.ajax({ - method: 'POST', - url: '', - headers: { 'X-CSRFToken': csrftoken, }, - data: { 'action': "upload_csv", 'data': data1 }, - success: function (ret) { - console.log(ret) - }, - error: function (ret) { - - } - }); - }); -}; diff --git a/base/static/js/main.js b/base/static/js/main.js index d943d03a9..248eff38c 100644 --- a/base/static/js/main.js +++ b/base/static/js/main.js @@ -73,20 +73,48 @@ document.addEventListener("DOMContentLoaded", function () { if (document.getElementById("backToDatasetButton")) { document.getElementById("backToDatasetButton").addEventListener("click", function () { // Redirect to the dataset selection section - window.location.href = "/#dataset_selection"; // Replace with the actual URL + window.location.href = "/#dataset_selection"; }); } if (document.getElementById("viewCounterfactualsButton")) { document.getElementById("viewCounterfactualsButton").addEventListener("click", function () { // Redirect to the counterfactuals view section - window.location.href = "/counterfactuals.html"; // Replace with the actual URL + window.location.href = "/counterfactuals.html"; }); } if (document.getElementById("viewPreTrainedButton")) { document.getElementById("viewPreTrainedButton").addEventListener("click", function () { - // Redirect to the counterfactuals view section - window.location.href = "/charts.html"; // Replace with the actual URL + // Redirect to the pre trained view section + window.location.href = "/charts.html"; }); -} \ No newline at end of file +} + +// JavaScript to handle delete functionality +document.addEventListener("DOMContentLoaded", function () { + + document.getElementById("radio_buttons").addEventListener("click", function (event) { + // Identify if the click originated from the button or its child span + let targetButton = event.target.closest(".delete-file-icon"); + console.log(targetButton) + // Only proceed if a delete-file-icon button was clicked + if (targetButton) { + // Get the filename from the data-file attribute + const fileName = targetButton.getAttribute("data-file"); + + const fileNameValue = targetButton.getAttribute("data-file-value"); + + // Set the file name in the modal for display + document.getElementById("fileToDeleteName").innerText = fileName; + + // Set the filename in the confirm button for reference during deletion + document.getElementById("confirmDeleteButton").setAttribute("data-file", fileName); + + document.getElementById("confirmDeleteButton").setAttribute("data-file-value", fileNameValue); + + // Show the delete confirmation modal + $('#deleteFileModal').modal('show'); + } + }); +}); \ No newline at end of file diff --git a/base/static/js/methods.js b/base/static/js/methods.js index ec8db56cc..557bc1c16 100755 --- a/base/static/js/methods.js +++ b/base/static/js/methods.js @@ -81,10 +81,10 @@ function transpose_table(tableHtml) { for (let colIndex = 0; colIndex < colNames.length; colIndex++) { transposedHtml += `<tr><td>${colNames[colIndex]}</td>`; transposedHtml += `<td>${rows[colIndex]}</td>`; - transposedHtml += `<td>${rows[colIndex + colNames.length ]}</td>`; + transposedHtml += `<td>${rows[colIndex + colNames.length]}</td>`; transposedHtml += '</tr>'; } - }else{ + } else { for (let colIndex = 0; colIndex < colNames.length; colIndex++) { transposedHtml += `<tr><td>${colNames[colIndex]}</td>`; transposedHtml += `<td>${rows[colIndex]}</td>`; @@ -98,6 +98,108 @@ function transpose_table(tableHtml) { return transposedHtml; } +function create_uploaded_file_radio(name, counter) { + var formCheckDiv = document.createElement("div"); + formCheckDiv.className = "form-check mb-1 d-flex align-items-center"; + + // Create the radio input element + var radioInput = document.createElement("input"); + radioInput.className = "form-check-input mr-2"; + radioInput.type = "radio"; + radioInput.name = "uploaded_file"; + radioInput.id = "file_" + counter; + radioInput.value = name; + radioInput.required = true; + + // Create the label element + var label = document.createElement("label"); + label.className = "form-check-label mr-auto"; + label.htmlFor = "file_" + counter; + label.innerText = name; + + // Create the delete button + var deleteButton = document.createElement("button"); + deleteButton.className = "delete-file-icon p-0 ml-2 text-muted close"; + deleteButton.type = "button"; + deleteButton.dataset.file = name; + deleteButton.setAttribute("aria-label", "Delete " + name); + + // Create the '×' span inside the delete button + var deleteIcon = document.createElement("span"); + deleteIcon.setAttribute("aria-hidden", "true"); + deleteIcon.innerHTML = "×"; + + // Append the delete icon to the delete button + deleteButton.appendChild(deleteIcon); + + // Append the radio input, label, and delete button to the container div + formCheckDiv.appendChild(radioInput); + formCheckDiv.appendChild(label); + formCheckDiv.appendChild(deleteButton); + + return formCheckDiv +} + + +function showSuccessMessage() { + const successMessage = document.getElementById("success-message"); + successMessage.classList.remove("d-none"); + + // Add a slight delay to trigger the transition + setTimeout(() => successMessage.classList.add("show"), 10); + + // Automatically hide the message after a few seconds + setTimeout(() => hideSuccessMessage(), 3000); +} + +function hideSuccessMessage() { + const successMessage = document.getElementById("success-message"); + successMessage.classList.remove("show"); + + // Delay hiding the element fully until after the transition + setTimeout(() => successMessage.classList.add("d-none"), 400); +} + +function showLoader(show, ids = ["#loader_ds", "#loader_stats"]) { + ids.forEach(id => { + $(id).toggle(show); + }); +} +function resetContainers(ids = [ + "#df_container", + "#stats_container", + "#figs", + "#df", + "#df_stats", + "#df_cached", + "#df_stats_cached", + "#ts_confidence_cached", + "#ts_stats_cached", + "#ts_confidence", + "#ts_stats" +]) { + ids.forEach(id => { + $(id).hide(); + }); +} + +function clearPreviousContent(ids = [ + "#df_container", + "#stats_container", + "#pretrained_radio", + "#feature1", + "#feature2", + "#label", + "#ts_confidence_container", + "#ts_stats_container" +]) { + ids.forEach(id => { + const element = document.querySelector(id); + if (element) element.remove(); + }); +} + + export { - create_selection, create_dataframe, create_div, transpose_table + create_selection, create_dataframe, create_div, transpose_table, create_uploaded_file_radio, showSuccessMessage, hideSuccessMessage, showLoader, clearPreviousContent, resetContainers } \ No newline at end of file diff --git a/base/static/js/radio_dataset.js b/base/static/js/radio_dataset.js deleted file mode 100755 index a197456e0..000000000 --- a/base/static/js/radio_dataset.js +++ /dev/null @@ -1,98 +0,0 @@ -import { create_dataframe, create_selection } from './methods.js'; - -$(document).ready(function () { - - function showLoader(show) { - $("#loader, #loader_stats").toggle(show); - } - - function resetContainers() { - $("#df_container, #stats_container, #figs, #df, #df_stats, #df_cached, #df_stats_cached, #ts_confidence_cached, #ts_stats_cached, #ts_confidence, #ts_stats").hide(); - } - - function clearPreviousContent() { - ["#df_container", "#stats_container", "#pretrained_radio", "#feature1", "#feature2", "#label", "#ts_confidence_container", "#ts_stats_container"].forEach(id => { - if ($(id).length) $(id).remove(); - }); - } - - function fetchDatasetData(df_name) { - const csrftoken = jQuery("[name=csrfmiddlewaretoken]").val(); - showLoader(true); - - $.ajax({ - method: 'POST', - url: '', - headers: { 'X-CSRFToken': csrftoken }, - data: { 'action': "dataset", 'df_name': df_name }, - success: function (values) { - showLoader(false); - handleSuccessResponse(values); - }, - error: function (ret) { - console.error("Failed to fetch dataset:", ret); - } - }); - } - - function handleSuccessResponse(values) { - if (!values) return; - - clearPreviousContent(); - const ret = JSON.parse(values); - const datasetType = ret["dataset_type"]; - - if (datasetType === "tabular") { - setupTabularDataset(ret); - } else if (datasetType === "timeseries") { - setupTimeseriesDataset(ret); - } - } - - function setupTabularDataset(ret) { - const { data_to_display: df, fig, features, feature1, feature2, labels, curlabel } = ret; - - const selection1 = create_selection(features, "feature1", null, feature1); - const selection2 = create_selection(features, "feature2", null, feature2); - const selection3 = create_selection(labels, "label", null, curlabel); - - const tb = create_dataframe(df, "df_container"); - - $("#model_container, #df, #df_stats").fadeIn(200); - $("#df_div").append(tb); - $("#selection").append(selection1, selection2, selection3); - - const figDiv = $("<div>", { id: 'stats_container', class: "plotly_fig" }).html(fig); - $("#stats_div").append(figDiv); - } - - function setupTimeseriesDataset(ret) { - const { fig, fig1 } = ret; - - const figDiv = $("<div>", { id: 'ts_confidence_container', class: "plotly_fig" }).html(fig); - const figDiv1 = $("<div>", { id: 'ts_stats_container', class: "plotly_fig" }).html(fig1); - - $("#ts_stats, #ts_confidence").fadeIn(200); - $("#ts_stats_div").append(figDiv); - $("#ts_confidence_div").append(figDiv1); - } - - $('.btn-dataset').click(function (e) { - const df_name = $(this).is('#upload') ? "upload" : $(this).attr('id'); - resetContainers(); - $("#upload_col").toggle(df_name === "upload"); - $("#timeseries-datasets").toggle(df_name === "timeseries"); - - $(this).toggleClass("active").siblings().removeClass("active"); - $(this).addClass("active"); - const timeseries_dataset = df_name === "timeseries" ? $("input:radio[name=timeseries_dataset]:checked").val() : ""; - if (timeseries_dataset || df_name !== "timeseries") { - fetchDatasetData(timeseries_dataset || df_name); - } - }); -}); - -document.getElementById("viewModelsButton").addEventListener("click", function () { - // Prompt or redirect the user to the pre-trained models section - window.location.href = "/charts.html"; // Replace with the actual URL -}); \ No newline at end of file diff --git a/base/static/js/radio_model.js b/base/static/js/radio_model.js index 6fb99c6e3..3306b7427 100755 --- a/base/static/js/radio_model.js +++ b/base/static/js/radio_model.js @@ -1,10 +1,7 @@ -import { create_dataframe, create_div, transpose_table } from './methods.js' - -// pretrained model selection and -// train new model selection (not execution) +import { create_dataframe, create_div } from './methods.js' $(document).ready(function () { - $('#pre_trained_models').change(function () { + $('#radio_buttons').change(function () { if ($("input[name=modeling_options]:checked").length > 0) { // pre trained model selected @@ -18,8 +15,6 @@ $(document).ready(function () { var url = "" if (currentUrl.includes('charts.html')) { - $("#figs").hide(); - $("#figs_2").hide(); // if they already exist, remove them and update them if (document.getElementById("principle_component_analysis")) { @@ -118,4 +113,77 @@ $(document).ready(function () { }); } }); + + document.getElementById('confirmDeleteButton').addEventListener('click', function () { + const fileNameValue = this.getAttribute('data-file-value'); + const fileName = this.getAttribute('data-file'); + const csrftoken = jQuery("[name=csrfmiddlewaretoken]").val(); + const uploadedDataset = $("input:radio[name=radio_buttons]:checked").val(); + // AJAX request to delete file + $.ajax({ + type: 'POST', + url: '', // Add the URL where this request should go + data: { + action: 'delete_pre_trained', + model_name: fileNameValue, + csrfmiddlewaretoken: csrftoken // Django CSRF token + }, + success: function () { + // Remove the file entry from the UI + const fileElement = $(`[data-file="${fileName}"]`).closest('.form-check'); + fileElement.remove(); + + // Check if there are any remaining .form-check elements + if ($('#radio_buttons .form-check').length === 0) { + // Replace the #radio_buttons content with the fallback message + const radioButtonsContainer = document.querySelector('#radio_buttons'); + radioButtonsContainer.innerHTML = ` + <p class="text-danger"> + There are no available pre-trained models. + Please <a href="/train.html" class="text-primary">train a model</a>. + </p> + `; + } + + // Attach a success message to the modal + const modalBody = document.querySelector('#deleteFileModal .modal-body'); + modalBody.innerHTML = ` + <div class="alert alert-success mb-3" role="alert"> + <i class="fas fa-check-circle mr-2"></i> + The file <strong>${fileName}</strong> has been successfully deleted. + </div> + `; + + // Optionally hide the modal after a delay + setTimeout(() => { + $('#deleteFileModal').modal('hide'); + modalBody.innerHTML = ''; // Clear the message after hiding + }, 2000); + + // Reset containers if the deleted file is the uploaded dataset + if (fileName === uploadedDataset) { + resetContainers(); + } + }, + + error: function () { + // Attach an error message to the modal + const modalBody = document.querySelector('#deleteFileModal .modal-body'); + modalBody.innerHTML = ` + <div class="alert alert-danger mb-3" role="alert"> + <i class="fas fa-times-circle mr-2"></i> + An error occurred while deleting the file. Please try again. + </div> + `; + + // Optionally reset the modal content after a delay + setTimeout(() => { + modalBody.innerHTML = ` + <p class="mb-1">Delete <span id="fileToDeleteName" class="font-weight-bold"></span> pre-trained classifier on <span class="font-weight-bold"> {{ df_name }} </span> dataset?</p> + <small class="text-muted">This action is permanent.</small> + `; + }, 3000); + } + }); + }); }); diff --git a/base/static/js/radio_timeseries_dataset.js b/base/static/js/radio_timeseries_dataset.js index 23f2bba8b..d74eb8b28 100644 --- a/base/static/js/radio_timeseries_dataset.js +++ b/base/static/js/radio_timeseries_dataset.js @@ -1,81 +1,73 @@ -import { create_dataframe, create_selection } from './methods.js' +import { create_dataframe, create_selection } from './methods.js'; $(document).ready(function () { - $('#timeseries-datasets').change(function () { - if ($("input[name=timeseries_dataset]:checked").length > 0) { + const selectedDataset = $("input[name=timeseries_dataset]:checked").val(); - var timeseries_dataset = $("input:radio[name=timeseries_dataset]:checked").val(); + if (selectedDataset) { + resetContainers(); + toggleSkeletons(true); - $("#df_container").hide(); - $("#stats_container").hide(); - $("#figs").hide(); - - $("#ts_confidence_cached").hide() - $("#ts_stats_cached").hide() - - $("#ts_confidence").hide() - $("#ts_stats").hide() - var csrftoken = jQuery("[name=csrfmiddlewaretoken]").val(); - - $("#loader").show(); - $("#loader_stats").show(); + const csrfToken = $("[name=csrfmiddlewaretoken]").val(); + // AJAX request to fetch the dataset $.ajax({ method: 'POST', - url: '', - headers: { 'X-CSRFToken': csrftoken, }, - data: { 'action': "timeseries-dataset", 'timeseries_dataset': timeseries_dataset }, - success: function (values) { - $("#loader").hide(); - $("#loader_stats").hide(); - // fetch data - // remove data if already displayed - if (document.getElementById("df_container")) { - $("#pretrained_radio").remove(); - $("#df_container").remove(); - $("#stats_container").remove(); - $("#feature1").remove(); - $("#feature2").remove(); - $("#label").remove(); - } - - if (document.getElementById("ts_confidence_container")) { - $("#ts_confidence_container").remove(); - $("#ts_stats_container").remove(); - } - - var ret = JSON.parse(values) - var dataset_type = ret["dataset_type"] - - if (values) { - // timeseries - // var feature = ret["feature"] - var fig = ret["fig"] - var fig1 = ret["fig1"] - - var iDiv = document.createElement('div'); - iDiv.id = 'ts_confidence_container'; - iDiv.innerHTML = fig; - iDiv.setAttribute("class", "plotly_fig") - - var iDiv1 = document.createElement('div'); - iDiv1.id = 'ts_stats_container'; - iDiv1.innerHTML = fig1; - iDiv1.setAttribute("class", "plotly_fig") - - $("#ts_stats").show(); - $("#ts_confidence").show(); - - $("#ts_stats_div").append(iDiv); - $("#ts_confidence_div").append(iDiv1); - } + url: '', // Specify your endpoint here + headers: { 'X-CSRFToken': csrfToken }, + data: { action: "timeseries-dataset", timeseries_dataset: selectedDataset }, + success: function (response) { + alert("herereererer") + toggleSkeletons(false); + handleTimeseriesResponse(response); }, - error: function (ret) { - console.log("All bad") + error: function (error) { + toggleSkeletons(false); + console.error("An error occurred:", error); } - }); } - }) -}); \ No newline at end of file + }); + + /** + * Reset the containers by hiding any previous data. + */ + function resetContainers() { + const elementsToHide = [ + "#df_div", "#stats_div", "#ts_confidence_div", "#ts_stats_div" + ]; + elementsToHide.forEach(selector => $(selector).empty().hide()); + } + + /** + * Toggle skeleton loaders for a smooth user experience. + * @param {boolean} show - Whether to show or hide the skeleton loaders. + */ + function toggleSkeletons(show) { + const skeletonSelectors = [ + "#df_skeleton", "#stats_skeleton", "#ts_confidence_skeleton", "#ts_stats_skeleton" + ]; + skeletonSelectors.forEach(selector => $(selector).toggle(show)); + } + + /** + * Handle the response for timeseries dataset. + * @param {Object|string} response - The server response. + */ + function handleTimeseriesResponse(response) { + try { + const data = JSON.parse(response); + if (!data) throw new Error("Invalid response format"); + + // Populate data and stats + if (data.fig) { + $("#ts_confidence_div").html(data.fig).show(); + } + if (data.fig1) { + $("#ts_stats_div").html(data.fig1).show(); + } + } catch (error) { + console.error("Failed to process response:", error); + } + } +}); diff --git a/base/static/js/radio_uploaded_dataset.js b/base/static/js/radio_uploaded_dataset.js deleted file mode 100644 index b48757961..000000000 --- a/base/static/js/radio_uploaded_dataset.js +++ /dev/null @@ -1,118 +0,0 @@ -import { create_dataframe, create_selection } from './methods.js' - -$(document).ready(function () { - - $('#uploaded_file').change(function () { - if ($("input[name=uploaded_file]:checked").length > 0) { - - var uploaded_dataset = $("input:radio[name=uploaded_file]:checked").val(); - - $("#df_container").hide(); - $("#stats_container").hide(); - $("#figs").hide(); - - $("#ts_confidence_cached").hide() - $("#ts_stats_cached").hide() - - $("#ts_confidence").hide() - $("#ts_stats").hide() - var csrftoken = jQuery("[name=csrfmiddlewaretoken]").val(); - - $("#loader").show(); - $("#loader_stats").show(); - - $.ajax({ - method: 'POST', - url: '', - headers: { 'X-CSRFToken': csrftoken, }, - data: { 'action': "uploaded_datasets", 'df_name': uploaded_dataset }, - success: function (values) { - $("#loader").hide(); - $("#loader_stats").hide(); - // fetch data - // remove data if already displayed - if (document.getElementById("df_container")) { - $("#pretrained_radio").remove(); - $("#df_container").remove(); - $("#stats_container").remove(); - $("#feature1").remove(); - $("#feature2").remove(); - $("#label").remove(); - } - - if (document.getElementById("ts_confidence_container")) { - $("#ts_confidence_container").remove(); - $("#ts_stats_container").remove(); - } - - var ret = JSON.parse(values) - var dataset_type = ret["dataset_type"] - - if (values) { - if (dataset_type == "tabular") { - var df = ret["data_to_display"] - var fig = ret["fig"] - var features = ret["features"] - var feature1 = ret["feature1"] - var feature2 = ret["feature2"] - - // cur labels - var labels = ret["labels"] - var curlabel = ret["curlabel"] - - var selection1 = create_selection(features, "feature1", null, feature1) - var selection2 = create_selection(features, "feature2", null, feature2) - var selection3 = create_selection(labels, "label", null, curlabel) - - // create table - var tb = create_dataframe(df, "df_container") - - $("#model_container").show() - $("#df").show(); - $("#df_stats").show(); - - // append new data - $("#df_div").append(tb); - $("#selection").append(selection1); - $("#selection").append(selection2); - $("#selection").append(selection3); - - // append fig - var iDiv = document.createElement('div'); - iDiv.id = 'stats_container'; - iDiv.innerHTML = fig; - iDiv.setAttribute("class", "plotly_fig") - - $("#stats_div").append(iDiv); - } else if (dataset_type == "timeseries") { - - // timeseries - // var feature = ret["feature"] - var fig = ret["fig"] - var fig1 = ret["fig1"] - - var iDiv = document.createElement('div'); - iDiv.id = 'ts_confidence_container'; - iDiv.innerHTML = fig; - iDiv.setAttribute("class", "plotly_fig") - - var iDiv1 = document.createElement('div'); - iDiv1.id = 'ts_stats_container'; - iDiv1.innerHTML = fig1; - iDiv1.setAttribute("class", "plotly_fig") - - $("#ts_stats").show(); - $("#ts_confidence").show(); - - $("#ts_stats_div").append(iDiv); - $("#ts_confidence_div").append(iDiv1); - - } - } - }, - error: function (ret) { - } - }); - } - }) -}); \ No newline at end of file diff --git a/base/static/js/train.js b/base/static/js/train.js index 1b5561190..9cc8e15ed 100755 --- a/base/static/js/train.js +++ b/base/static/js/train.js @@ -1,6 +1,5 @@ // train a new model -import { create_dataframe, create_div, change_nav_text } from './methods.js' -change_nav_text("train_nav") +import { create_dataframe, create_div } from './methods.js' $(document).ready(function () { @@ -26,57 +25,285 @@ $(document).ready(function () { }); $('.train_test').click(function () { - // get preprocessing variables, model and test set ratio - // send ajax request to back end and wait for results - var array_preprocessing = [] + const classifier = document.getElementById("classifier").value; + const errorMessage = $("#error_message_new_x_2"); - $("#loader_train").show(); - var test_set_ratio - var classifier = document.getElementById("classifier").value - var class_label = "" - var autoencoder = "" - var data_to_pass = {} - if (classifier != "wildboar_knn" && classifier != "wildboar_rsf" && classifier != "glacier") { - class_label = document.getElementById("class_label_train").value - test_set_ratio = document.getElementById("slider").value - document.getElementsByName("boxes").forEach(function (elem) { - if (elem.checked == true) { - array_preprocessing.push(elem.value); - } - }); - data_to_pass = { 'action': "train", 'model_name': classifier, 'test_set_ratio': test_set_ratio, 'array_preprocessing': JSON.stringify(array_preprocessing), 'class_label': class_label } - } else if (classifier == "glacier") { - // time series data, no class label - autoencoder = document.getElementById("autoencoder").value - // TODO: maybe add test set ratio - data_to_pass = { 'action': "train", 'model_name': classifier, 'autoencoder': autoencoder } - } else if (classifier == "wildboar_knn" || classifier == "wildboar_rsf") { - test_set_ratio = document.getElementById("slider").value - document.getElementsByName("boxes").forEach(function (elem) { - if (elem.checked == true) { - array_preprocessing.push(elem.value); - } - }); - data_to_pass = { 'action': "train", 'model_name': classifier, 'test_set_ratio': test_set_ratio, 'array_preprocessing': JSON.stringify(array_preprocessing) } + let array_preprocessing = []; + let test_set_ratio, class_label, autoencoder; + let data_to_pass = {}; + + // Helper function to show errors + function showError(message) { + errorMessage.text(message); + errorMessage.show(); } - // ajax request for training - var csrftoken = jQuery("[name=csrfmiddlewaretoken]").val(); + + // Helper function to get checked values of checkboxes by name + function getCheckedValues(name) { + return Array.from(document.getElementsByName(name)) + .filter((elem) => elem.checked) + .map((elem) => elem.value); + } + + // Check if a classifier is selected + if (!classifier) { + // Show loader while training + showError("Please select a classifier before proceeding."); + return; + } + + // Check if at least one preprocessing checkbox is checked + const anyPreprocessingChecked = getCheckedValues("boxes").length > 0; + + if (!anyPreprocessingChecked && classifier !== "glacier") { + showError("Please select at least one preprocessing option."); + return; + } + + // Hide the error message if validations pass + errorMessage.hide(); + + $("#train_test_btn").hide() + + // Show loader while training + $("#loader_train").removeClass("d-none").show(); + + // Set up data to pass based on classifier + if (classifier === "glacier") { + autoencoder = document.getElementById("autoencoder").value; + data_to_pass = { + action: "train", + model_name: classifier, + autoencoder: autoencoder + }; + } else { + test_set_ratio = document.getElementById("slider").value; + class_label = document.getElementById("class_label_train")?.value || ""; + array_preprocessing = getCheckedValues("boxes"); + + data_to_pass = { + action: "train", + model_name: classifier, + test_set_ratio: test_set_ratio, + array_preprocessing: JSON.stringify(array_preprocessing), + class_label: class_label + }; + } + + // AJAX request for training + const csrftoken = jQuery("[name=csrfmiddlewaretoken]").val(); $.ajax({ method: 'POST', url: '', - headers: { 'X-CSRFToken': csrftoken, }, + headers: { 'X-CSRFToken': csrftoken }, data: data_to_pass, - processData: true, // This should be `true` for form data - contentType: 'application/x-www-form-urlencoded; charset=UTF-8', // Standard form content type, - success: function (values) { + processData: true, + contentType: 'application/x-www-form-urlencoded; charset=UTF-8', + success: function (ret) { $("#loader_train").hide(); + + try { + $("#train_test_btn").show() + + // if they already exist, remove them and update them + if (document.getElementById("principle_component_analysis")) { + $("#principle_component_analysis").remove(); + } + if (document.getElementById("class_report")) { + $("#class_report").remove(); + } + if (document.getElementById("feature_importance")) { + $("#feature_importance").remove(); + } + if (document.getElementById("classifier_data")) { + $("#classifier_data").remove(); + } + if (document.getElementById("tsne_plot")) { + $("#tsne_plot").remove(); + } + + var ret = JSON.parse(ret) + // Parse successful response data + const class_report = ret["class_report"]; + const classifier_data = ret["classifier_data"]; + const pca = ret["pca"]; + const dataset_type = ret["dataset_type"]; + $("#tab").show(); + + if (dataset_type == "timeseries") { + // For timeseries datasets + const tsne = ret["tsne"]; + $("#tsne-tab-nav").show(); + const col_div_tsne = create_div("tsne_plot", "plotly_fig"); + col_div_tsne.insertAdjacentHTML('beforeend', tsne); + $("#tsne_container").append(col_div_tsne); + } else { + // For other datasets + $("#feature-tab-nav").show(); + const feature_importance = ret["feature_importance"]; + const col_div_fi = create_div("feature_importance", "plotly_fig"); + col_div_fi.insertAdjacentHTML('beforeend', feature_importance); + $("#fi_container").append(col_div_fi); + } + + // Create and append dataframes + const tb = create_dataframe(classifier_data, "details_container"); + const cr_tb = create_dataframe(class_report, "cr_container"); + + // Create and append plots + const col_div_pca = create_div("principle_component_analysis", "plotly_fig"); + col_div_pca.insertAdjacentHTML('beforeend', pca); + + const col_div_class_report = create_div("class_report", "plotly_fig sticky-top-table"); + col_div_class_report.append(cr_tb); + + const col_div_classifier_data = create_div("classifier_data", "plotly_fig sticky-top-table"); + col_div_classifier_data.append(tb); + + // Append content to modal tabs + $("#classification_report").append(col_div_class_report); + $("#details").append(col_div_classifier_data); + $("#pca_container").append(col_div_pca); + + // Show modal for analysis + $("#modelAnalysisModal").modal("show"); + + } catch (e) { + console.error("Error processing response:", e); + $("#modelAnalysisModal").modal("show"); + } }, error: function (ret) { + $("#loader_train").hide(); + + // Prepare error message + const errorMessage = $("#error_message_new_x_2"); + const errorMessageText = $("#error_message_text"); + let backendErrorMessage = "An error occurred."; // Default message + + try { + if (ret.responseJSON && ret.responseJSON.message) { + backendErrorMessage = ret.responseJSON.message + ret.responseJSON.line; + } else if (ret.responseText) { + const parsedResponse = JSON.parse(ret.responseText); + backendErrorMessage = parsedResponse.message || backendErrorMessage; + } + } catch (e) { + console.error("Error parsing error response:", e); + backendErrorMessage = ret.responseText || "Unknown error."; + } + + // Display error message and trigger modal + errorMessageText.text(backendErrorMessage); + errorMessage.show(); } }); + }); + document.getElementById("discard-model").addEventListener("click", function () { + // Append a confirmation message to the modal + const modalBody = document.querySelector("#modelAnalysisModal .modal-body"); + const messageContainer = document.createElement("div"); + messageContainer.id = "discard-message"; + messageContainer.className = "alert"; // Bootstrap class for alert styles + + // Add a message to confirm the user's decision + messageContainer.classList.add("alert-warning"); + messageContainer.innerHTML = ` + <i class="fas fa-exclamation-triangle mr-2"></i> + Are you sure you want to discard this model? This action cannot be undone. + <div class="mt-3"> + <button id="confirm-discard" class="btn btn-danger btn-sm">Yes, Discard</button> + <button id="cancel-discard" class="btn btn-secondary btn-sm">Cancel</button> + </div> + `; + modalBody.appendChild(messageContainer); + + // Add event listeners for confirm and cancel buttons + document.getElementById("confirm-discard").addEventListener("click", function () { + // Data to send in the AJAX request + const data = { action: "discard_model" }; + + // Fetch CSRF token (assuming Django or similar framework) + const csrftoken = jQuery("[name=csrfmiddlewaretoken]").val(); + + // Send AJAX POST request to the backend + $.ajax({ + method: "POST", + url: "", // Replace with your actual backend URL + headers: { "X-CSRFToken": csrftoken }, // Include CSRF token + data: data, + success: function (response) { + // Update the modal with a success message + messageContainer.classList.remove("alert-warning"); + messageContainer.classList.add("alert-success"); + messageContainer.innerHTML = ` + <i class="fas fa-check-circle mr-2"></i> + The model has been successfully discarded. + `; + + // Optionally close the modal after a delay + setTimeout(() => { + $("#modelAnalysisModal").modal("hide"); + // Optionally refresh the page or update UI + // location.reload(); // Uncomment to refresh the page + }, 2000); + }, + error: function (xhr) { + // Update the modal with an error message + messageContainer.classList.remove("alert-warning"); + messageContainer.classList.add("alert-danger"); + const errorMessage = xhr.responseJSON?.message || "An error occurred while discarding the model."; + messageContainer.innerHTML = ` + <i class="fas fa-times-circle mr-2"></i> + Failed to discard the model: ${errorMessage}. + `; + }, + }); + }); + + // Cancel discard operation + document.getElementById("cancel-discard").addEventListener("click", function () { + // Remove the confirmation message + modalBody.removeChild(messageContainer); + }); + }); + + document.getElementById("save-model").addEventListener("click", function () { + // Get the modal body element + const modalBody = document.querySelector("#modelAnalysisModal .modal-body"); + + // Create a confirmation message container + const confirmationMessage = document.createElement("div"); + confirmationMessage.className = "alert alert-success mt-3"; // Bootstrap alert styles + confirmationMessage.innerHTML = ` + <i class="fas fa-check-circle mr-2"></i> + The model has been successfully saved! + `; + + // Clear existing content in the modal body (optional) + modalBody.innerHTML = ""; + + // Append the confirmation message to the modal body + modalBody.appendChild(confirmationMessage); + + // Set a timeout to hide the modal after showing the message + setTimeout(() => { + $("#modelAnalysisModal").modal("hide"); + + // Optionally reset the modal body content after hiding + setTimeout(() => { + modalBody.innerHTML = ` + <div class="alert alert-info"> + <i class="fas fa-info-circle mr-2"></i> + After training your model/classifier, you should now decide whether to <strong>keep</strong> it or <strong>discard</strong> it based on its performance metrics and visualizations below. + </div> + <!-- Tabs Navigation and other content here --> + `; + }, 500); // Small delay to ensure the modal is fully hidden before resetting + }, 2000); // Hide the modal after 2 seconds + }); - }) }); diff --git a/base/templates/base/charts.html b/base/templates/base/charts.html index fba3223af..157251bd1 100755 --- a/base/templates/base/charts.html +++ b/base/templates/base/charts.html @@ -60,11 +60,11 @@ <div class="card-body"> {% csrf_token %} <p class="text-muted"><strong>Select a pre-trained model below to view its detailed analysis:</strong></p> - <div id="pre_trained_models"> + <div id="radio_buttons"> {% if not df_name %} <p class="text-muted">The available pre-trained models will show up here. You first need to <a href="/" class="text-primary">pick or upload a dataset</a>.</p> {% elif not available_pretrained_models_info %} - <p class="text-danger">There are no available pre-trained models for <b>{{df_name}}</b>. Please <a href="/train.html" class="text-primary">train a model</a>.</p> + <p class="text-danger">There are no available pre-trained models. Please <a href="/train.html" class="text-primary">train a model</a>.</p> {% else %} {% for value, text in available_pretrained_models_info %} <div class="form-check py-1"> @@ -78,6 +78,7 @@ </div> </div> </div> + <!-- Figures Section: Classification, PCA, Feature Importance --> <div class="row" id="tab" style="display:none;"> <div class="col-lg-12"> @@ -147,6 +148,28 @@ </div> </div> + <!-- Minimal Delete Confirmation Modal --> + <div class="modal fade" id="deleteFileModal" tabindex="-1" role="dialog" aria-labelledby="deleteFileModalLabel" aria-hidden="true"> + <div class="modal-dialog modal-dialog-centered" role="document"> + <div class="modal-content border-0 shadow-sm"> + <div class="modal-header border-0"> + <h6 class="modal-title text-danger" id="deleteFileModalLabel">Confirm Deletion</h6> + <button type="button" class="close text-muted" data-dismiss="modal" aria-label="Close" style="font-size: 1.2rem;"> + × + </button> + </div> + <div class="modal-body text-center py-3"> + <p class="mb-1">Delete <span id="fileToDeleteName" class="font-weight-bold"></span> pre trained classifier on <span class="font-weight-bold"> {{ df_name }} </span> dataset?</p> + <small class="text-muted">This action is permanent.</small> + </div> + <div class="modal-footer justify-content-center border-0"> + <button type="button" class="custom-btn-secondary" data-dismiss="modal">Cancel</button> + <button type="button" class="custom-btn-danger" id="confirmDeleteButton">Delete</button> + </div> + </div> + </div> + </div> + <div class="row mt-3" id="new_or_load"> <div class="col d-flex justify-content-center"> <div class="text-center mt-4 d-flex justify-content-center"> diff --git a/base/templates/base/counterfactuals.html b/base/templates/base/counterfactuals.html index 2b528afac..d90f6eb4f 100755 --- a/base/templates/base/counterfactuals.html +++ b/base/templates/base/counterfactuals.html @@ -134,7 +134,7 @@ <i class="fas fa-info-circle"></i> </button> </div> - <div class="card-body"> + <div class="card-body" id="radio_buttons"> {% csrf_token %} {% if not df_name %} <p class="text-muted"> @@ -195,13 +195,13 @@ </div> {% endfor %} {% else %} - <div class="alert alert-warning text-center mb-0">No pre-computed experiments available.</div> + <div class="alert alert-warning text-center mb-0" id="no-pre-computed">No pre-computed experiments available.</div> {% endif %} - </div> + </div> </div> </div> </div> - </div> + </div> <!-- Modal for Glacier Overview --> <div class="modal fade" id="glacierInfoModal" tabindex="-1" role="dialog" aria-labelledby="glacierInfoModalLabel" aria-hidden="true"> @@ -278,9 +278,10 @@ </div> </div> </div> + <!-- Right Column: New Experiment Details --> - <div class="row" id="new_experiment_details" style="display:none;"> - <div class="col-xl-4 col-lg-4 mb-4" > + <!-- <div class="row" id="new_experiment_details" style="display:none;" style="padding-top: 50px;"> + <div class="col-xl-4 col-lg-4 mb-4"> <div class="card border-0 shadow-sm h-100"> <div class="card-header bg-light text-dark py-3 d-flex justify-content-between align-items-center"> <h6 class="m-0 font-weight-bold">New Experiment Details</h6> @@ -299,7 +300,6 @@ <input name="w_value" id="slider" type="range" min="0" max="1" step="0.1" class="w-100 mb-2"> <output id="value" class="d-block text-center font-weight-bold"></output> - <!-- Run Experiment Button --> <div class="d-flex justify-content-center mt-4"> <div id="error_message_new_x" class="alert alert-danger text-center" style="display: none; width: 100%; max-width: 400px;" role="alert"> <i class="fas fa-exclamation-triangle"></i> Please correct errors before proceeding. @@ -310,8 +310,75 @@ </div> </div> </div> - </div> + </div> --> + <!-- Modal Structure --> + <div class="modal fade" id="newExperimentModal" tabindex="-1" role="dialog" aria-labelledby="newExperimentModalLabel" aria-hidden="true"> + <div class="modal-dialog modal-lg" role="document"> + <div class="modal-content shadow-lg"> + <!-- Modal Header --> + <div class="modal-header bg-primary text-white"> + <h5 class="modal-title" id="newExperimentModalLabel">New Experiment Details</h5> + <button type="button" class="close text-white" data-dismiss="modal" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + </div> + <!-- Modal Body --> + <div class="modal-body p-4"> + <!-- Constraint Selection --> + <div class="form-group mb-4"> + <label for="constraint" class="font-weight-bold text-muted">Constraint</label> + <select required id="constraint" class="form-control" aria-describedby="constraintHelp"> + <option value="" disabled selected hidden>Select constraints for counterfactuals</option> + <option value="unconstrained">Unconstrained</option> + <option value="local">Local</option> + <option value="global">Global</option> + <option value="uniform">Uniform</option> + </select> + <small id="constraintHelp" class="form-text text-muted">Choose the type of constraint for the experiment.</small> + </div> + + <!-- Predicted Margin Weight --> + <div class="form-group mb-4"> + <label for="slider" class="font-weight-bold text-muted">Predicted Margin Weight</label> + <input name="w_value" id="slider" type="range" min="0" max="1" step="0.1" class="form-control-range"> + <output id="value" class="d-block text-center font-weight-bold mt-2">0.5</output> + </div> + + <!-- Error Message --> + <div id="error_message_new_x" class="alert d-none" role="alert"></div> + + <!-- Success Message --> + <div id="success_message" class="alert alert-success d-none" role="alert"></div> + + <!-- Run Experiment Button --> + <div class="d-flex justify-content-center mt-4"> + <button class="btn btn-primary btn-lg compute_counterfactual d-flex align-items-center" id="cfbtn_2" role="button" name="cf"> + <i class="fas fa-play-circle mr-2"></i> Run New Experiment! + </button> + </div> + <div class="row justify-content-center align-items-center my-4"> + <div id="cfbtn_loader_2" class="col-auto" style="display:none;"> + <div class="d-flex align-items-center text-muted"> + <div class="spinner-border spinner-border-sm text-primary mr-2" role="status"></div> + <span>Processing...</span> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + + +<script> + // Update slider output dynamically + const slider = document.getElementById("slider"); + const output = document.getElementById("value"); + slider.addEventListener("input", () => { + output.textContent = slider.value; + }); +</script> <!-- Optional Column: Class Label --> {% if dataset_type == "timeseries" %} <div class="row" id="class_label_container" style="display: none; padding-top: 70px;"> @@ -374,8 +441,12 @@ {% endif %} <div class="d-flex justify-content-center mt-4"> - <div id="error_message_new_x_2" class="alert alert-danger text-center" style="display: none; width: 100%; max-width: 400px;" role="alert"> - <i class="fas fa-exclamation-triangle"></i> Please correct errors before proceeding. + <div id="error_message_new_x_2" class="alert alert-danger alert-dismissible text-center" style="display: none; width: 100%; max-width: 400px;" role="alert"> + <i class="fas fa-exclamation-triangle"></i> + <span>Please correct errors before proceeding.</span> + <button type="button" class="close" aria-label="Close" onclick="$('#error_message_new_x_2').hide();"> + <span aria-hidden="true">×</span> + </button> </div> </div> diff --git a/base/templates/base/home.html b/base/templates/base/home.html index cd0e9d22f..06a35dffb 100755 --- a/base/templates/base/home.html +++ b/base/templates/base/home.html @@ -6,128 +6,288 @@ <!-- Main Content --> <div id="content"> - <!-- Introduction Section with Collapsible Content --> - <div id="home_intro" class="intro-section py-5 bg-light text-center"> - <div class="container"> - <h1 class="h3 text-dark mb-3"> - Welcome to <a href="https://datascience.dsv.su.se/projects/extremum.html" target="_blank" class="text-primary">Extremum Dashboard</a> + <!-- Intro Section --> + <div id="home_intro" class="intro-section py-5 text-center position-relative"> + <div class="container"> + <!-- Animated Background Graphics --> + <div class="background-shape shape-1"></div> + <div class="background-shape shape-2"></div> + + <!-- Main Heading --> + <div class="intro-content position-relative"> + <div class="logos d-flex justify-content-center align-items-center mb-4 fade-in"> + <img src="{% static 'img/su_logo.png' %}" alt="Stockholm University Logo" class="logo su-logo mx-3"> + <img src="{% static 'img/digital_features.png' %}" alt="Digital Features Logo" class="logo df-logo mx-3"> + </div> + <h1 class="display-4 text-dark mb-4 fade-in"> + Welcome to the <a href="https://datascience.dsv.su.se/projects/extremum.html" target="_blank" class="text-primary">Extremum Dashboard</a> </h1> - <p class="lead text-muted">An efficient way to explore health informatics and time-series datasets with ease.</p> + <p class="lead text-muted fade-in mx-auto" style="max-width: 800px;"> + Your gateway to exploring health informatics and time-series datasets with ease. + </p> + </div> + </div> +</div> - <!-- Learn More Button with Expand/Collapse Functionality --> - <button class="btn btn-outline-secondary mb-3" type="button" data-toggle="collapse" data-target="#introContent" aria-expanded="false" aria-controls="introContent" id="toggleIntro"> - <span class="mr-1">Read More</span> - <i class="fas fa-chevron-down ml-2"></i> - </button> - <!-- Collapsible Content for "Learn More" --> - <div class="collapse" id="introContent"> - <p class="text-muted"> - The Extremum Dashboard supports researchers and data enthusiasts by providing tools for dataset selection, advanced visualization, and statistical analysis. +<div class="about-project-section py-5 position-relative"> + <div class="container" style="padding-top:250px;"> + <!-- Main Section with Split Layout --> + <div class="about-project-section py-5 position-relative fade-in"> + <div class="container"> + <div class="row align-items-center"> + <!-- Content Section --> + <div class="col-lg-7"> + <h2 class="h4 text-dark mb-3 fade-in">About the Extremum Dashboard</h2> + <p class="text-muted fade-in"> + The <strong>Extremum Dashboard</strong>, developed by <strong>Stockholm University</strong>, is part of the + <a href="https://datascience.dsv.su.se/projects/extremum.html" target="_blank" class="text-primary">EXTREMUM project</a>. It combines advanced AI with ethical practices to improve healthcare outcomes. + </p> + <ul class="list-unstyled mt-4 fade-in"> + <li class="mb-3"> + <i class="fas fa-layer-group text-primary mr-2"></i> + <strong>Unified Data Representation:</strong> Seamlessly integrate complex medical datasets. + </li> + <li class="mb-3"> + <i class="fas fa-brain text-success mr-2"></i> + <strong>Explainable Predictive Models:</strong> Build AI solutions that are interpretable and reliable. + </li> + <li class="mb-3"> + <i class="fas fa-balance-scale text-warning mr-2"></i> + <strong>Ethical Compliance:</strong> Ensure AI aligns with ethical and legal standards. + </li> + </ul> + <button class="btn btn-primary rounded-pill px-4 mt-4 fade-in" type="button" data-toggle="collapse" data-target="#extremumDetails" aria-expanded="false" aria-controls="extremumDetails"> + Learn More <i class="fas fa-chevron-down ml-2"></i> + </button> + </div> + + <!-- Image Section --> + <div class="col-lg-5 text-center"> + <img src="https://datascience.dsv.su.se/img/logo/dsgroup.png" + alt="EXTREMUM Visualization" + class="img-fluid rounded shadow-lg fade-in" + loading="lazy" + style="max-height: 250px;"> + </div> + </div> + </div> + </div> + + + <div class="collapse mt-4 fade-in" id="extremumDetails"> + <div class="text-muted mx-auto" style="max-width: 700px;"> + <h4 class="h5 text-dark text-center mb-3">About the EXTREMUM Project</h4> + <p> + The <strong>EXTREMUM Project</strong> focuses on developing an explainable machine learning platform to analyze complex medical data. It addresses two key healthcare areas: </p> - <ul class="list-unstyled text-left mx-auto" style="max-width: 600px;"> - <li><i class="fas fa-database text-primary mr-2"></i><strong>Dataset Selection:</strong> Choose from preloaded datasets or upload your own for tailored analysis.</li> - <li><i class="fas fa-chart-line text-success mr-2"></i><strong>Timeseries Analysis:</strong> Explore timeseries datasets with customizable parameters and statistical insights.</li> - <li><i class="fas fa-eye text-info mr-2"></i><strong>Interactive Visualization:</strong> View tabular data and statistical insights with interactive graphs.</li> + <ul class="list-unstyled text-center my-4"> + <li class="mb-3"> + <i class="fas fa-heartbeat text-danger"></i> + <span class="ml-2">Adverse Drug Event Detection</span> + </li> + <li> + <i class="fas fa-stethoscope text-info"></i> + <span class="ml-2">Cardiovascular Disease Detection</span> + </li> </ul> - <p class="text-muted">This platform turns data into actionable insights effortlessly!</p> + <p> + This project integrates medical data sources, builds interpretable predictive models, and ensures ethical integrity in machine learning. + </p> + <p class="text-center"> + <a href="https://datascience.dsv.su.se/projects/extremum.html" target="_blank" class="btn btn-outline-primary rounded-pill"> + Learn More + </a> + </p> + </div> + </div> + + + <!-- Feature Carousel Section --> + <div class="feature-carousel py-5 bg-light mt-5 fade-in"> + <div class="container"> + <h3 class="h4 text-dark text-center mb-4">Key Innovations in EXTREMUM</h3> + <p class="text-muted text-center mx-auto mb-5" style="max-width: 700px;"> + Discover the powerful tools and methodologies developed under the EXTREMUM project, designed to revolutionize explainable AI for healthcare applications. + </p> + <div id="carouselFeatures" class="carousel slide" data-ride="carousel" data-interval="5000" data-pause="hover"> + <ol class="carousel-indicators"> + <li data-target="#carouselFeatures" data-slide-to="0" class="active" tabindex="0" aria-label="Feature 1"></li> + <li data-target="#carouselFeatures" data-slide-to="1" tabindex="0" aria-label="Feature 2"></li> + </ol> + + <div class="carousel-inner"> + <div class="carousel-item active"> + <div class="feature-card p-5 shadow rounded text-center"> + <i class="fas fa-wave-square text-info fa-3x mb-4"></i> + <h5 class="text-dark">Wildboar</h5> + <p class="text-muted"> + Created by <strong>Isak Samsten</strong>, Wildboar is a Python library for temporal machine learning, offering tools for classification, regression, and explainability. + </p> + <a href="https://github.com/wildboar-foundation/wildboar" target="_blank" class="btn btn-primary rounded-pill px-4 py-2 mt-3"> + Learn More <i class="fas fa-external-link-alt ml-2"></i> + </a> + </div> + </div> + + <div class="carousel-item"> + <div class="feature-card p-5 shadow rounded text-center"> + <i class="fas fa-snowflake text-primary fa-3x mb-4"></i> + <h5 class="text-dark">Glacier</h5> + <p class="text-muted"> + Developed by <strong>Zhendong Wang</strong>, Glacier generates counterfactual explanations for time series classification, ensuring realistic and interpretable results. + </p> + <a href="https://github.com/zhendong3wang/learning-time-series-counterfactuals" target="_blank" class="btn btn-primary rounded-pill px-4 py-2 mt-3"> + Learn More <i class="fas fa-external-link-alt ml-2"></i> + </a> + </div> + </div> + </div> + + <a class="carousel-control-prev" href="#carouselFeatures" role="button" data-slide="prev"> + <span class="carousel-control-prev-icon bg-dark rounded-circle p-2" aria-hidden="true"></span> + <span class="sr-only">Previous</span> + </a> + <a class="carousel-control-next" href="#carouselFeatures" role="button" data-slide="next"> + <span class="carousel-control-next-icon bg-dark rounded-circle p-2" aria-hidden="true"></span> + <span class="sr-only">Next</span> + </a> + </div> </div> </div> </div> +</div> - <!-- New Key Features or Benefits Section --> - <div class="key-features-section py-5 text-center bg-white"> + +<div> + <!-- Call to Action Section --> + <div class="separator-section py-5 text-center bg-light"> <div class="container"> - <h2 class="h4 text-gray-800 mb-4">Why Use Extremum Dashboard?</h2> - <div class="row"> - <div class="col-md-4 mb-4"> - <div class="feature-card border-0 p-3 shadow-sm animate-card"> - <i class="fas fa-bolt text-primary fa-2x mb-3"></i> - <h5 class="text-dark">Fast & Efficient</h5> - <p class="text-muted">Quickly analyze large datasets with optimized performance and get insights in seconds.</p> - </div> - </div> - <div class="col-md-4 mb-4"> - <div class="feature-card border-0 p-3 shadow-sm animate-card"> - <i class="fas fa-chart-pie text-success fa-2x mb-3"></i> - <h5 class="text-dark">Comprehensive Visualizations</h5> - <p class="text-muted">Access a wide range of visualizations to understand your data better.</p> - </div> - </div> - <div class="col-md-4 mb-4"> - <div class="feature-card border-0 p-3 shadow-sm animate-card"> - <i class="fas fa-lock text-info fa-2x mb-3"></i> - <h5 class="text-dark">Secure & Reliable</h5> - <p class="text-muted">Your data is safe with us, with top-notch security measures in place.</p> - </div> - </div> - </div> + <h3 class="h5 text-dark mb-4 fade-in">Ready to start your journey?</h3> + <button class="btn btn-outline-primary fade-in" onclick="document.getElementById('dataset_selection').scrollIntoView({behavior: 'smooth'})"> + Explore Datasets <i class="fas fa-arrow-down ml-2"></i> + </button> </div> </div> - - <!-- Separator Section with Interactive Scroll Button --> - <div class="separator-section py-4 text-center"> - <hr class="w-50 mx-auto mb-4"> - <button class="btn btn-primary btn-lg" onclick="document.getElementById('dataset_selection').scrollIntoView({behavior: 'smooth'})"> - Start Exploring Datasets - <i class="fas fa-arrow-down ml-2"></i> - </button> - </div> - <div class="cool-separator my-5"> - <hr> - </div> - </div> <!-- Page Heading --> -<!-- Combined Heading and Button Group for Dataset Selection --> -<div class="text-center mb-5" style="padding-top:250px;"> - <h2 id="dataset_selection" class="h4 mb-4 text-dark">Choose a Dataset</h2> - <!-- Dataset Selection Button Group --> - <div class="row justify-content-center"> +<!-- Combined Heading and Button Group for Dataset Selection --> +<style> + /* Section Styling */ + .dataset-section { + padding: 100px 20px; + background-color: #f8f9fa; + text-align: center; + } + + .dataset-section h2 { + font-size: 1.5rem; + font-weight: 600; + color: #333; + margin-bottom: 1rem; + } + + .dataset-section p { + font-size: 0.95rem; + color: #666; + margin-bottom: 2rem; + max-width: 600px; + margin: 0 auto; + line-height: 1.5; + } + + /* Button Styling */ + .btn-dataset { + font-size: 1rem; + font-weight: 500; + border: 1px solid #ccc; + color: #333; + background-color: white; + border-radius: 5px; + padding: 12px 20px; + margin: 10px; + transition: all 0.2s ease; + width: 200px; + } + + .btn-dataset:hover { + border-color: #007bff; + color: #007bff; + } + + .btn-dataset.active { + background-color: #007bff; + color: white; + border-color: #007bff; + } + + /* Responsive Alignment */ + .dataset-section .row { + justify-content: center; + gap: 1rem; + } + + @media (max-width: 768px) { + .btn-dataset { + width: 180px; + padding: 10px 15px; + } + } +</style> + +<div class="dataset-section" style="padding-top:300px;"> + <!-- Title --> + <h2 id="dataset_selection">Choose Your Dataset</h2> + <p> + Select a dataset to visualize its graphs and perform advanced operations like using pre-trained models + or computing counterfactuals. Your choice will be used throughout the session. + </p> + + <!-- Dataset Selection Buttons --> + <div class="row"> {% csrf_token %} - + <!-- Breast Cancer Dataset --> - <div class="col-auto"> - <button type="button" class="btn btn-dataset px-4 py-2 mb-2 {% if df_name == 'breast-cancer' %}active{% endif %}" id="breast-cancer"> - <i class="fas fa-dna"></i> Breast Cancer - </button> - </div> + <button type="button" class="btn btn-dataset {% if df_name == 'breast-cancer' %}active{% endif %}" id="breast-cancer"> + Breast Cancer + </button> <!-- Stroke Dataset --> - <div class="col-auto"> - <button type="button" class="btn btn-dataset px-4 py-2 mb-2 {% if df_name == 'stroke' %}active{% endif %}" id="stroke"> - <i class="fas fa-heartbeat"></i> Stroke - </button> - </div> + <button type="button" class="btn btn-dataset {% if df_name == 'stroke' %}active{% endif %}" id="stroke"> + Stroke + </button> <!-- Timeseries Dataset --> - <div class="col-auto"> - <button type="button" class="btn btn-dataset px-4 py-2 mb-2 {% if dataset_type == 'timeseries' %}active{% endif %}" id="timeseries"> - <i class="fas fa-chart-line"></i> Timeseries - </button> - </div> + <button type="button" class="btn btn-dataset {% if dataset_type == 'timeseries' %}active{% endif %}" id="timeseries"> + Timeseries + </button> - <!-- Upload Dataset --> - <div class="col-auto"> - <button type="button" class="btn btn-dataset px-4 py-2 mb-2 {% if upload %}active{% endif %}" id="upload"> - <i class="fas fa-upload"></i> Upload - </button> - </div> + <!-- Upload Dataset (Optional) --> + <!-- Uncomment if needed + <button type="button" class="btn btn-dataset {% if upload == 1 %}active{% endif %}" id="upload"> + Upload Dataset + </button> + --> </div> </div> + <!-- Upload Form Section --> -<div class="row justify-content-center"> - <div class="col-xl-5 col-lg-5" id="upload_col" {% if upload %} style="display: block;" {% else %} style="display: none;" {% endif %}> - <div class="card shadow-sm mb-4 border-0 animate-card"> - <div class="card-body"> +<!-- <div class="row justify-content-center"> + <div class="col-xl-5 col-lg-6" id="upload_col" {% if upload %} style="display: block;" {% else %} style="display: none;" {% endif %}> + <div class="card shadow-sm border-0 animate-card"> + <div class="card-header bg-primary text-muted d-flex align-items-center"> + <h6 class="mb-0">Upload Dataset</h6> + <i class="fas fa-upload ml-auto"></i> + </div> + <div class="card-body bg-light"> <div class="row"> - <!-- Left Column: Form Section --> - <div class="col-md-6"> + <div class="col-md-7 mb-4"> <form id="csv_form" method="POST" enctype="multipart/form-data"> {% csrf_token %} - <fieldset class="form-group mb-3"> + + <fieldset class="form-group mb-4"> <legend class="col-form-label small text-secondary font-weight-semibold">Data Type</legend> <div class="form-check"> <input class="form-check-input" type="radio" name="dataset_type" id="tabular" value="tabular" required> @@ -138,43 +298,81 @@ <label class="form-check-label" for="timeseries">Timeseries</label> </div> </fieldset> - - <div class="form-group"> - <label class="small text-secondary font-weight-semibold" for="doc">Upload File</label> + + <div class="form-group mb-4"> + <label class="small text-secondary font-weight-semibold" for="doc">Select File</label> <input class="form-control-file" type="file" id="doc" name="excel_file" required> + <small class="text-muted d-block mt-1">Supported format: CSV</small> </div> - - <div class="form-group"> - <input class="btn btn-primary btn-sm mt-2" type="submit" value="Upload" id="upload_btn"> + + <div class="form-group d-flex align-items-center w-100"> + <input class="btn btn-primary btn-sm mr-3" type="submit" value="Upload" id="upload_btn"> + + <div class="loader" id="cfbtn_loader" style="display: none; margin-left: 5px;"> + <i class="fas fa-spinner fa-spin"></i> + </div> + + <div id="success-message" class="alert alert-success custom-alert d-none ml-3 mb-0" role="alert"> + <i class="fas fa-check-circle"></i> + <span class="ml-2">File uploaded successfully.</span> + <button type="button" class="close ml-auto p-0" aria-label="Close" onclick="hideSuccessMessage();"> + <span aria-hidden="true">×</span> + </button> + </div> </div> </form> </div> - - <!-- Right Column: Uploaded Files Section --> - <div class="col-md-6" id="uploaded_file"> - <fieldset class="form-group mb-3"> + + <div class="col-md-5 mb-5"> + <fieldset class="form-group mb-4"> <legend class="col-form-label small text-secondary font-weight-semibold">Uploaded Files</legend> {% if uploaded_files %} - <fieldset class="form-group"> + <fieldset class="form-group" id="radio_buttons"> {% for uploaded_file in uploaded_files %} - <div class="form-check mb-1"> - <input class="form-check-input" type="radio" name="uploaded_file" id="uploaded_file_{{ forloop.counter }}" value="{{ uploaded_file }}" required> - <label class="form-check-label" for="uploaded_file_{{ forloop.counter }}">{{ uploaded_file }}</label> + <div class="form-check mb-2 d-flex align-items-center"> + <input class="form-check-input mr-2" type="radio" {% if df_name == uploaded_file %} checked {% endif %} name="uploaded_file" id="element_{{ forloop.counter }}" value="{{ uploaded_file }}" required> + <label class="form-check-label mr-auto" for="element_{{ forloop.counter }}">{{ uploaded_file }}</label> + <button type="button" class="delete-file-icon p-0 ml-2 text-muted close" data-file="{{ uploaded_file }}" data-file-value="{{uploaded_file}}" aria-label="Delete {{ uploaded_file }}"> + <span aria-hidden="true">×</span> + </button> </div> {% endfor %} </fieldset> {% else %} - <p class="small text-muted">No files uploaded.</p> + <p class="small text-muted">No files uploaded yet. Please upload a dataset to select it here.</p> {% endif %} </fieldset> </div> </div> </div> + <div class="card-footer bg-white text-center"> + <small class="text-muted">Manage your datasets effectively. Ensure data is accurate and up-to-date.</small> + </div> + </div> + </div> +</div> --> + +<!-- Minimal Delete Confirmation Modal --> +<div class="modal fade" id="deleteFileModal" tabindex="-1" role="dialog" aria-labelledby="deleteFileModalLabel" aria-hidden="true"> + <div class="modal-dialog modal-dialog-centered" role="document"> + <div class="modal-content border-0 shadow-sm"> + <div class="modal-header border-0"> + <h6 class="modal-title text-danger" id="deleteFileModalLabel">Confirm Deletion</h6> + <button type="button" class="close text-muted" data-dismiss="modal" aria-label="Close" style="font-size: 1.2rem;"> + × + </button> + </div> + <div class="modal-body text-center py-3"> + <p class="mb-1">Delete <span id="fileToDeleteName" class="font-weight-bold"></span>?</p> + <small class="text-muted">This action is permanent.</small> + </div> + <div class="modal-footer justify-content-center border-0"> + <button type="button" class="custom-btn-secondary" data-dismiss="modal">Cancel</button> + <button type="button" class="custom-btn-danger" id="confirmDeleteButton">Delete</button> + </div> </div> </div> </div> - - <!-- Timeseries Dataset Selection --> <div class="row justify-content-center"> <div class="col-lg-5" {% if dataset_type != "timeseries" %} style="display:none;" {% endif %} id="timeseries-datasets"> @@ -269,7 +467,58 @@ </div> </div> +<!-- Modal Window --> +<div class="modal fade" id="labelSelectionModal" tabindex="-1" aria-labelledby="labelSelectionModalLabel" aria-hidden="true" data-backdrop="static" data-keyboard="false"> + <div class="modal-dialog modal-dialog-centered"> + <div class="modal-content"> + <!-- Modal Header --> + <div class="modal-header"> + <h5 class="modal-title" id="labelSelectionModalLabel">Assign Positive and Negative Labels</h5> + </div> + <!-- Modal Body --> + <div class="modal-body"> + {% csrf_token %} + <p class="text-muted">Please assign one label as <strong>Positive</strong> and another as <strong>Negative</strong>.</p> + <!-- Positive Label Dropdown --> + <div class="form-group"> + <label for="positive-label" class="font-weight-semibold">Positive Label</label> + <select id="positive-label" class="form-control"> + <option value="" disabled selected>Select a positive label</option> + <!-- Options populated dynamically --> + </select> + </div> + <!-- Negative Label Dropdown --> + <div class="form-group mt-3"> + <label for="negative-label" class="font-weight-semibold">Negative Label</label> + <select id="negative-label" class="form-control"> + <option value="" disabled selected>Select a negative label</option> + <!-- Options populated dynamically --> + </select> + </div> + <!-- Error Message --> + <div id="selection-error" class="alert alert-danger d-none mt-3"> + <i class="fas fa-exclamation-triangle"></i> Labels must be different. Please select one positive and one negative label. + </div> + <!-- Loader --> + <div id="loader" class="d-none text-center mt-3"> + <div class="spinner-border text-primary" role="status"> + <span class="sr-only">Loading...</span> + </div> + <p>Saving your choices...</p> + </div> + </div> + + <!-- Modal Footer --> + <div class="modal-footer"> + <button type="button" class="btn btn-primary" id="save-label-choices">Save Choices</button> + </div> + </div> + </div> +</div> + + {% if dataset_type == "tabular" and df_name and data_to_display %} + <div class="row mb-4"> <!-- Data Card with Original ID --> <div class="col-lg-6" id="df_cached"> @@ -317,7 +566,20 @@ </div> </div> </div> + + <div class="row mt-3" id="new_or_load_cached"> + <div class="col d-flex justify-content-center"> + <div class="text-center mt-4"> + <button id="viewModelsButton" class="btn btn-view-models"> + View Pre-trained Models + <i class="fas fa-arrow-right ml-2"></i> <!-- Font Awesome icon for added appeal --> + </button> + </div> + </div> + </div> + {% elif dataset_type == "timeseries" %} + <div class="row mb-4"> <!-- Confidence Interval Card with Original ID --> <div class="col-lg-6" id="ts_confidence_cached"> @@ -343,20 +605,27 @@ </div> </div> </div> -{% endif %} - -<!-- Loader --> -<div class="row"> - <div class="col d-flex justify-content-center"> - <span class="loader" id="loader" style="display: none;"></span> + <div class="row mt-3" id="new_or_load_cached"> + <div class="col d-flex justify-content-center"> + <div class="text-center mt-4"> + <button id="viewPreTrainedButton" class="btn btn-view-models"> + View Pre-trained Models + <i class="fas fa-arrow-right ml-2"></i> <!-- Font Awesome icon for added appeal --> + </button> + </div> + </div> </div> -</div> -<div class="row mt-3" id="new_or_load"> +{% endif %} +<!-- Loader --> +<div class="d-flex justify-content-center"> + <span class="loader" id="loader_ds" style="display: none;"></span> +</div> +<div class="row mt-3" id="new_or_load" style="display:none;"> <div class="col d-flex justify-content-center"> <div class="text-center mt-4"> - <button id="viewModelsButton" class="btn btn-view-models"> + <button id="viewPreTrainedButton" class="btn btn-view-models"> View Pre-trained Models <i class="fas fa-arrow-right ml-2"></i> <!-- Font Awesome icon for added appeal --> </button> @@ -365,9 +634,6 @@ </div> <!-- JavaScript --> -<script type="module" src="{% static 'js/radio_dataset.js' %}"></script> -<script type="module" src="{% static 'js/selection_change.js' %}"></script> -<script type="module" src="{% static 'js/radio_timeseries_dataset.js' %}"></script> -<script type="module" src="{% static 'js/radio_uploaded_dataset.js' %}"></script> +<script type="module" src="{% static 'js/home.js' %}"></script> {% endblock content%} diff --git a/base/templates/base/train.html b/base/templates/base/train.html index f3f2d90cf..60b936371 100755 --- a/base/templates/base/train.html +++ b/base/templates/base/train.html @@ -21,7 +21,7 @@ <div class="row"> {% if df_name %} - <div class="col-xl-10 col-lg-10"> + <div class="col-xl-12 col-lg-12"> <div class="card shadow-sm mb-4 border-0 animate-card"> <div class="card-header bg-light text-dark py-3 d-flex flex-row align-items-center justify-content-between"> <!-- Softer background and font color --> <h6 class="m-0">DataFrame Summary Information</h6> <!-- Reduced emphasis --> @@ -33,7 +33,6 @@ <div class="card-body"> <div class="row"> - <div class="col-xl-3 col-lg-3"> <div class="py-3 d-flex flex-row align-items-center justify-content-between"> <h6 class="m-0 text-muted">Classifier</h6> <!-- Text-muted for subtlety --> @@ -63,7 +62,7 @@ <!-- Test set ratio slider --> <div class="row" style="padding-top:20px;"> - <div class="col-xl-6 col-lg-6" id="ratio" {% if dataset_type == "timeseries" %} style="display:none;" {% endif %}> + <div class="col-xl-4 col-lg-4" id="ratio" {% if dataset_type == "timeseries" %} style="display:none;" {% endif %}> <div class="py-3 d-flex flex-row align-items-center justify-content-between"> <h6 class="m-0 text-muted">Test Set Ratio</h6> </div> @@ -71,7 +70,7 @@ <output id="value"></output> </div> {% if dataset_type == "tabular" %} - <div class="col-xl-4 col-lg-4" id="class_label" style="display:none;"> + <div class="col-xl-4 col-lg-4" id="class_label"> <div class="py-3 d-flex flex-row align-items-center justify-content-between"> <h6 class="m-0 text-muted">Class Label</h6> </div> @@ -94,17 +93,7 @@ </div> {% endif %} </div> - <!-- Action Button Section --> - <div class="row justify-content-md-center" style="padding-top:30px;"> - {% csrf_token %} - <button class="btn btn-outline-primary train_test" role="button" disabled>Go!</button> <!-- Outlined button for minimalistic style --> - <div id="loader_train" style="display: none;"> - <div class="col-sm d-flex justify-content-center"> - <span class="loader"></span> - </div> - </div> - </div> </div> </div> </div> @@ -121,6 +110,142 @@ </div> {% endif %} </div> + <div class="d-flex justify-content-center mt-4"> + <div id="error_message_new_x_2" class="alert alert-danger alert-dismissible text-center" style="display: none; width: 100%; max-width: 400px;" role="alert"> + <i class="fas fa-exclamation-triangle"></i> + <span id="error_message_text">Please correct errors before proceeding.</span> + <button type="button" class="close" aria-label="Close" onclick="$('#error_message_new_x_2').hide();"> + <span aria-hidden="true">×</span> + </button> + </div> + </div> + + <!-- Modal Window --> + <div class="modal fade" id="modelAnalysisModal" tabindex="-1" aria-labelledby="modelAnalysisModalLabel" aria-hidden="true" data-backdrop="static" data-keyboard="false"> + <div class="modal-dialog modal-dialog-centered modal-lg"> + <div class="modal-content"> + <!-- Modal Header --> + <div class="modal-header bg-light text-dark"> + <h5 class="modal-title" id="modelAnalysisModalLabel">Model Analysis and Decision</h5> + </div> + <!-- Modal Body --> + <div class="modal-body"> + <!-- Prompt Message --> + <div class="alert alert-info"> + <i class="fas fa-info-circle mr-2"></i> + After training your model/classifier, you should now decide whether to <strong>keep</strong> it or <strong>discard</strong> it based on its performance metrics and visualizations below. + </div> + + <!-- Tabs Navigation --> + <ul class="nav nav-tabs" id="analysisTabs" role="tablist"> + <li class="nav-item"> + <a class="nav-link" id="classification-tab" data-toggle="tab" href="#classification" role="tab" aria-controls="classification" aria-selected="false"> + <i class="fas fa-chart-line mr-2"></i>Classification Report + </a> + </li> + <li class="nav-item"> + <a class="nav-link" id="details-tab" data-toggle="tab" href="#details" role="tab" aria-controls="details" aria-selected="false"> + <i class="fas fa-info-circle mr-2"></i>Classifier Details + </a> + </li> + <li class="nav-item"> + <a class="nav-link active" id="pca-tab" data-toggle="tab" href="#pca" role="tab" aria-controls="pca" aria-selected="true"> + <i class="fas fa-project-diagram mr-2"></i>PCA + </a> + </li> + <li class="nav-item" id="feature-tab-nav" style="display: none;"> + <a class="nav-link" id="fi-tab" data-toggle="tab" href="#feature" role="tab" aria-controls="feature" aria-selected="false"> + <i class="fas fa-th mr-2"></i>Feature Importance + </a> + </li> + <li class="nav-item" id="tsne-tab-nav" style="display: none;"> + <a class="nav-link" id="tsne-tab" data-toggle="tab" href="#tsne" role="tab" aria-controls="tsne" aria-selected="true"> + <i class="fas fa-clone mr-2"></i>TSNE + </a> + </li> + </ul> + + <!-- Tabs Content --> + <div class="tab-content mt-3" id="analysisTabsContent"> + <!-- Classification Report Tab --> + <div class="tab-pane fade" id="classification" role="tabpanel" aria-labelledby="classification-tab"> + <div id="classification_report" class="p-3"></div> + </div> + + <!-- Classifier Details Tab --> + <div class="tab-pane fade" id="details" role="tabpanel" aria-labelledby="details-tab"> + <div id="details_content" class="p-3 overflow-auto"></div> + </div> + + <!-- PCA Tab --> + <div class="tab-pane fade show active" id="pca" role="tabpanel" aria-labelledby="pca-tab"> + <div id="pca_container" class="p-3"></div> + </div> + + <!-- Feature Importance Tab --> + <div class="tab-pane fade" id="feature" role="tabpanel" aria-labelledby="fi-tab"> + <div id="fi_container" class="p-3"></div> + </div> + + <!-- TSNE Tab --> + <div class="tab-pane fade" id="tsne" role="tabpanel" aria-labelledby="tsne-tab"> + <div id="tsne_container" class="p-3"></div> + </div> + </div> + </div> + + {% csrf_token %} + + <!-- Modal Footer --> + <div class="modal-footer"> + <button type="button" class="btn btn-success" id="save-model"> + <i class="fas fa-save mr-2"></i>Save Model + </button> + <button type="button" class="btn btn-danger" id="discard-model"> + <i class="fas fa-trash-alt mr-2"></i>Discard Model + </button> + </div> + </div> + </div> + </div> + + <div class="row justify-content-center align-items-center my-4"> + <!-- CSRF Token --> + {% csrf_token %} + + <!-- Button --> + <div class="col-auto" id="train_test_btn"> + <button class="btn btn-primary train_test align-items-center px-4 py-2 shadow-sm"> + <i class="fas fa-play mr-2"></i> + Start Training + </button> + </div> + + <!-- Loader --> + <div id="loader_train" class="col-auto d-none"> + <div class="d-flex align-items-center text-muted"> + <div class="spinner-border spinner-border-sm text-primary mr-2" role="status"></div> + <span>Processing...</span> + </div> + </div> + </div> + + <div class="row mt-3" id="new_or_load"> + <div class="col d-flex justify-content-center"> + <div class="text-center mt-4 d-flex justify-content-center"> + <!-- Back to Dataset Selection Button --> + <button id="backToDatasetButton" class="btn btn-view-models mr-3"> + <i class="fas fa-arrow-left mr-2"></i> Back to Dataset Selection + </button> + + <!-- View Counterfactuals Button --> + <button id="viewPreTrainedButton" class="btn btn-view-models"> + View Pre-Trained Models <i class="fas fa-arrow-right ml-2"></i> + </button> + </div> + </div> + </div> + </div> </div> <script src="{% static 'js/slider.js' %}"></script> diff --git a/base/views.py b/base/views.py index 39cb317c4..19ac3e02a 100755 --- a/base/views.py +++ b/base/views.py @@ -1,177 +1,41 @@ -from django.shortcuts import render, HttpResponse, HttpResponseRedirect -from django.shortcuts import redirect, render +from django.shortcuts import render +from django.shortcuts import render import base.pipeline as pipeline -import pickle, os -import pandas as pd -import json -from sklearn.preprocessing import LabelEncoder -import joblib from dict_and_html import * -from django.conf import settings from . import methods from .methods import PIPELINE_PATH -import math -import numpy as np -from django.core.files.storage import FileSystemStorage import random -from collections import defaultdict -from .glacier.src.glacier_compute_counterfactuals import gc_compute_counterfactuals import base.pipeline as pipeline -import concurrent.futures +import os +from . handlers import home_handler, counterfactuals_handler, charts_handler, train_handler def home(request): # ajax request condition if request.headers.get("X-Requested-With") == "XMLHttpRequest": - return ajax_requests(request.POST.get("action"), request) + return home_handler(request.POST.get("action"), request) - if request.method == "POST" and request.FILES["excel_file"]: - uploaded_file = request.FILES["excel_file"] # Get the file from request.FILES - dataset_type = request.POST.get("dataset_type") + if "upload" in request.session: + upload = request.session.get("upload") + else: + upload = 0 - # action to add dataset when from radio button click - # add name of used dataframe in session for future use - request.session["df_name"] = "upload" - name = uploaded_file.name - - # Split the name and extension - base_name, extension = os.path.splitext(name) - request.session["df_name_upload_base_name"] = base_name - request.session["df_name_upload_extension"] = extension - - df_name = base_name - - df_name_path = os.path.join( - PIPELINE_PATH + f"{base_name}", - ) - - if not os.path.exists(df_name_path): - os.makedirs(df_name_path) - - fs = FileSystemStorage() # FileSystemStorage to save the file - - # Save the file with the new filename - fs = FileSystemStorage(location=df_name_path) - filename = fs.save(uploaded_file.name, uploaded_file) # Save file - - request.session["excel_file_name"] = df_name_path - - excel_file_name_path = os.path.join(PIPELINE_PATH + f"{base_name}" + "/" + name) - - df = methods.get_dataframe(excel_file_name_path) - - ## update the datasets_types json file - datasets_types_pipeline_json_path = os.path.join( - PIPELINE_PATH + "/dataset_types_pipeline.json" - ) - jsonFile = pipeline.pipeline_json(datasets_types_pipeline_json_path) - - # with open(datasets_types_pipeline_json_path, "r") as jsonFile: - # datasets_types_pipeline_json = pipeline.load( - # jsonFile - # ) # data becomes a dictionary - - jsonFile.append_pipeline_json({df_name: [dataset_type, "uploaded"]}) - dataset_type = jsonFile.read_pipline_json([df_name])[0] - uploaded_files = jsonFile.get_keys_with_specific_value("uploaded") - - # datasets_types_pipeline_json[df_name] = dataset_type - # with open(datasets_types_pipeline_json_path, "w") as file: - # pipeline.dump( - # datasets_types_pipeline_json, file, indent=4 - # ) # Write with pretty print (indent=4) - - if df.columns.str.contains(" ").any(): - df.columns = df.columns.str.replace(" ", "_") - # if columns contain space - os.remove(excel_file_name_path) - df.to_csv(excel_file_name_path, index=None) - df = methods.get_dataframe(excel_file_name_path) - - if "id" in df.columns: - df.drop(["id"], axis=1, inplace=True) - df.to_csv(excel_file_name_path, index=False) - - if dataset_type == "tabular": - # tabular datasets - features = df.columns - feature1 = df.columns[3] - feature2 = df.columns[2] - - labels = list(df.select_dtypes(include=["object", "category"]).columns) - # Find binary columns (columns with only two unique values, including numerics) - binary_columns = [col for col in df.columns if df[col].nunique() == 2] - - # Combine categorical and binary columns into one list - labels = list(set(labels + binary_columns)) - - label = random.choice(labels) - fig = methods.stats( - excel_file_name_path, - dataset_type, - None, - None, - feature1, - feature2, - label, - df_name, - ) - - # tabular dataset - request.session["data_to_display"] = df[:10].to_html() - request.session["features"] = list(features) - request.session["feature1"] = feature1 - request.session["feature2"] = feature2 - request.session["labels"] = list(labels) - request.session["curlabel"] = label - request.session["fig"] = fig - - context = { - "dataset_type": dataset_type, - "data_to_display": df[:10].to_html(), - "fig": fig, - "features": list(features), # error if not a list - "feature1": feature1, - "feature2": feature2, - "labels": list(labels), - "curlabel": label, - "df_name": request.session["df_name"], - } - elif dataset_type == "timeseries": - fig, fig1 = methods.stats(excel_file_name_path, dataset_type) - request.session["fig"] = fig - request.session["fig1"] = fig1 - context = { - "dataset_type": dataset_type, - "df_name": df_name, - "fig": fig, - "fig1": fig1, - } - - print("Uploaded files: ", uploaded_files) - context.update({"uploaded_files": uploaded_files}) - request.session["context"] = context - - return render(request, "base/home.html", context) - - upload = 0 ## get the type of the active dataset - datasets_types_pipeline_json_path = os.path.join( - PIPELINE_PATH + "/dataset_types_pipeline.json" + datasets_types_PipelineJSON_path = os.path.join( + PIPELINE_PATH + "dataset_types_pipeline.json" ) - datasets_types_pipeline_json = pipeline.pipeline_json( - datasets_types_pipeline_json_path + datasets_types_PipelineJSON = pipeline.PipelineJSON( + datasets_types_PipelineJSON_path ) - uploaded_files = datasets_types_pipeline_json.get_keys_with_specific_value( + + uploaded_files = datasets_types_PipelineJSON.get_keys_with_value( "uploaded" ) - - if "df_name" in request.session: + + if "df_name" in request.session and request.session["df_name"] != "upload": df_name = request.session.get("df_name") - if df_name == "upload": - upload = 1 - df_name = request.session.get("df_name_upload_base_name") - dataset_type = datasets_types_pipeline_json.read_pipline_json([df_name]) + + dataset_type = datasets_types_PipelineJSON.read_from_json([df_name]) if type(dataset_type) is list: dataset_type = dataset_type[0] @@ -200,13 +64,14 @@ def home(request): } elif dataset_type == "timeseries": context = { + "upload": upload, "df_name": df_name, "dataset_type": dataset_type, "fig": request.session.get("fig"), "fig1": request.session.get("fig1"), } - # else: - # context = {} + else: + context = {} else: context = {} @@ -217,7 +82,7 @@ def home(request): def counterfactuals(request): # ajax request condition if request.headers.get("X-Requested-With") == "XMLHttpRequest": - return ajax_requests(request.POST.get("action", None), request) + return counterfactuals_handler(request.POST.get("action", None), request) available_pretrained_models_info = [] if "df_name" in request.session: df_name = request.session.get("df_name") @@ -239,16 +104,16 @@ def counterfactuals(request): # if it does not exist, obviously there are no pre trained # models available_pretrained_models = [] - jsonFile = pipeline.pipeline_json(json_path) - if not jsonFile.check_key_pipeline_json("classifier"): + jsonFile = pipeline.PipelineJSON(json_path) + if not jsonFile.key_exists("classifier"): # pre trained models do not exist available_pretrained_models = [] else: # if it exists # check the section of "classifiers" # folder path - if jsonFile.check_key_pipeline_json("classifier"): - available_pretrained_models = jsonFile.read_pipline_json( + if jsonFile.key_exists("classifier"): + available_pretrained_models = jsonFile.read_from_json( ["classifier"] ).keys() available_pretrained_models_info = ( @@ -258,21 +123,21 @@ def counterfactuals(request): ) ## get the type of the active dataset - datasets_types_pipeline_json_path = os.path.join( + datasets_types_PipelineJSON_path = os.path.join( PIPELINE_PATH + "/dataset_types_pipeline.json" ) - datasets_types_pipeline_json = pipeline.pipeline_json( - datasets_types_pipeline_json_path + datasets_types_PipelineJSON = pipeline.PipelineJSON( + datasets_types_PipelineJSON_path ) - dataset_type = datasets_types_pipeline_json.read_pipline_json([df_name]) + dataset_type = datasets_types_PipelineJSON.read_from_json([df_name]) if type(dataset_type) is list: dataset_type = dataset_type[0] - + # model_name_path = os.path.join( # PIPELINE_PATH + f"{df_name}" + "/trained_models/" + pre_trained_model_name # ) # tsne = joblib.load(model_name_path + "/tsne.sav") - + if dataset_type == "tabular": context = { @@ -297,12 +162,12 @@ def counterfactuals(request): target_label_info = zip(target_label_value, target_label_text) available_pre_computed_counterfactuals = [] - if jsonFile.check_key_pipeline_json("classifier"): - if jsonFile.check_key_pipeline_json("glacier"): - if jsonFile.check_key_pipeline_json("experiments"): + if jsonFile.key_exists("classifier"): + if jsonFile.key_exists("glacier"): + if jsonFile.key_exists("experiments"): # applies only to glacier # there are pre computed counterfactuals - experiments_dict = jsonFile.read_pipline_json( + experiments_dict = jsonFile.read_from_json( ["classifier", "glacier", "experiments"] ) list_of_experiment_keys = list(experiments_dict.keys()) @@ -314,6 +179,8 @@ def counterfactuals(request): else: available_pre_computed_counterfactuals = None + + print(available_pre_computed_counterfactuals) context = { "df_name": df_name, @@ -331,7 +198,7 @@ def counterfactuals(request): def train(request): # ajax request condition if request.headers.get("X-Requested-With") == "XMLHttpRequest": - return ajax_requests(request.POST.get("action"), request) + return train_handler(request.POST.get("action"), request) df_name = request.session.get("df_name") @@ -339,13 +206,13 @@ def train(request): df_name = request.session.get("df_name_upload_base_name") ## get the type of the active dataset - datasets_types_pipeline_json_path = os.path.join( + datasets_types_PipelineJSON_path = os.path.join( PIPELINE_PATH + "/dataset_types_pipeline.json" ) - datasets_types_pipeline_json = pipeline.pipeline_json( - datasets_types_pipeline_json_path + datasets_types_PipelineJSON = pipeline.PipelineJSON( + datasets_types_PipelineJSON_path ) - dataset_type = datasets_types_pipeline_json.read_pipline_json([df_name]) + dataset_type = datasets_types_PipelineJSON.read_from_json([df_name]) if type(dataset_type) is list: dataset_type = dataset_type[0] @@ -400,6 +267,7 @@ def train(request): excel_file_name_path = os.path.join( PIPELINE_PATH + f"{df_name}" + "/" + df_name + ".csv" ) + df = methods.get_dataframe(excel_file_name_path) df.columns = df.columns.str.replace(" ", "_") preprocessing_value = ["std", "denoise", "imp"] @@ -408,6 +276,7 @@ def train(request): "Denoising", "Imputations", ] + preprocessing_info = zip(preprocessing_value, preprocessing_text) classifiers_value = ["wildboar_knn", "wildboar_rsf", "glacier"] classifiers_text = [ @@ -415,6 +284,7 @@ def train(request): "Wildboar-Random Shapelet Forest (classifier train)", "Glacier 1DCNN", ] + classifiers_info = zip(classifiers_value, classifiers_text) context = { "dataset_type": dataset_type, @@ -430,10 +300,11 @@ def train(request): def charts(request): # ajax request condition if request.headers.get("X-Requested-With") == "XMLHttpRequest": - return ajax_requests(request.POST.get("action"), request) + return charts_handler(request.POST.get("action"), request) if "df_name" in request.session: - df_name = request.session.get("df_name") + df_name = request.session["df_name"] + if df_name == "upload": df_name = request.session.get("df_name_upload_base_name") @@ -441,11 +312,11 @@ def charts(request): json_path = os.path.join(PIPELINE_PATH + f"{df_name}" + "/pipeline.json") if os.path.exists(json_path): - jsonFile = pipeline.pipeline_json(json_path) + jsonFile = pipeline.PipelineJSON(json_path) # if it does not exist, obviously there are no pre trained # models - if not jsonFile.check_key_pipeline_json("classifier"): + if not jsonFile.key_exists("classifier"): # pre trained models do not exist # check if dataset directory exists df_dir = os.path.join(PIPELINE_PATH + f"{df_name}") @@ -460,7 +331,7 @@ def charts(request): # if it exists # check the section of "classifiers" # folder path - available_pretrained_models = jsonFile.read_pipline_json( + available_pretrained_models = jsonFile.read_from_json( ["classifier"] ).keys() @@ -479,1283 +350,3 @@ def charts(request): context = {} return render(request, "base/charts.html", context) - - -## AJAX REQUSTS HANDLER -def ajax_requests(action, request): - if action == "click_graph": - # get df used name - df_name = request.session.get("df_name") - df_name = request.session.get("df_name") - if df_name == "upload": - df_name = request.session.get("df_name_upload_base_name") - # get model_name - model_name = request.POST.get("model_name") - - # preprocessed_path - excel_file_name_preprocessed_path = os.path.join( - PIPELINE_PATH + f"{df_name}" + "/" + df_name + "_preprocessed" + ".csv" - ) - - excel_file_name_path = os.path.join( - PIPELINE_PATH + f"{df_name}" + "/" + df_name + ".csv" - ) - - model_name_path = os.path.join( - PIPELINE_PATH + f"{df_name}" + "/trained_models/" + model_name - ) - - # pipeline path - json_path = os.path.join(PIPELINE_PATH, f"{df_name}" + "/pipeline.json") - - # load pipeline data - # jsonFile = open(json_path, "r") - # pipeline_data = pipeline_json.load(jsonFile) # data becomes a dictionary - # class_label = pipeline_data["classifier"][model_name]["class_label"] - jsonFile = pipeline.pipeline_json(json_path) - class_label = jsonFile.read_pipline_json( - ["classifier", model_name, "class_label"] - ) - - df = pd.read_csv(excel_file_name_path) - - # Load your saved feature importance from a .sav file - feature_importance_df = pd.read_csv( - model_name_path + "/feature_importance_df.csv" - ) - # sorted_df = feature_importance_df.sort_values(by="importance", ascending=False) - - # x and y coordinates of the clicked point in tsne - x_coord = request.POST["x"] - y_coord = request.POST["y"] - - # tsne_projections - tsne_projections_path = os.path.join( - PIPELINE_PATH - + f"{df_name}/" - + f"trained_models/{model_name}" - + "/tsne_projections.json", - ) - - # tsne projections of all points (saved during generation of tsne) - projections = pd.read_json(tsne_projections_path) - projections = projections.values.tolist() - - # projections array is a list of pairs with the (x, y) - # [ [], [], [] ... ] - # coordinates for a point in tsne. These are actual absolute - # coordinates and not SVG. - # find the pair of the projection with x and y coordinates matching that of - # clicked point coordinates - for clicked_id, item in enumerate(projections): - if math.isclose(item[0], float(x_coord)) and math.isclose( - item[1], float(y_coord) - ): - break - - # save clicked point projections - request.session["clicked_point"] = item - # get clicked point row - row = df.iloc[[int(clicked_id)]] - request.session["cfrow_id"] = clicked_id - request.session["cfrow_og"] = row.to_html() - context = { - "row": row.to_html(index=False), - "feature_importance_dict": feature_importance_df.to_dict(orient="records"), - } - - elif action == "counterfactual_select": - - # if <select> element is used, and a specific counterfactual - # is inquired to be demonstrated: - df_name = request.session.get("df_name") - df_name = request.session.get("df_name") - if df_name == "upload": - df_name = request.session.get("df_name_upload_base_name") - - model_name = request.session.get("model_name") - model_name_path = os.path.join( - PIPELINE_PATH + f"{df_name}" + "/trained_models/" + model_name - ) - - excel_file_name_path = os.path.join( - PIPELINE_PATH + f"{df_name}" + "/" + df_name + ".csv" - ) - - # pipeline path - json_path = os.path.join(PIPELINE_PATH, f"{df_name}" + "/pipeline.json") - # load pipeline data - jsonFile = pipeline.pipeline_json(json_path) - - class_label = jsonFile.read_pipline_json( - ["classifier", model_name, "class_label"] - ) - - # decode counterfactual to original values - preprocessing_list = jsonFile.read_pipline_json( - ["classifier", model_name, "preprocessing"] - ) - - df = pd.read_csv(excel_file_name_path) - cf_df = pd.read_csv(model_name_path + "/counterfactuals.csv") - cf_id = request.POST["cf_id"] - row = cf_df.iloc[[int(cf_id)]] - - if "id" in df.columns: - df = df.drop("id", axis=1) - - dec_row = methods.decode_cf( - df, row, class_label, model_name_path, preprocessing_list - ) - - fig = joblib.load(model_name_path + "/tsne_cfs.sav") - - # tsne stores data for each class in different data[] - # index. - # data[0] is class A - # data[1] is class B - # ... - # data[n-2] is counterfactuals - # data[n-1] is clicked point - - fig_data_array_length = len(fig.data) - for i in range(fig_data_array_length - 2): - fig.data[i].update( - opacity=0.3, - ) - - # last one, data[n-1], contains clicked point - l = fig.data[fig_data_array_length - 1] - clicked_id = -1 - for clicked_id, item in enumerate(list(zip(l.x, l.y))): - if math.isclose( - item[0], request.session.get("clicked_point")[0] - ) and math.isclose(item[1], request.session.get("clicked_point")[1]): - break - - # data[n-2] contains counterfactuals - fig.data[fig_data_array_length - 2].update( - selectedpoints=[int(cf_id)], - unselected=dict( - marker=dict( - opacity=0.3, - ) - ), - ) - fig.data[fig_data_array_length - 1].update( - selectedpoints=[clicked_id], - unselected=dict( - marker=dict( - opacity=0.3, - ) - ), - ) - - if "id" in df.columns: - df = df.drop("id", axis=1) - - # order the columns - dec_row = dec_row[df.columns] - clicked_point_row_id = request.session.get("cfrow_id") - - # return only the differences - dec_row = dec_row.reset_index(drop=True) - df2 = df.iloc[[int(clicked_point_row_id)]].reset_index(drop=True) - difference = dec_row.loc[ - :, - [ - methods.compare_values(dec_row[col].iloc[0], df2[col].iloc[0]) - for col in dec_row.columns - ], - ] - - merged_df = pd.concat([df2[difference.columns], difference], ignore_index=True) - print(merged_df) - context = { - "row": merged_df.to_html(index=False), - "fig": fig.to_html(), - } - - elif action == "reset_graph": - - model_name = request.session.get("model_name") - # dataframe name - excel_file_name = request.session.get("df_name") - # save the plots for future use - # folder path: pipelines/<dataset name>/trained_models/<model_name>/ - model_name_path = os.path.join( - PIPELINE_PATH + f"{excel_file_name}" + "/trained_models/" + model_name - ) - - model_name_dir_path = os.path.join(PIPELINE_PATH + f"{df_name}") - - tsne = joblib.load(model_name_dir_path + "/tsne.sav") - context = {"fig": tsne.to_html()} - - elif action == "dataset" or action == "uploaded_datasets": - - # action to add dataset when from radio button click - name = request.POST.get("df_name") - request.session["df_name"] = name - - if name == "upload": - name = request.session.get("df_name_upload_base_name") - - if name == "timeseries": - name = request.session.get("df_name") - - excel_file_name_path = os.path.join( - PIPELINE_PATH + f"{name}" + "/" + name + ".csv", - ) - - datasets_types_pipeline_json_path = os.path.join( - PIPELINE_PATH + "/dataset_types_pipeline.json" - ) - datasets_types_pipeline_json = pipeline.pipeline_json( - datasets_types_pipeline_json_path - ) - dataset_type = datasets_types_pipeline_json.read_pipline_json([name]) - uploaded_files = datasets_types_pipeline_json.get_keys_with_specific_value( - "uploaded" - ) - - if request.POST.get("df_name") == "upload" or action == "uploaded_datasets": - if type(dataset_type) is list: - dataset_type = dataset_type[0] - - if request.POST.get("df_name") != "upload" or action == "uploaded_datasets": - if os.path.exists(excel_file_name_path): - df = methods.get_dataframe(excel_file_name_path) - df.columns = df.columns.str.replace(" ", "_") - request.session["excel_file_name"] = excel_file_name_path - - json_path = os.path.join(PIPELINE_PATH + f"{name}" + "/pipeline.json") - if not os.path.exists(json_path): - pipeline_json = pipeline.pipeline_json(json_path) - pipeline_json.append_pipeline_json({"name": name}) - - if "tabular" == dataset_type: - - if "id" in df.columns: - df.drop(["id"], axis=1, inplace=True) - df.to_csv(excel_file_name_path, index=False) - - # tabular datasets - features = df.columns - feature1 = df.columns[3] - feature2 = df.columns[2] - label = "" - - labels = list( - df.select_dtypes(include=["object", "category"]).columns - ) - # Find binary columns (columns with only two unique values, including numerics) - binary_columns = [ - col for col in df.columns if df[col].nunique() == 2 - ] - - # Combine categorical and binary columns into one list - labels = list(set(labels + binary_columns)) - label = random.choice(labels) - fig = methods.stats( - excel_file_name_path, - dataset_type, - feature1=feature1, - feature2=feature2, - label=label, - ) - - # tabular dataset - request.session["data_to_display"] = df[:10].to_html() - request.session["features"] = list(features) - request.session["feature1"] = feature1 - request.session["feature2"] = feature2 - request.session["labels"] = list(labels) - request.session["curlabel"] = label - request.session["fig"] = fig - - context = { - "dataset_type": dataset_type, - "data_to_display": df[:10].to_html(), - "fig": fig, - "features": list(features), # error if not a list - "feature1": feature1, - "feature2": feature2, - "labels": list(labels), - "curlabel": label, - "uploaded_files": list(uploaded_files), - } - elif dataset_type == "timeseries": - - fig, fig1 = methods.stats( - excel_file_name_path, dataset_type, name=name - ) - # timeseries - request.session["fig"] = fig - request.session["fig1"] = fig1 - context = {"fig": fig, "fig1": fig1, "dataset_type": dataset_type} - - request.session["context"] = context - else: - context = {"uploaded_files": list(uploaded_files)} - else: - context = {} - elif action == "stat": - - name = request.session.get("df_name") - datasets_types_pipeline_json_path = os.path.join( - PIPELINE_PATH + "/dataset_types_pipeline.json" - ) - jsonFile = pipeline.pipeline_json(datasets_types_pipeline_json_path) - dataset_type = jsonFile.read_pipline_json([name]) - - if type(dataset_type) is list: - dataset_type = dataset_type[0] - - file_path = os.path.join( - PIPELINE_PATH + f"{name}" + "/" + name + ".csv", - ) - if dataset_type == "tabular": - feature1 = request.POST.get("feature1") - feature2 = request.POST.get("feature2") - label = request.POST.get("label") - else: - feature1 = request.POST.get("feature1") - feature2 = [] - label = [] - - fig = methods.stats( - file_path, - dataset_type, - None, - None, - feature1=feature1, - feature2=feature2, - label=label, - ) - context = { - "fig": fig, - } - elif action == "train": - - # train a new model - # parameters sent via ajax - model_name = request.POST.get("model_name") - df_name = request.session.get("df_name") - - # dataframe name - - if df_name == "upload": - df_name = request.session.get("df_name_upload_base_name") - - request.session["model_name"] = model_name - test_set_ratio = "" - if "test_set_ratio" in request.POST: - test_set_ratio = request.POST.get("test_set_ratio") - - datasets_types_pipeline_json_path = os.path.join( - PIPELINE_PATH + "/dataset_types_pipeline.json" - ) - jsonFile = pipeline.pipeline_json(datasets_types_pipeline_json_path) - dataset_type = jsonFile.read_pipline_json([df_name]) - - if type(dataset_type) is list: - dataset_type = dataset_type[0] - - if "array_preprocessing" in request.POST: - array_preprocessing = request.POST.get("array_preprocessing") - - if dataset_type == "tabular": - class_label = request.POST.get("class_label") - preprocessing_info = { - "preprocessing": array_preprocessing, - "test_set_ratio": test_set_ratio, - "explainability": {"technique": "dice"}, - "class_label": class_label, - } - elif dataset_type == "timeseries": - if model_name != "glacier": - preprocessing_info = { - "preprocessing": array_preprocessing, - "test_set_ratio": test_set_ratio, - "explainability": {"technique": model_name}, - } - else: - # Path to the Bash script - autoencoder = request.POST.get("autoencoder") - preprocessing_info = { - "autoencoder": autoencoder, - "explainability": {"technique": model_name}, - } - - # absolute excel_file_name_path - excel_file_name_path = os.path.join( - PIPELINE_PATH + f"{df_name}" + "/" + df_name + ".csv" - ) - - # load paths - # absolute excel_file_preprocessed_path - excel_file_name_preprocessed_path = os.path.join( - PIPELINE_PATH, - f"{df_name}" + "/" + df_name + "_preprocessed" + ".csv", - ) - - json_path = os.path.join(PIPELINE_PATH + f"{df_name}" + "/pipeline.json") - jsonFile = pipeline.pipeline_json(json_path) - # save the plots for future use - # folder path: pipelines/<dataset name>/trained_models/<model_name>/ - - model_name_path = os.path.join( - PIPELINE_PATH + f"{df_name}" + "/trained_models/" + model_name - ) - - model_name_dir_path = os.path.join(PIPELINE_PATH + f"{df_name}") - - # make the dir - if not os.path.exists(model_name_path): - os.makedirs(model_name_path) - - # if json exists, simply append to it - # jsonFile = open(json_path, "r") - # pipeline_json = pipeline_json.load(jsonFile) # data becomes a dictionary - # jsonFile.close() # Close the JSON file - - # if "classifier" in pipeline_json.keys(): - # temp_jason = {model_name: preprocessing_info} - # pipeline_json["classifier"].update(temp_jason) - # else: - # temp_jason = { - # "preprocessed_name": df_name + "_preprocessed.csv", - # "classifier": {model_name: preprocessing_info}, - # } - # pipeline_json.update(temp_jason) - - if jsonFile.check_key_pipeline_json("classifier"): - temp_json = {model_name: preprocessing_info} - jsonFile.update_pipeline_json(["classifier"], temp_json) - else: - temp_jason = { - "preprocessed_name": df_name + "_preprocessed.csv", - "classifier": {model_name: preprocessing_info}, - } - jsonFile.append_pipeline_json(temp_jason) - - if os.path.exists(excel_file_name_preprocessed_path) == True: - # if preprocessed_file exists - # delete it and do preprocessing again - # maybe should optimize it for cases - # where the preprocessing is the same with - # the one applited on the existing file - os.remove(excel_file_name_preprocessed_path) - - # generate filename - idx = excel_file_name_path.index(".") - excel_file_name_preprocessed = ( - df_name[:idx] + "_preprocessed" + excel_file_name_path[idx:] - ) - - # save file for preprocessing - preprocess_df = pd.read_csv(excel_file_name_path) - request.session["excel_file_name_preprocessed"] = excel_file_name_preprocessed - - if dataset_type == "tabular": - le = LabelEncoder() - preprocess_df[class_label] = le.fit_transform(preprocess_df[class_label]) - pickle.dump(le, open(model_name_path + "/label_encoder.sav", "wb")) - - if "array_preprocessing" in request.POST: - preprocess_df = methods.preprocess( - preprocess_df, - array_preprocessing, - excel_file_name_path, - dataset_type, - model_name_path, - class_label, - ) - elif dataset_type == "timeseries": - - pos = jsonFile.read_pipline_json(["pos"]) - neg = jsonFile.read_pipline_json(["neg"]) - pos_label, neg_label = 1, 0 - - if pos != pos_label: - preprocess_df.iloc[:, -1] = preprocess_df.iloc[:, -1].apply( - lambda x: pos_label if x == int(pos) else x - ) - if neg != neg_label: - preprocess_df.iloc[:, -1] = preprocess_df.iloc[:, -1].apply( - lambda x: neg_label if x == int(neg) else x - ) - - if "array_preprocessing" in request.POST: - preprocess_df = methods.preprocess( - preprocess_df, - array_preprocessing, - excel_file_name_path, - dataset_type, - model_name_path, - ) - - # PCA - pca = methods.generatePCA(preprocess_df) - # TSNE - if dataset_type == "tabular": - tsne, projections = methods.generateTSNE( - preprocess_df, dataset_type, class_label - ) - else: - tsne, projections = methods.generateTSNE(preprocess_df, dataset_type) - - # save the plots - pickle.dump(tsne, open(model_name_path + "/tsne.sav", "wb")) - pickle.dump(pca, open(model_name_path + "/pca.sav", "wb")) - - # save projections file for future use - with open(model_name_path + "/tsne_projections.json", "w") as f: - json.dump(projections.tolist(), f, indent=2) - - if dataset_type == "tabular": - # training - feature_importance, classification_report, importance_dict = ( - methods.training( - preprocess_df, - model_name, - float(test_set_ratio), - class_label, - dataset_type, - df_name, - model_name_path, - ) - ) - - # save some files - pickle.dump( - classification_report, - open(model_name_path + "/classification_report.sav", "wb"), - ) - pickle.dump( - feature_importance, - open(model_name_path + "/feature_importance.sav", "wb"), - ) - - # feature importance on the original categorical columns (if they exist) - df = pd.read_csv(excel_file_name_path) - df = df.drop(class_label, axis=1) - - # Initialize a dictionary to hold aggregated feature importances - categorical_columns = methods.get_categorical_features(df) - - if categorical_columns: - aggregated_importance = {} - encoded_columns = methods.update_column_list_with_one_hot_columns( - df, preprocess_df, df.columns - ) - - feature_mapping = defaultdict(list) - for col in encoded_columns: - # Check if the column matches a pattern in categorical column names - for original_col in categorical_columns: - if col.startswith(original_col + "_"): - feature_mapping[original_col].append(col) - break - else: - # If no match, it's likely a numerical or non-encoded feature, map it to itself - feature_mapping[col].append(col) - - # Aggregate the feature importances - for original_feature, encoded_columns in feature_mapping.items(): - if original_feature not in encoded_columns: - aggregated_importance[original_feature] = np.sum( - [importance_dict[col] for col in encoded_columns] - ) - else: - aggregated_importance[original_feature] = importance_dict[ - original_feature - ] - - importance_df = pd.DataFrame( - { - "feature": list(aggregated_importance.keys()), - "importance": list(aggregated_importance.values()), - } - ) - importance_df.to_csv( - model_name_path + "/feature_importance_df.csv", index=None - ) - else: - # if no categorical columns - - # Combine feature names with their respective importance values - feature_importance_df = pd.DataFrame( - { - "feature": importance_dict.keys(), - "importance": importance_dict.values(), - } - ) - - feature_importance_df.to_csv( - model_name_path + "/feature_importance_df.csv", index=None - ) - - # load pipeline data - # jsonFile = open(json_path, "r") - # pipeline_data = json.load(jsonFile) # data becomes a dictionary - # classifier_data = pipeline_data["classifier"][model_name] - # classifier_data_html = dict_and_html(classifier_data) - - classifier_data = jsonFile.read_pipline_json(["classifier", model_name]) - - classifier_data_html = dict_and_html(classifier_data) - - context = { - "dataset_type": dataset_type, - "tsne": tsne.to_html(), - "class_report": classification_report.to_html(), - "feature_importance": feature_importance.to_html(), - "classifier_data": classifier_data_html, - } - elif dataset_type == "timeseries": - - # training - # if model_name == "glacier": - # path = model_name_path_type_dir - # else: - path = model_name_path - dataset_camel = methods.convert_to_camel_case(df_name) - if "Ecg" in dataset_camel: - dataset_camel = dataset_camel.replace("Ecg", "ECG") - - experiment = methods.fetch_line_by_dataset( - PIPELINE_PATH + "/glacier_experiments.txt", - dataset_camel, - ) - - if experiment is not None: - stripped_arguments = methods.extract_arguments_from_line(experiment) - - if model_name == "glacier": - classification_report = methods.training( - preprocess_df, - model_name, - float(test_set_ratio) if test_set_ratio != "" else 0, - "", - dataset_type, - df_name, - path, - autoencoder, - stripped_arguments, - ) - else: - classification_report = methods.training( - preprocess_df, - model_name, - float(test_set_ratio) if test_set_ratio != "" else 0, - "", - dataset_type, - df_name, - path, - ) - - pickle.dump( - classification_report, - open(path + "/classification_report.sav", "wb"), - ) - - context = { - "dataset_type": dataset_type, - "pca": pca.to_html(), - "tsne": tsne.to_html(), - "classification_report": classification_report.to_html(), - } - - preprocess_df.to_csv(excel_file_name_preprocessed_path, index=False) - - elif action == "pre_trained": - # load pre trained models - pre_trained_model_name = request.POST.get("pre_trained") - request.session["model_name"] = pre_trained_model_name - # dataframe name - df_name = request.session.get("df_name") - - if df_name == "upload": - df_name = request.session.get("df_name_upload_base_name") - - model_name_path = os.path.join( - PIPELINE_PATH + f"{df_name}" + "/trained_models/" + pre_trained_model_name - ) - - model_name_dir_path = os.path.join(PIPELINE_PATH + f"{df_name}") - - # get the type of the file - datasets_types_pipeline_json_path = os.path.join( - PIPELINE_PATH + "/dataset_types_pipeline.json" - ) - datasets_types_pipeline = pipeline.pipeline_json( - datasets_types_pipeline_json_path - ) - dataset_type = datasets_types_pipeline.read_pipline_json([df_name]) - - if type(dataset_type) is list: - dataset_type = dataset_type[0] - - if "url" in request.POST: - url = request.POST.get("url") - if url == "counterfactuals": - # only TSNE - tsne = joblib.load(model_name_path + "/tsne.sav") - - # Assuming you already have your fig object created, you can update it like this: - # Improved and modern t-SNE visualization - tsne.update_layout( - # Modern Legend Design - legend=dict( - x=0.9, - y=0.95, - xanchor="right", - yanchor="top", - bgcolor="rgba(255,255,255,0.8)", # Light semi-transparent white background - bordercolor="rgba(0,0,0,0.1)", # Light border for contrast - borderwidth=1, - font=dict(size=12, color="#444") # Subtle grey for legend text - ), - # Tight Margins to Focus on the Plot - margin=dict(l=10, r=10, t=30, b=10), # Very slim margins for a modern look - # Axis Design: Minimalist and Clean - xaxis=dict( - title_text="", # No axis labels for a clean design - tickfont=dict(size=10, color="#aaa"), # Light grey for tick labels - showline=True, - linecolor="rgba(0,0,0,0.2)", # Subtle line color for axis lines - zeroline=False, # No zero line for a sleek look - showgrid=False, # Hide grid lines for a minimal appearance - ticks="outside", # Small ticks outside the axis - ticklen=3 # Short tick marks for subtlety - ), - yaxis=dict( - title_text="", # No axis labels - tickfont=dict(size=10, color="#aaa"), - showline=True, - linecolor="rgba(0,0,0,0.2)", - zeroline=False, - showgrid=False, - ticks="outside", - ticklen=3 - ), - # Sleek Background - plot_bgcolor="#fafafa", # Very light grey background for a smooth finish - paper_bgcolor="#ffffff", # Pure white paper background - # Modern Title with Elegant Style - title=dict( - text="t-SNE Visualization of Data", - font=dict(size=16, color="#222", family="Helvetica, Arial, sans-serif"), # Classy font style - x=0.5, - xanchor="center", - yanchor="top", - pad=dict(t=15) # Padding to separate the title from the plot - ) - ) - - # Add hover effects for a smooth user experience - tsne.update_traces(hoverinfo="text+name", hoverlabel=dict(bgcolor="white", font_size=12, font_family="Arial")) - - context = { - "tsne": tsne.to_html(), - } - else: - # load plots - pca = joblib.load(model_name_path + "/pca.sav") - classification_report = joblib.load( - model_name_path + "/classification_report.sav" - ) - # tsne = joblib.load(model_name_path + "/tsne.sav") - - # pipeline path - json_path = os.path.join(PIPELINE_PATH, f"{df_name}" + "/pipeline.json") - jsonFile = pipeline.pipeline_json(json_path) - - # load pipeline data - # jsonFile = open(json_path, "r") - # pipeline_data = json.load(jsonFile) # data becomes a dictionary - # classifier_data = pipeline_data["classifier"][pre_trained_model_name] - - classifier_data = jsonFile.read_pipline_json( - ["classifier", pre_trained_model_name] - ) - classifier_data_flattened = methods.flatten_dict(classifier_data) - classifier_data_df = pd.DataFrame([classifier_data_flattened]) - - if dataset_type == "tabular": - feature_importance = joblib.load( - model_name_path + "/feature_importance.sav" - ) - context = { - "dataset_type": dataset_type, - "pca": pca.to_html(), - "class_report": classification_report.to_html(), - "feature_importance": feature_importance.to_html(), - "classifier_data": classifier_data_df.to_html(), - } - elif dataset_type == "timeseries": - tsne = joblib.load(model_name_path + "/tsne.sav") - context = { - "dataset_type": dataset_type, - "pca": pca.to_html(), - "class_report": classification_report.to_html(), - "tsne": tsne.to_html(), - "classifier_data": classifier_data_df.to_html(), - } - - elif action == "cf": - # dataframe name - df_name = request.session.get("df_name") - if df_name == "upload": - df_name = request.session.get("df_name_upload_base_name") - - # preprocessed_path - excel_file_name_preprocessed_path = os.path.join( - PIPELINE_PATH + f"{df_name}" + "/" + df_name + "_preprocessed" + ".csv" - ) - - excel_file_name_path = os.path.join( - PIPELINE_PATH + f"{df_name}" + "/" + df_name + ".csv" - ) - # which model is being used during that session - model_name = request.POST.get("model_name") - # path of used model - model_name_path = os.path.join( - PIPELINE_PATH + f"{df_name}/" + "trained_models/" + f"{model_name}/" - ) - model_name_dir_path = os.path.join(PIPELINE_PATH + f"{df_name}") - - # read preprocessed data - if os.path.exists(excel_file_name_preprocessed_path): - df = pd.read_csv(excel_file_name_preprocessed_path) - else: - df = pd.read_csv(excel_file_name_path) - - datasets_types_pipeline_json_path = os.path.join( - PIPELINE_PATH + "/dataset_types_pipeline.json" - ) - datasets_types_pipeline = pipeline.pipeline_json( - datasets_types_pipeline_json_path - ) - dataset_type = datasets_types_pipeline.read_pipline_json([df_name]) - - if type(dataset_type) is list: - dataset_type = dataset_type[0] - - df_id = request.session.get("cfrow_id") - if dataset_type == "tabular": - - # get row - features_to_vary = json.loads(request.POST.get("features_to_vary")) - - row = df.iloc[[int(df_id)]] - - # not preprocessed - notpre_df = pd.read_csv(excel_file_name_path) - notpre_row = notpre_df.iloc[[int(df_id)]] - - # if feature_to_vary has a categorical column then I cannot just - # pass that to dice since the trained model does not contain the - # categorical column but the one-hot-encoded sub-columns - features_to_vary = methods.update_column_list_with_one_hot_columns( - notpre_df, df, features_to_vary - ) - - # pipeline path - json_path = os.path.join(PIPELINE_PATH, f"{df_name}" + "/pipeline.json") - - # load pipeline data - jsonFile = pipeline.pipeline_json(json_path) - class_label = jsonFile.read_pipline_json( - ["classifier", model_name, "class_label"] - ) # data becomes a dictionary - - # number of counterfactuals - # (TBD) input field value as parameter - # in ajax - num_counterfactuals = 5 - le = LabelEncoder() - notpre_df[class_label] = le.fit_transform(notpre_df[class_label]) - - continuous_features = methods.get_continuous_features(df) - non_continuous_features = methods.get_non_continuous_features(df) - - # load used classifier - clf = joblib.load(model_name_path + model_name + ".sav") - - try: - # Set up the executor to run the function in a separate thread - with concurrent.futures.ThreadPoolExecutor() as executor: - # Submit the function to the executor - future = executor.submit( - methods.counterfactuals, - row, - clf, - df, - class_label, - continuous_features, - num_counterfactuals, - features_to_vary, - ) - # Wait for the result with a timeout of 10 seconds - counterfactuals = future.result(timeout=10) - print("Counterfactuals computed successfully!") - except concurrent.futures.TimeoutError: - message = ( - "It seems like it took more than expected. Refresh and try again..." - ) - print(message) - exit(1) - - if counterfactuals: - cf_df = counterfactuals[0].final_cfs_df - counterfactuals[0].final_cfs_df.to_csv( - model_name_path + "counterfactuals.csv", index=False - ) - - # get coordinates of the clicked point (saved during 'click' event) - clicked_point = request.session.get("clicked_point") - clicked_point_df = pd.DataFrame( - { - "0": clicked_point[0], - "1": clicked_point[1], - f"{class_label}": row[class_label].astype(str), - } - ) - - # tSNE - cf_df = pd.read_csv(model_name_path + "counterfactuals.csv") - model_name_dir_path = os.path.join(PIPELINE_PATH + f"{df_name}") - tsne_path_to_augment = model_name_path + "tsne.sav" - - tsne = methods.generateAugmentedTSNE( - df, - cf_df, - num_counterfactuals, - clicked_point_df, - tsne_path_to_augment, - class_label, - ) - - tsne.update_layout( - # Modern Legend Design - legend=dict( - x=0.85, - y=0.95, - xanchor="right", - yanchor="top", - bgcolor="rgba(0,0,0,0.05)", # Transparent black background for a sleek look - bordercolor="rgba(0,0,0,0.1)", # Soft border for separation - borderwidth=1, - font=dict(size=12, color="#333") # Modern grey font color for text - ), - # Tight Margins for a Focused Plot Area - margin=dict(l=20, r=20, t=40, b=40), # Reduced margins for a cleaner look - # Axis Titles and Labels: Minimalist Design - xaxis=dict( - title_font=dict(size=14, color="#555"), # Medium grey color for axis title - tickfont=dict(size=11, color="#777"), # Light grey color for tick labels - showline=True, - linecolor="rgba(0,0,0,0.15)", # Subtle line color for axis lines - zeroline=False, # Hide the zero line for a cleaner design - showgrid=False # No grid lines for a modern look - ), - yaxis=dict( - title_font=dict(size=14, color="#555"), - tickfont=dict(size=11, color="#777"), - showline=True, - linecolor="rgba(0,0,0,0.15)", - zeroline=False, - showgrid=False - ), - # Sleek Background Design - plot_bgcolor="white", # Crisp white background for a modern touch - paper_bgcolor="white", # Ensure the entire background is uniform - # Title: Modern Font and Centered - title=dict( - text="t-SNE Visualization of Data", - font=dict(size=18, color="#333", family="Arial, sans-serif"), # Modern font style - x=0.5, - xanchor="center", - yanchor="top", - pad=dict(t=10) # Padding to give the title breathing space - ) - ) - - pickle.dump(tsne, open(model_name_path + "tsne_cfs.sav", "wb")) - - context = { - "dataset_type": dataset_type, - "model_name": model_name, - "tsne": tsne.to_html(), - "num_counterfactuals": num_counterfactuals, - "default_counterfactual": "1", - "clicked_point": notpre_row.to_html(), - "counterfactual": cf_df.iloc[[1]].to_html(), - } - - else: - context = { - "dataset_type": dataset_type, - "model_name": model_name, - "message": "Please try again with different features.", - } - elif dataset_type == "timeseries": - model_name = request.POST["model_name"] - model_name_path = os.path.join( - PIPELINE_PATH + f"{df_name}/" + "trained_models/" + f"{model_name}/" - ) - path = model_name_path - if model_name == "glacier": - constraint = request.POST["constraint"] - path = os.path.join( - PIPELINE_PATH - + f"{df_name}/" - + "trained_models/" - + f"{model_name}/" - + f"{constraint}/" - ) - - X_test_path = os.path.join(model_name_path + "X_test.csv") - y_test_path = os.path.join(model_name_path + "y_test.npy") - y_pred_path = os.path.join(path + "y_pred.npy") - X_cf_path = os.path.join(path + "X_cf.npy") - cf_pred_path = os.path.join(path + "cf_pred.npy") - - X_test = pd.read_csv(X_test_path) - y_test = np.load(y_test_path) - y_pred = np.load(y_pred_path) - X_cf = np.load(X_cf_path) - cf_pred = np.load(cf_pred_path) - - if model_name != "glacier": - scaler = joblib.load(model_name_path + "/min_max_scaler.sav") - X_test = pd.DataFrame(scaler.inverse_transform(X_test)) - X_cf = scaler.inverse_transform(X_cf) - - fig = methods.ecg_plot_counterfactuals( - int(df_id), X_test, y_test, y_pred, X_cf, cf_pred - ) - - context = { - "df_name": df_name, - "fig": fig.to_html(), - "dataset_type": dataset_type, - } - elif action == "compute_cf": - model_name = request.POST.get("model_name") - if model_name == "glacier": - constraint_type = request.POST.get("constraint") - w_value = request.POST.get("w_value") - df_name = request.session.get("df_name") - - model_name_path = os.path.join( - PIPELINE_PATH + f"{df_name}/" + "trained_models/" + f"{model_name}/" - ) - model_name_path_constraint = model_name_path + f"{constraint_type}/" - if not os.path.exists(model_name_path_constraint): - os.makedirs(model_name_path_constraint) - - # https://github.com/wildboar-foundation/wildboar/blob/master/docs/guide/explain/counterfactuals.rst#id27 - classifier = joblib.load(model_name_path + "/classifier.sav") - - # pipeline path - json_path = os.path.join(PIPELINE_PATH, f"{df_name}" + "/pipeline.json") - # load pipeline data - jsonFile = pipeline.pipeline_json(json_path) - autoencoder = jsonFile.read_pipline_json( - ["classifier", model_name, "autoencoder"] - ) - - experiment_dict = {"constraint": constraint_type, "w_value": w_value} - - # if "experiments" in pipeline_data["classifier"][model_name]: - # # if there exists key with value "experiments" - # keys = pipeline_data["classifier"][model_name]["experiments"].keys() - # last_key_int = int(list(keys)[-1]) - # last_key_int_incr_str = str(last_key_int + 1) - # else: - # last_key_int_incr_str = "0" - # experiment_key_dict = {"experiments": {last_key_int_incr_str: {}}} - # pipeline_data["classifier"][model_name].update(experiment_key_dict) - - # outter_dict = {last_key_int_incr_str: experiment_dict} - # pipeline_data["classifier"][model_name]["experiments"].update(outter_dict) - - if jsonFile.check_key_pipeline_json("experiments"): - keys = jsonFile.read_pipline_json( - ["classifier", model_name, "experiments"] - ).keys() - last_key_int = int(list(keys)[-1]) - last_key_int_incr_str = str(last_key_int + 1) - else: - last_key_int_incr_str = "0" - experiment_key_dict = {"experiments": {last_key_int_incr_str: {}}} - jsonFile.update_pipeline_json( - ["classifier", model_name], experiment_key_dict - ) - - outter_dict = {last_key_int_incr_str: experiment_dict} - jsonFile.update_pipeline_json( - ["classifier", model_name, "experiments"], outter_dict - ) - - if autoencoder == "Yes": - autoencoder = joblib.load(model_name_path + "/autoencoder.sav") - else: - autoencoder = None - - gc_compute_counterfactuals( - model_name_path, - model_name_path_constraint, - constraint_type, - [0.0001], - float(w_value), - 0.5, - classifier, - autoencoder, - ) - path = model_name_path_constraint - context = {"experiment_dict": experiment_dict} - elif action == "class_label_selection": - - df_name = request.session.get("df_name") - - if df_name == "upload": - df_name = request.session["df_name_upload_base_name"] - - datasets_types_pipeline_json_path = os.path.join( - PIPELINE_PATH + "/dataset_types_pipeline.json" - ) - - dataset_type_json = pipeline.pipeline_json(datasets_types_pipeline_json_path) - - dataset_type = dataset_type_json.read_pipline_json([df_name]) - # preprocessed_path - excel_file_name_preprocessed_path = os.path.join( - PIPELINE_PATH + f"{df_name}" + "/" + df_name + "_preprocessed" + ".csv" - ) - - excel_file_name_path = os.path.join( - PIPELINE_PATH + f"{df_name}" + "/" + df_name + ".csv" - ) - - # which model is being used during that session - model_name = request.POST.get("model_name") - - model_name_path = os.path.join( - PIPELINE_PATH + f"{df_name}" + "/trained_models/" + model_name - ) - - X_test_path = os.path.join( - PIPELINE_PATH - + f"{df_name}" - + "/trained_models" - + f"/{model_name}" - + "/X_test.csv" - ) - y_test_path = os.path.join( - PIPELINE_PATH - + f"{df_name}" - + "/trained_models" - + f"/{model_name}" - + "/y_test.npy" - ) - - X_test = pd.read_csv(X_test_path) - y_test = np.load(y_test_path) - - if model_name != "glacier": - scaler = joblib.load(model_name_path + "/min_max_scaler.sav") - X_test = pd.DataFrame(scaler.inverse_transform(X_test)) - - if dataset_type == "timeseries": - class_label = request.POST.get("class_label") - cfrow_id = request.POST.get("cfrow_id") - - class_label = ( - int(class_label) - if class_label.isdigit() - else ( - float(class_label) - if class_label.replace(".", "", 1).isdigit() - else class_label - ) - ) - - fig, index = methods.get_ecg_entry( - X_test, y_test, int(cfrow_id), class_label - ) - request.session["cfrow_id"] = index - request.session["class_label"] = class_label - context = {"fig": fig.to_html(), "dataset_type": dataset_type} - elif action == "dataset_charts": - df_name = request.POST.get("df_name") - request.session["df_name"] = df_name - context = {} - elif action == "timeseries-dataset": - - # action to add dataset when from radio button click - name = request.POST.get("timeseries_dataset") - - # add name of used dataframe in session for future use - request.session["df_name"] = name - excel_file_name_path = os.path.join( - PIPELINE_PATH + f"{name}" + "/" + name + ".csv", - ) - datasets_types_pipeline_json_path = os.path.join( - PIPELINE_PATH + "/dataset_types_pipeline.json" - ) - datasets_types_pipeline_json = pipeline.pipeline_json( - datasets_types_pipeline_json_path - ) - if os.path.exists(excel_file_name_path): - - dataset_type = datasets_types_pipeline_json.read_pipline_json([name]) - - df = methods.get_dataframe(excel_file_name_path) - df.columns = df.columns.str.replace(" ", "_") - request.session["excel_file_name"] = excel_file_name_path - - # find the available pre trained datasets - # check the pipeline file - json_path = os.path.join(PIPELINE_PATH, f"{name}" + "/pipeline.json") - jsonFile = pipeline.pipeline_json(json_path) - - preprocessing_info = {"name": name} - dataset_camel = methods.convert_to_camel_case(name) - if "Ecg" in dataset_camel: - dataset_camel = dataset_camel.replace("Ecg", "ECG") - experiment = methods.fetch_line_by_dataset( - PIPELINE_PATH + "/glacier_experiments.txt", - dataset_camel, - ) - if experiment is not None: - stripped_arguments = methods.extract_arguments_from_line(experiment) - indices_to_keys = { - 1: "pos", - 2: "neg", - } - - # Create a dictionary by fetching items from the list at the specified indices - inner_dict = { - key: stripped_arguments[index] for index, key in indices_to_keys.items() - } - preprocessing_info.update(inner_dict) - jsonFile.append_pipeline_json(preprocessing_info) - - pos = inner_dict["pos"] - neg = inner_dict["neg"] - fig, fig1 = methods.stats( - excel_file_name_path, dataset_type, int(pos), int(neg), name=name - ) - # timeseries - request.session["fig"] = fig - request.session["fig1"] = fig1 - context = {"fig": fig, "fig1": fig1, "dataset_type": dataset_type} - else: - context = {} - - return HttpResponse(json.dumps(context))