diff --git a/.gitignore b/.gitignore index 251fde10..66e1db9b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,4 @@ src/api_service/experiments/venv docker-compose.yml src/secretkey.txt docker-compose.mauri_dev.yml -venv -.DS_Store \ No newline at end of file +venv \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 8d900f25..6b106de2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,64 +1,47 @@ FROM python:3.12-slim-bookworm # Docker Files Vars -ARG LISTEN_PORT=8000 -ARG LISTEN_IP="0.0.0.0" +ARG LISTEN_PORT 8000 +ARG LISTEN_IP "0.0.0.0" # Default values for deploying with multiomix image -ENV LISTEN_PORT=$LISTEN_PORT -ENV LISTEN_IP=$LISTEN_IP -ENV DJANGO_SETTINGS_MODULE="multiomics_intermediate.settings_prod" -ENV RESULT_DATAFRAME_LIMIT_ROWS=500 -ENV TABLE_PAGE_SIZE=10 +ENV LISTEN_PORT $LISTEN_PORT +ENV LISTEN_IP $LISTEN_IP +ENV DJANGO_SETTINGS_MODULE "multiomics_intermediate.settings_prod" +ENV RESULT_DATAFRAME_LIMIT_ROWS 500 +ENV TABLE_PAGE_SIZE 10 # Modulector connection parameters -ENV MODULECTOR_HOST="127.0.0.1" -ENV MODULECTOR_PORT="8001" +ENV MODULECTOR_HOST "127.0.0.1" +ENV MODULECTOR_PORT "8001" # BioAPI connection parameters -ENV BIOAPI_HOST="127.0.0.1" -ENV BIOAPI_PORT="8002" +ENV BIOAPI_HOST "127.0.0.1" +ENV BIOAPI_PORT "8002" # PostgreSQL DB connection parameters -ENV POSTGRES_USERNAME="multiomics" -ENV POSTGRES_PASSWORD="multiomics" -ENV POSTGRES_HOST="db" -ENV POSTGRES_PORT=5432 -ENV POSTGRES_DB="multiomics" +ENV POSTGRES_USERNAME "multiomics" +ENV POSTGRES_PASSWORD "multiomics" +ENV POSTGRES_HOST "db" +ENV POSTGRES_PORT 5432 +ENV POSTGRES_DB "multiomics" # Mongo DB connection parameters -ENV MONGO_USERNAME="multiomics" -ENV MONGO_PASSWORD="multiomics" -ENV MONGO_HOST="mongo" -ENV MONGO_PORT=27017 -ENV MONGO_DB="multiomics" +ENV MONGO_USERNAME "multiomics" +ENV MONGO_PASSWORD "multiomics" +ENV MONGO_HOST "mongo" +ENV MONGO_PORT 27017 +ENV MONGO_DB "multiomics" # Redis -ENV REDIS_HOST="redis" -ENV REDIS_PORT=6379 +ENV REDIS_HOST "redis" +ENV REDIS_PORT 6379 # Installs system dependencies and Node.js RUN apt-get update && apt-get install -y python3-pip curl libcurl4-openssl-dev libssl-dev libxml2-dev \ && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && apt-get install -y nodejs && mkdir /config \ && mkdir /src -# Install R 4.4.2 from CRAN (Debian bookworm) and system dependencies -RUN set -eux; \ - echo "deb http://deb.debian.org/debian sid main" > /etc/apt/sources.list.d/debian-unstable.list; \ - printf 'APT::Default-Release "%s";\n' "bookworm" > /etc/apt/apt.conf.d/00default-release; \ - echo 'APT::Install-Recommends "false";' > /etc/apt/apt.conf.d/90no-recommends; \ - printf 'Package: *\nPin: release a=unstable\nPin-Priority: 50\n' > /etc/apt/preferences.d/99pin-unstable; \ - apt-get update; \ - apt-get install -y --no-install-recommends \ - ca-certificates curl gnupg build-essential gfortran libblas-dev liblapack-dev git \ - libssl-dev libcurl4-openssl-dev libxml2-dev; \ - apt-get install -y \ - r-base=4.2.2.20221110-2 r-base-dev=4.2.2.20221110-2 r-recommended=4.2.2.20221110-2; \ - rm -rf /var/lib/apt/lists/* - -# Install Bioconductor limma (stats and base come with R) -RUN R -q -e 'options(repos=c(CRAN="https://cloud.r-project.org")); install.packages("BiocManager"); BiocManager::install(version="3.16", ask=FALSE); BiocManager::install("limma", ask=FALSE, update=FALSE)' - # Installs Python dependencies and compiles the frontend ADD config/requirements.txt /config/ WORKDIR /src @@ -75,3 +58,4 @@ HEALTHCHECK --interval=5m --timeout=30s CMD ["/bin/bash", "-c", "/src/tools/chec ENTRYPOINT ["/bin/bash", "-c", "/src/entrypoint.sh"] EXPOSE $LISTEN_PORT + diff --git a/Dockerfile-celery b/Dockerfile-celery index 89b892b9..ea55bcba 100644 --- a/Dockerfile-celery +++ b/Dockerfile-celery @@ -1,64 +1,47 @@ FROM python:3.12-slim-bookworm # Docker Files Vars -ARG LISTEN_PORT=8000 -ARG LISTEN_IP="0.0.0.0" +ARG LISTEN_PORT 8000 +ARG LISTEN_IP "0.0.0.0" # Default values for deploying with multiomix image -ENV LISTEN_PORT=$LISTEN_PORT -ENV LISTEN_IP=$LISTEN_IP -ENV DJANGO_SETTINGS_MODULE="multiomics_intermediate.settings_prod" -ENV RESULT_DATAFRAME_LIMIT_ROWS=500 -ENV TABLE_PAGE_SIZE=10 +ENV LISTEN_PORT $LISTEN_PORT +ENV LISTEN_IP $LISTEN_IP +ENV DJANGO_SETTINGS_MODULE "multiomics_intermediate.settings_prod" +ENV RESULT_DATAFRAME_LIMIT_ROWS 500 +ENV TABLE_PAGE_SIZE 10 # Modulector connection parameters -ENV MODULECTOR_HOST="127.0.0.1" -ENV MODULECTOR_PORT="8001" +ENV MODULECTOR_HOST "127.0.0.1" +ENV MODULECTOR_PORT "8001" # BioAPI connection parameters -ENV BIOAPI_HOST="127.0.0.1" -ENV BIOAPI_PORT="8002" +ENV BIOAPI_HOST "127.0.0.1" +ENV BIOAPI_PORT "8002" # PostgreSQL DB connection parameters -ENV POSTGRES_USERNAME="multiomics" -ENV POSTGRES_PASSWORD="multiomics" -ENV POSTGRES_HOST="db" -ENV POSTGRES_PORT=5432 -ENV POSTGRES_DB="multiomics" +ENV POSTGRES_USERNAME "multiomics" +ENV POSTGRES_PASSWORD "multiomics" +ENV POSTGRES_HOST "db" +ENV POSTGRES_PORT 5432 +ENV POSTGRES_DB "multiomics" # Mongo DB connection parameters -ENV MONGO_USERNAME="multiomics" -ENV MONGO_PASSWORD="multiomics" -ENV MONGO_HOST="mongo" -ENV MONGO_PORT=27017 -ENV MONGO_DB="multiomics" +ENV MONGO_USERNAME "multiomics" +ENV MONGO_PASSWORD "multiomics" +ENV MONGO_HOST "mongo" +ENV MONGO_PORT 27017 +ENV MONGO_DB "multiomics" # Redis -ENV REDIS_HOST="redis" -ENV REDIS_PORT=6379 +ENV REDIS_HOST "redis" +ENV REDIS_PORT 6379 # Installs system dependencies RUN apt-get update && apt-get install -y python3-pip curl libcurl4-openssl-dev libssl-dev libxml2-dev \ && mkdir /config \ && mkdir /src -# Install R 4.4.2 from CRAN (Debian bookworm) and system dependencies -RUN set -eux; \ - echo "deb http://deb.debian.org/debian sid main" > /etc/apt/sources.list.d/debian-unstable.list; \ - printf 'APT::Default-Release "%s";\n' "bookworm" > /etc/apt/apt.conf.d/00default-release; \ - echo 'APT::Install-Recommends "false";' > /etc/apt/apt.conf.d/90no-recommends; \ - printf 'Package: *\nPin: release a=unstable\nPin-Priority: 50\n' > /etc/apt/preferences.d/99pin-unstable; \ - apt-get update; \ - apt-get install -y --no-install-recommends \ - ca-certificates curl gnupg build-essential gfortran libblas-dev liblapack-dev git \ - libssl-dev libcurl4-openssl-dev libxml2-dev; \ - apt-get install -y \ - r-base=4.2.2.20221110-2 r-base-dev=4.2.2.20221110-2 r-recommended=4.2.2.20221110-2; \ - rm -rf /var/lib/apt/lists/* - -# Install Bioconductor limma (stats and base come with R) -RUN R -q -e 'options(repos=c(CRAN="https://cloud.r-project.org")); install.packages("BiocManager"); BiocManager::install(version="3.16", ask=FALSE); BiocManager::install("limma", ask=FALSE, update=FALSE)' - # Installs Python dependencies ADD config/requirements_celery.txt /config/requirements.txt RUN pip install --upgrade pip && pip3 install -r /config/requirements.txt diff --git a/README.md b/README.md index e354cd0a..874a5f58 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Multiomix logo +Multiomix logo # Multiomix @@ -15,7 +15,6 @@ This document is focused on the **development** of the system. If you are lookin - Node JS >= `20.x` (tested version: `20.x`) - [Modulector][modulector] `2.2.0` - [BioAPI][bioapi] `1.2.1` -- R `4.4.2` (required for `differential-expression`) ## Installation @@ -67,7 +66,6 @@ Every time you want to work with Multiomix, you need to follow the below steps: 1. `python3 -m celery -A multiomics_intermediate worker -l info -Q stats` 1. `python3 -m celery -A multiomics_intermediate worker -l info -Q inference` 1. `python3 -m celery -A multiomics_intermediate worker -l info -Q sync_datasets` - 1. `python3 -m celery -A multiomics_intermediate worker -l info -Q differential_expression` 1. If you want to check Task in the GUI you can run [Flower](https://flower.readthedocs.io/en/latest/index.html) `python3 -m celery -A multiomics_intermediate flower` **NOTE:** maybe in Windows is needed to add `--pool=solo` to the previous commands. Example: `python3 -m celery -A multiomics_intermediate worker -l info -Q correlation_analysis --concurrency 1 --pool=solo` diff --git a/config/requirements.txt b/config/requirements.txt index 1279b8de..253e7564 100644 --- a/config/requirements.txt +++ b/config/requirements.txt @@ -28,5 +28,3 @@ scipy==1.13.0 statsmodels==0.14.2 xlrd==2.0.1 openpyxl==3.1.5 -rpy2==3.6.1 -urllib3==2.5.0 \ No newline at end of file diff --git a/config/requirements_celery.txt b/config/requirements_celery.txt index 5c6b30bc..859c0823 100644 --- a/config/requirements_celery.txt +++ b/config/requirements_celery.txt @@ -25,5 +25,3 @@ scipy==1.13.0 statsmodels==0.14.2 xlrd==2.0.1 openpyxl==3.1.5 -rpy2==3.6.1 -urllib3==2.5.0 \ No newline at end of file diff --git a/docker-compose_dist.yml b/docker-compose_dist.yml index b8aa0abe..ca1ac3db 100644 --- a/docker-compose_dist.yml +++ b/docker-compose_dist.yml @@ -233,21 +233,6 @@ services: # REDIS_HOST: 'redis' # REDIS_PORT: 6379 - # Celery worker for differential expression - differential-expression-worker: - image: omicsdatascience/multiomix:5.6.0-celery - restart: 'always' - depends_on: - - db - - mongo - volumes: - - media_data:/src/media - environment: - <<: *common-variables - QUEUE_NAME: 'differential_expression' # This MUST NOT be changed - CONCURRENCY: 2 - # PostgreSQL, Mongo y Redis usan los valores por defecto del resto de servicios - # Django backend service multiomix: image: omicsdatascience/multiomix:5.6.0 @@ -305,7 +290,6 @@ services: - stats-worker - inference-worker - sync-datasets-worker - - differential-expression-worker volumes: mongo_data: diff --git a/src/api_service/migrations/0062_alter_experiment_clinical_source_and_more.py b/src/api_service/migrations/0062_alter_experiment_clinical_source_and_more.py deleted file mode 100644 index c86db344..00000000 --- a/src/api_service/migrations/0062_alter_experiment_clinical_source_and_more.py +++ /dev/null @@ -1,142 +0,0 @@ -# Generated by Django 4.2.19 on 2026-01-14 21:38 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ( - "datasets_synchronization", - "0036_alter_cgdsstudy_clinical_patient_dataset_and_more", - ), - ("genes", "0002_auto_20210114_2331"), - ("user_files", "0015_alter_userfile_options"), - ("api_service", "0061_alter_experiment_shared_users"), - ] - - operations = [ - migrations.AlterField( - model_name="experiment", - name="clinical_source", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="experiments_as_clinical_source", - to="api_service.experimentclinicalsource", - ), - ), - migrations.AlterField( - model_name="experiment", - name="gem_source", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="experiments_as_gem_source", - to="api_service.experimentsource", - ), - ), - migrations.AlterField( - model_name="experiment", - name="mRNA_source", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="experiments_as_mrna_source", - to="api_service.experimentsource", - ), - ), - migrations.AlterField( - model_name="experimentclinicalsource", - name="extra_cgds_dataset", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="experiment_clinical_sources_as_extra_cgds_dataset", - to="datasets_synchronization.cgdsdataset", - ), - ), - migrations.AlterField( - model_name="experimentsource", - name="cgds_dataset", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="experiment_sources_as_cgds_dataset", - to="datasets_synchronization.cgdsdataset", - ), - ), - migrations.AlterField( - model_name="experimentsource", - name="user_file", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="experiment_sources_as_user_file", - to="user_files.userfile", - ), - ), - migrations.AlterField( - model_name="genecnacombination", - name="experiment", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="%(class)ss", - to="api_service.experiment", - ), - ), - migrations.AlterField( - model_name="genecnacombination", - name="gene", - field=models.ForeignKey( - db_column="gene", - db_constraint=False, - on_delete=django.db.models.deletion.DO_NOTHING, - related_name="%(class)ss_as_gene", - to="genes.gene", - ), - ), - migrations.AlterField( - model_name="genemethylationcombination", - name="experiment", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="%(class)ss", - to="api_service.experiment", - ), - ), - migrations.AlterField( - model_name="genemethylationcombination", - name="gene", - field=models.ForeignKey( - db_column="gene", - db_constraint=False, - on_delete=django.db.models.deletion.DO_NOTHING, - related_name="%(class)ss_as_gene", - to="genes.gene", - ), - ), - migrations.AlterField( - model_name="genemirnacombination", - name="experiment", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="%(class)ss", - to="api_service.experiment", - ), - ), - migrations.AlterField( - model_name="genemirnacombination", - name="gene", - field=models.ForeignKey( - db_column="gene", - db_constraint=False, - on_delete=django.db.models.deletion.DO_NOTHING, - related_name="%(class)ss_as_gene", - to="genes.gene", - ), - ), - ] diff --git a/src/api_service/models.py b/src/api_service/models.py index a0177314..e18ec18e 100644 --- a/src/api_service/models.py +++ b/src/api_service/models.py @@ -45,7 +45,6 @@ class ExperimentSource(models.Model): inference_experiments_as_mrna: QuerySet[InferenceExperiment] gem_source: QuerySet['Experiment'] mrna_source: QuerySet['Experiment'] - user_file = models.ForeignKey( UserFile, on_delete=models.CASCADE, @@ -336,9 +335,7 @@ def number_of_rows(self) -> int: """ if self.user_file: return self.user_file.number_of_rows - if self.cgds_dataset: - return self.__get_cgds_datasets_joined_df().shape[0] - return 0 + return self.__get_cgds_datasets_joined_df().shape[0] @property def number_of_samples(self) -> int: @@ -348,9 +345,7 @@ def number_of_samples(self) -> int: """ if self.user_file: return self.user_file.number_of_samples - if self.cgds_dataset: - return self.__get_cgds_datasets_joined_df().shape[1] - return 0 + return self.__get_cgds_datasets_joined_df().shape[1] class Experiment(models.Model): @@ -401,8 +396,10 @@ class Experiment(models.Model): # TODO: this can be stored in the Methylation type entity. Set the corresponding nullity in the new schema correlate_with_all_genes: bool = models.BooleanField(blank=False, null=False, default=True) - shared_institutions = models.ManyToManyField(Institution, blank=True, related_name='shared_correlation_analysis') - shared_users = models.ManyToManyField(User, blank=True, related_name='shared_users_correlation_analysis') + shared_institutions = models.ManyToManyField(Institution, blank=True, + related_name='shared_correlation_analysis') + shared_users = models.ManyToManyField(User, blank=True, + related_name='shared_users_correlation_analysis') is_public = models.BooleanField(blank=False, null=False, default=False) @property @@ -420,7 +417,7 @@ def get_combination_class(self): """ return get_combination_class(self.type) - def get_clinical_columns(self) -> list[str]: + def get_clinical_columns(self): """ Gets a list of columns from the clinical data @return: List of fields in clinical data diff --git a/src/api_service/utils.py b/src/api_service/utils.py index ff242e33..511cc9fa 100644 --- a/src/api_service/utils.py +++ b/src/api_service/utils.py @@ -131,7 +131,5 @@ def get_cgds_dataset(cgds_study: CGDSStudy, file_type: FileType) -> Optional[CGD return cgds_study.cna_dataset elif file_type == FileType.METHYLATION: return cgds_study.methylation_dataset - elif file_type == FileType.CLINICAL: - return cgds_study.clinical_patient_dataset else: return None diff --git a/src/api_service/views.py b/src/api_service/views.py index 901da96b..c0566d7f 100644 --- a/src/api_service/views.py +++ b/src/api_service/views.py @@ -210,10 +210,10 @@ def get_queryset(self): experiment: Experiment = Experiment.objects.filter( Q(pk=experiment_id) & ( - Q(user=user) | - Q(is_public=True) | - Q(shared_institutions__institutionadministration__user=user) | - Q(shared_users=user) + Q(user=user) | + Q(is_public=True) | + Q(shared_institutions__institutionadministration__user=user) | + Q(shared_users=user) ) ).distinct().get() combinations_queryset = experiment.combinations @@ -293,7 +293,6 @@ def get_queryset(self): serializer_class = ExperimentSerializerDetail permission_classes = [permissions.IsAuthenticated, ExperimentIsNotRunning] - class RemoveInstitutionFromExperimentView(APIView): """ API endpoint to remove an institution from an experiment. @@ -301,8 +300,7 @@ class RemoveInstitutionFromExperimentView(APIView): """ permission_classes = [permissions.IsAuthenticated] - @staticmethod - def post(request): + def post(self, request): """ Remove an institution from the experiment. """ @@ -330,16 +328,14 @@ def post(request): {"message": f"Institution {institution.id} removed from experiment {experiment.id}."} ) - class RemoveUserFromExperimentView(APIView): """ - API endpoint to remove a user from an experiment. + API endpoint to remove an user from an experiment. Only the owner of the experiment can perform this action. """ permission_classes = [permissions.IsAuthenticated] - @staticmethod - def post(request): + def post(self, request): """ Remove an institution from the experiment. """ @@ -367,7 +363,6 @@ def post(request): {"message": f"User {user.id} removed from experiment {experiment.id}."} ) - class ToggleExperimentPublicView(APIView): """ API endpoint to toggle the 'is_public' field of an experiment. @@ -375,8 +370,7 @@ class ToggleExperimentPublicView(APIView): """ permission_classes = [permissions.IsAuthenticated] - @staticmethod - def post(request): + def post(self, request): """ Toggle the 'is_public' field of the experiment. """ @@ -396,7 +390,6 @@ def post(request): {"id": experiment.id, "is_public": experiment.is_public} ) - class InstitutionNonExperimentsSharedListView(generics.ListAPIView): """ REST endpoint: Get all institution NOT associated with a specific experiment. @@ -416,7 +409,6 @@ def get_queryset(self): id__in=experiment.shared_institutions.values_list('id', flat=True) ) - class UsersNonExperimentsSharedListView(generics.ListAPIView): """ REST endpoint: Get all users NOT associated with a specific experiment. @@ -434,7 +426,6 @@ def get_queryset(self): associated_user_ids = experiment.shared_users.values_list('id', flat=True) return get_user_model().objects.exclude(id__in=associated_user_ids) - class UsersExperimentsSharedListView(generics.ListAPIView): """ REST endpoint: Get all institution associated with a specific experiment. @@ -450,7 +441,6 @@ def get_queryset(self): experiment = get_object_or_404(Experiment, id=experiment_id) return experiment.shared_users - class InstitutionExperimentsSharedListView(generics.ListAPIView): """ REST endpoint: Get all institution associated with a specific experiment. @@ -466,15 +456,13 @@ def get_queryset(self): experiment = get_object_or_404(Experiment, id=experiment_id) return experiment.shared_institutions - class AddInstitutionToExperimentView(APIView): """ API endpoint to add an institution to an experiment. """ permission_classes = [permissions.IsAuthenticated] - @staticmethod - def post(request): + def post(self, request): data = request.data institution_id = data.get('institutionId') experiment_id = data.get('experimentId') @@ -484,7 +472,7 @@ def post(request): {"error": "Both 'institutionId' and 'experimentId' are required."}, status=status.HTTP_400_BAD_REQUEST ) - + experiment = get_object_or_404(Experiment, id=experiment_id) institution = get_object_or_404(Institution, id=institution_id) @@ -493,15 +481,13 @@ def post(request): serializer = InstitutionSerializer(institution) return Response(serializer.data) - class AddUserToExperimentView(APIView): """ API endpoint to add an institution to an experiment. """ permission_classes = [permissions.IsAuthenticated] - @staticmethod - def post(request): + def post(self, request): data = request.data user_id = data.get('userId') experiment_id = data.get('experimentId') @@ -511,7 +497,7 @@ def post(request): {"error": "Both 'institutionId' and 'experimentId' are required."}, status=status.HTTP_400_BAD_REQUEST ) - + experiment = get_object_or_404(Experiment, id=experiment_id) user = get_object_or_404(User, id=user_id) @@ -1215,14 +1201,14 @@ def post(request: Request): # Gets Gene and GEM expression with time values gene_values, gem_values, clinical_time_values, _gene_samples, _gem_samples, \ clinical_samples = pipelines.get_valid_data_from_sources( - experiment, - gene, - gem, - round_values=False, - return_samples_identifiers=True, - clinical_attribute=time_attribute, - fill_clinical_missing_samples=False - ) + experiment, + gene, + gem, + round_values=False, + return_samples_identifiers=True, + clinical_attribute=time_attribute, + fill_clinical_missing_samples=False + ) # Gets event values clinical_event_values: np.ndarray = experiment.clinical_source.get_specific_samples_and_attributes( diff --git a/src/api_service/websocket_functions.py b/src/api_service/websocket_functions.py index 9fc51255..237e34b9 100644 --- a/src/api_service/websocket_functions.py +++ b/src/api_service/websocket_functions.py @@ -40,7 +40,7 @@ def send_update_cgds_studies_command(): def send_update_biomarkers_command(user_id: int): """ - Sends a message indicating that a Biomarker's state update has occurred + Sends a message indicating that an Biomarker's state update has occurred """ user_group_name = f'notifications_{user_id}' message = { @@ -51,7 +51,7 @@ def send_update_biomarkers_command(user_id: int): def send_update_user_file_command(user_id: int): """ - Sends a message indicating that a user file's state update has occurred + Sends a message indicating that an user file's state update has occurred """ user_group_name = f'notifications_{user_id}' message = { @@ -84,18 +84,6 @@ def send_update_trained_models_command(user_id: int): send_message(user_group_name, message) -def send_update_differential_expression_experiments_command(user_id: int): - """ - Sends a message indicating that a DifferentialExpressionExperiment state update has occurred - @param user_id: DifferentialExpressionExperiment's user's id to send the WS message - """ - user_group_name = f'notifications_{user_id}' - message = { - 'command': 'update_differential_expression_experiments' - } - send_message(user_group_name, message) - - def send_update_prediction_experiment_command(user_id: int): """ Sends a message indicating that a InferenceExperiment state update has occurred @@ -119,10 +107,9 @@ def send_update_cluster_label_set_command(user_id: int): } send_message(user_group_name, message) - def send_update_institutions_command(user_id: int): """ - Sends a message indicating that an Institution state update has occurred + Sends a message indicating that a Institution state update has occurred @param user_id: Institution's user's id to send the WS message """ user_group_name = f'notifications_{user_id}' @@ -131,14 +118,13 @@ def send_update_institutions_command(user_id: int): } send_message(user_group_name, message) - def send_update_user_for_institution_command(user_id: int): """ - Sends a message indicating that an Institution_user state update has occurred + Sends a message indicating that a Institution_user state update has occurred @param user_id: Institution's user's id to send the WS message """ user_group_name = f'notifications_{user_id}' message = { 'command': 'update_user_for_institution' } - send_message(user_group_name, message) + send_message(user_group_name, message) \ No newline at end of file diff --git a/src/datasets_synchronization/admin.py b/src/datasets_synchronization/admin.py index 6463ff93..670b8c4c 100644 --- a/src/datasets_synchronization/admin.py +++ b/src/datasets_synchronization/admin.py @@ -5,9 +5,8 @@ class CGDSStudyAdmin(admin.ModelAdmin): list_display = ('name', 'description', 'version', 'date_last_synchronization', 'state') - list_filter = ('state', 'tissues') + list_filter = ('state',) search_fields = ('name', 'description') - filter_horizontal = ('tissues',) def delete_queryset(self, request, queryset): """ @@ -43,9 +42,9 @@ def delete_queryset(self, request, queryset): 'mirna_dataset__name', 'mrna_dataset__name') + class SurvivalColumnsTupleAdmin(admin.ModelAdmin): """Useful for SurvivalColumnsTupleCGDSDataset and SurvivalColumnsTupleUserFile models.""" - @staticmethod @admin.display(description='CGDS Dataset') def dataset(obj: Union[SurvivalColumnsTupleCGDSDataset, SurvivalColumnsTupleUserFile]) -> str: @@ -54,7 +53,6 @@ def dataset(obj: Union[SurvivalColumnsTupleCGDSDataset, SurvivalColumnsTupleUser list_display = ('pk', 'dataset', 'time_column', 'event_column') search_fields = ('time_column', 'event_column') - # IMPORTANT: these models should be managed in the CGDS Panel in the frontend! admin.site.register(CGDSStudy, CGDSStudyAdmin) admin.site.register(CGDSDataset, CGDSDatasetAdmin) diff --git a/src/datasets_synchronization/migrations/0036_alter_cgdsstudy_clinical_patient_dataset_and_more.py b/src/datasets_synchronization/migrations/0036_alter_cgdsstudy_clinical_patient_dataset_and_more.py deleted file mode 100644 index 189242c2..00000000 --- a/src/datasets_synchronization/migrations/0036_alter_cgdsstudy_clinical_patient_dataset_and_more.py +++ /dev/null @@ -1,80 +0,0 @@ -# Generated by Django 4.2.19 on 2026-01-14 21:38 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ("datasets_synchronization", "0035_auto_20230922_2356"), - ] - - operations = [ - migrations.AlterField( - model_name="cgdsstudy", - name="clinical_patient_dataset", - field=models.OneToOneField( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="cgds_studies_as_clinical_patient_dataset", - to="datasets_synchronization.cgdsdataset", - ), - ), - migrations.AlterField( - model_name="cgdsstudy", - name="clinical_sample_dataset", - field=models.OneToOneField( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="cgds_studies_as_clinical_sample_dataset", - to="datasets_synchronization.cgdsdataset", - ), - ), - migrations.AlterField( - model_name="cgdsstudy", - name="cna_dataset", - field=models.OneToOneField( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="cgds_studies_as_cna_dataset", - to="datasets_synchronization.cgdsdataset", - ), - ), - migrations.AlterField( - model_name="cgdsstudy", - name="methylation_dataset", - field=models.OneToOneField( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="cgds_studies_as_methylation_dataset", - to="datasets_synchronization.cgdsdataset", - ), - ), - migrations.AlterField( - model_name="cgdsstudy", - name="mirna_dataset", - field=models.OneToOneField( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="cgds_studies_as_mirna_dataset", - to="datasets_synchronization.cgdsdataset", - ), - ), - migrations.AlterField( - model_name="cgdsstudy", - name="mrna_dataset", - field=models.OneToOneField( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="cgds_studies_as_mrna_dataset", - to="datasets_synchronization.cgdsdataset", - ), - ), - ] diff --git a/src/datasets_synchronization/migrations/0037_cgdsstudy_tissue.py b/src/datasets_synchronization/migrations/0037_cgdsstudy_tissue.py deleted file mode 100644 index 2d916279..00000000 --- a/src/datasets_synchronization/migrations/0037_cgdsstudy_tissue.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 4.2.19 on 2026-02-23 00:20 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ("tissues", "0001_initial"), - ( - "datasets_synchronization", - "0036_alter_cgdsstudy_clinical_patient_dataset_and_more", - ), - ] - - operations = [ - migrations.AddField( - model_name="cgdsstudy", - name="tissue", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="tissues.tissue", - ), - ), - ] diff --git a/src/datasets_synchronization/migrations/0038_remove_cgdsstudy_tissue_cgdsstudy_tissues.py b/src/datasets_synchronization/migrations/0038_remove_cgdsstudy_tissue_cgdsstudy_tissues.py deleted file mode 100644 index b004849a..00000000 --- a/src/datasets_synchronization/migrations/0038_remove_cgdsstudy_tissue_cgdsstudy_tissues.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 4.2.19 on 2026-03-05 22:27 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("tissues", "0002_tissue_code_and_initial_data"), - ("datasets_synchronization", "0037_cgdsstudy_tissue"), - ] - - operations = [ - migrations.RemoveField( - model_name="cgdsstudy", - name="tissue", - ), - migrations.AddField( - model_name="cgdsstudy", - name="tissues", - field=models.ManyToManyField(blank=True, to="tissues.tissue"), - ), - ] diff --git a/src/datasets_synchronization/models.py b/src/datasets_synchronization/models.py index 43a7b472..41482be8 100644 --- a/src/datasets_synchronization/models.py +++ b/src/datasets_synchronization/models.py @@ -10,7 +10,6 @@ from common.methylation import MethylationPlatform from feature_selection.models import TrainedModel from statistical_properties.models import StatisticalValidation -from tissues.models import Tissue from user_files.models import UserFile from user_files.models_choices import FileType from pandas import DataFrame @@ -105,7 +104,6 @@ def __get_reverse_study(self) -> Optional['CGDSDataset']: return cast(Optional['CGDSDataset'], self.clinical_patient_dataset) elif hasattr(self, 'clinical_sample_dataset'): return cast(Optional['CGDSDataset'], self.clinical_sample_dataset) - return None @property def study(self) -> Optional['CGDSDataset']: @@ -113,7 +111,7 @@ def study(self) -> Optional['CGDSDataset']: def __str__(self) -> str: study_name = self.study.name if self.study else '-' - return f'PK: {self.pk} | File: {self.file_path} | Col: {self.mongo_collection_name} | Assigned to study: {study_name}' + return f'File: {self.file_path} | Col: {self.mongo_collection_name} | Assigned to study: {study_name}' def __compute_number_of_row_and_samples_and_save(self) -> None: """ @@ -289,7 +287,6 @@ class CGDSStudy(models.Model): null=True, related_name='cgds_studies_as_clinical_sample_dataset' ) - tissues = models.ManyToManyField(Tissue, blank=True) task_id: Optional[str] = models.CharField(max_length=100, blank=True, null=True) # Celery Task ID def __str__(self) -> str: @@ -329,4 +326,4 @@ def delete(self, *args, **kwargs) -> None: dataset.delete() # Sends a websocket message to update the state in the frontend - send_update_cgds_studies_command() \ No newline at end of file + send_update_cgds_studies_command() diff --git a/src/datasets_synchronization/serializers.py b/src/datasets_synchronization/serializers.py index 3f78d88f..26bd47fe 100644 --- a/src/datasets_synchronization/serializers.py +++ b/src/datasets_synchronization/serializers.py @@ -28,7 +28,7 @@ class Meta: fields = ['id', 'time_column', 'event_column'] def get_fields(self, *args, **kwargs): - fields = super(SurvivalColumnsTupleCGDSSimpleSerializer, self).get_fields() + fields = super(SurvivalColumnsTupleCGDSSimpleSerializer, self).get_fields(*args, **kwargs) request = self.context.get('request', None) if request and getattr(request, 'method', None) == "POST": fields['id'].required = False @@ -123,9 +123,9 @@ def __check_collection_name(mongo_collection_name: str, editing_cgds_dataset_id: }) def __update_cgds_dataset( - self, - cgds_dataset_instance: CGDSDataset, - validated_data_pop + self, + cgds_dataset_instance: CGDSDataset, + validated_data_pop ) -> Optional[CGDSDataset]: """ Updates a CGDSDataset instance from a request data @@ -186,9 +186,10 @@ def __update_survival_columns(cgds_dataset_instance: CGDSDataset, validated_data # If there's an existing id, updates the element if 'id' in survival_column: try: - survival_column_obj: SurvivalColumnsTupleCGDSDataset = SurvivalColumnsTupleCGDSDataset.objects.get( - pk=survival_column['id'] - ) + survival_column_obj: SurvivalColumnsTupleCGDSDataset = SurvivalColumnsTupleCGDSDataset. \ + objects.get( + pk=survival_column['id'] + ) survival_column_obj.time_column = survival_column['time_column'] survival_column_obj.event_column = survival_column['event_column'] survival_column_obj.save() @@ -224,10 +225,9 @@ def create(self, validated_data) -> Optional[CGDSStudy]: methylation_dataset = self.__create_cgds_dataset(validated_data.pop('methylation_dataset')) clinical_patient_dataset = self.__create_cgds_dataset(validated_data.pop('clinical_patient_dataset')) clinical_sample_dataset = self.__create_cgds_dataset(validated_data.pop('clinical_sample_dataset')) - tissues = validated_data.pop('tissues', []) # Creates the CGDSStudy - cgds_study = CGDSStudy.objects.create( + return CGDSStudy.objects.create( mrna_dataset=mrna_dataset, mirna_dataset=mirna_dataset, cna_dataset=cna_dataset, @@ -236,8 +236,6 @@ def create(self, validated_data) -> Optional[CGDSStudy]: clinical_sample_dataset=clinical_sample_dataset, **validated_data ) - cgds_study.tissues.set(tissues) - return cgds_study @staticmethod def __set_clinical_datasets_to_existing_experiments(cgds_study: CGDSStudy): @@ -305,27 +303,27 @@ def update(self, instance: CGDSStudy, validated_data): instance.clinical_patient_dataset = clinical_patient_dataset instance.clinical_sample_dataset = clinical_sample_dataset - # Updates M2M tissues if provided - if 'tissues' in validated_data: - instance.tissues.set(validated_data['tissues']) - # Saves new changes and returns instance instance.save() return instance class SimpleCGDSDatasetSerializer(serializers.ModelSerializer): - """CGDSDataset serializer with few fields for list views.""" - name = serializers.CharField(source='study.name', read_only=True) - description = serializers.CharField(source='study.description', read_only=True) - version = serializers.CharField(source='study.version', read_only=True) - file_obj = serializers.SerializerMethodField(method_name='get_file_obj') - class Meta: model = CGDSDataset - fields = ['id', 'name', 'description', 'version', 'date_last_synchronization', 'file_type', 'file_obj'] - - @staticmethod - def get_file_obj(_instance: CGDSDataset): - """Returns None to avoid sending the file in list views.""" - return None + fields = [] + + def to_representation(self, instance): + # Gets the file content for user_file + data = super(SimpleCGDSDatasetSerializer, self).to_representation(instance) + + # Serialize the study + study = instance.study + data['name'] = study.name + data['description'] = study.description + data['version'] = study.version + data['date_last_synchronization'] = instance.date_last_synchronization + data['file_type'] = instance.file_type + data['file_obj'] = None + + return data diff --git a/src/datasets_synchronization/urls.py b/src/datasets_synchronization/urls.py index e5f71a5d..595a6b54 100644 --- a/src/datasets_synchronization/urls.py +++ b/src/datasets_synchronization/urls.py @@ -7,9 +7,6 @@ # CGDS Studies path('studies', views.CGDSStudyList.as_view(), name='cgds_studies'), path('studies//', views.CGDSStudyDetail.as_view()), - # CGDS Datasets - path('cgds-dataset-clinical-attributes/', views.CGDSDatasetClinicalAttributes.as_view(), name='cgds_dataset_clinical_attributes'), - path('cgds-dataset-clinical-attributes//', views.CGDSDatasetClinicalAttributes.as_view()), # Synchronization path('sync', views.SyncCGDSStudy.as_view(), name='sync_cgds_study'), path('stop-sync', views.StopCGDSSync.as_view(), name='stop_cgds_study_sync') diff --git a/src/datasets_synchronization/views.py b/src/datasets_synchronization/views.py index 3e7097f7..2818b2c6 100644 --- a/src/datasets_synchronization/views.py +++ b/src/datasets_synchronization/views.py @@ -11,14 +11,11 @@ from common.pagination import StandardResultsSetPagination from common.response import ResponseStatus from .enums import SyncCGDSStudyResponseCode, SyncStrategy -from .models import CGDSStudy, CGDSDatasetSynchronizationState, CGDSStudySynchronizationState, CGDSDataset +from .models import CGDSStudy, CGDSDatasetSynchronizationState, CGDSStudySynchronizationState from rest_framework import generics, permissions, filters -from django_filters.rest_framework import DjangoFilterBackend from user_files.models_choices import FileType from .serializers import CGDSStudySerializer -from django.shortcuts import render, get_object_or_404 -from api_service.utils import get_cgds_dataset -from user_files.models_choices import FileType +from django.shortcuts import render @login_required @@ -83,8 +80,7 @@ def get_queryset(self): serializer_class = CGDSStudySerializer permission_classes = [permissions.IsAuthenticated] pagination_class = StandardResultsSetPagination - filter_backends = [filters.OrderingFilter, filters.SearchFilter, DjangoFilterBackend] - filterset_fields = ['tissues'] + filter_backends = [filters.OrderingFilter, filters.SearchFilter] search_fields = ['name', 'description'] ordering_fields = '__all__' @@ -218,17 +214,3 @@ def get(request: Request): # Formats to JSON the ResponseStatus object return Response(response) - - -class CGDSDatasetClinicalAttributes(APIView): - """REST endpoint: list for CGDSDataset header.""" - permission_classes = [permissions.IsAuthenticated] - - @staticmethod - def get(request, pk: int): - """Gets the clinical attributes of a CGDSDataset.""" - cgds_study = get_object_or_404(CGDSStudy, pk=pk) - # Gets the corresponding Study's Dataset - cgds_dataset = get_cgds_dataset(cgds_study, FileType.CLINICAL) - list_of_samples = cgds_dataset.get_column_names() - return Response(list_of_samples) diff --git a/src/differential_expression/admin.py b/src/differential_expression/admin.py index 4ce2c201..f1244222 100644 --- a/src/differential_expression/admin.py +++ b/src/differential_expression/admin.py @@ -1,78 +1 @@ from django.contrib import admin -from .models import ( - DifferentialExpressionExperiment, - DifferentialExpressionExperimentResult -) - - -@admin.register(DifferentialExpressionExperiment) -class DifferentialExpressionExperimentAdmin(admin.ModelAdmin): - """Admin configuration for DifferentialExpressionExperiment model.""" - - list_display = ( - 'id', 'name', 'user', 'state', 'clinical_attribute', 'tool', - 'threshold_percentile', 'threshold', 'top', 'execution_time', 'created_at' - ) - list_filter = ( - 'state', 'is_public', 'clinical_attribute', 'created_at' - ) - search_fields = ('name', 'description', 'user__username', 'clinical_attribute') - readonly_fields = ( - 'id', 'task_id', 'execution_time', 'attempt', 'created_at', 'updated_at', - 'get_results_count' - ) - filter_horizontal = ('shared_institutions', 'shared_users') - - fieldsets = ( - ('Basic Information', { - 'fields': ('id', 'name', 'description', 'user') - }), - ('Data Sources', { - 'fields': ('clinical_source', 'mrna_source') - }), - ('Analysis Parameters', { - 'fields': ('clinical_attribute', 'tool', 'threshold_percentile', 'threshold', 'top') - }), - ('Execution Information', { - 'fields': ('state', 'task_id', 'execution_time', 'attempt', 'created_at', 'updated_at'), - 'classes': ('collapse',) - }), - ('Results Statistics', { - 'fields': ('get_results_count',), - 'classes': ('collapse',) - }), - ('Sharing', { - 'fields': ('is_public', 'shared_institutions', 'shared_users'), - 'classes': ('collapse',) - }), - ) - - def get_results_count(self, obj): - """Returns the total number of results.""" - try: - return obj.results.count() - except: - return 0 - get_results_count.short_description = 'Total Results' - - -@admin.register(DifferentialExpressionExperimentResult) -class DifferentialExpressionExperimentResultAdmin(admin.ModelAdmin): - """Admin configuration for DifferentialExpressionExperimentResult model.""" - - list_display = ( - 'id', 'experiment', 'gene', 'adj_p_val', 'log_fc', - 'p_value', 'ave_expr' - ) - list_filter = ('experiment__state', 'experiment__user') - search_fields = ('gene', 'experiment__name', 'experiment__user__username') - readonly_fields = ('id',) - - fieldsets = ( - ('Basic Information', { - 'fields': ('id', 'experiment', 'gene') - }), - ('Statistical Results', { - 'fields': ('p_value', 'adj_p_val', 'log_fc', 'ave_expr', 't_statistic', 'b_statistic') - }), - ) diff --git a/src/differential_expression/migrations/0001_initial.py b/src/differential_expression/migrations/0001_initial.py deleted file mode 100644 index c098fcec..00000000 --- a/src/differential_expression/migrations/0001_initial.py +++ /dev/null @@ -1,62 +0,0 @@ -# Generated by Django 4.2.19 on 2026-01-29 23:38 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('institutions', '0005_alter_institution_options_and_more'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('api_service', '0062_alter_experiment_clinical_source_and_more'), - ] - - operations = [ - migrations.CreateModel( - name='DifferentialExpressionExperiment', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=300)), - ('description', models.TextField(blank=True, null=True)), - ('clinical_attribute', models.CharField(max_length=100)), - ('tool', models.CharField(choices=[('DESEQ', 'Deseq'), ('LIMMA', 'Limma')], default='DESEQ', help_text='Tool to use for differential expression analysis', max_length=10)), - ('threshold_percentile', models.FloatField(default=0.15)), - ('threshold', models.FloatField(default=0.0001)), - ('top', models.IntegerField(default=100, help_text='Number of significant results to keep (max 1000)')), - ('execution_time', models.FloatField(blank=True, default=0.0, help_text='Execution time in seconds', null=True)), - ('task_id', models.CharField(blank=True, help_text='Celery Task ID', max_length=100, null=True)), - ('attempt', models.PositiveSmallIntegerField(default=0, help_text='Number of attempts to prevent a buggy experiment running forever')), - ('state', models.IntegerField(choices=[(1, 'Completed'), (2, 'Finished With Error'), (3, 'In Process'), (4, 'Waiting For Queue'), (5, 'No Samples In Common'), (6, 'Stopping'), (7, 'Stopped'), (8, 'Reached Attempts Limit'), (9, 'No Features Found'), (10, 'Empty Dataset'), (11, 'Timeout Exceeded')], default=4, help_text='Current state of the differential expression experiment')), - ('created_at', models.DateTimeField(auto_now_add=True, help_text='When the experiment was created', null=True)), - ('updated_at', models.DateTimeField(auto_now=True, help_text='When the experiment was last updated')), - ('is_public', models.BooleanField(default=False)), - ('clinical_source', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='differential_expression_experiments_as_clinical', to='api_service.experimentclinicalsource')), - ('mrna_source', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='differential_expression_experiments_as_mrna', to='api_service.experimentsource')), - ('shared_institutions', models.ManyToManyField(blank=True, related_name='shared_differential_expression', to='institutions.institution')), - ('shared_users', models.ManyToManyField(blank=True, related_name='shared_users_differential_expression', to=settings.AUTH_USER_MODEL)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - ), - migrations.CreateModel( - name='DifferentialExpressionExperimentResult', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('gene', models.CharField(help_text='Gene identifier', max_length=100)), - ('ave_expr', models.FloatField(help_text='Average expression level')), - ('p_value', models.FloatField(help_text='P-value from statistical test')), - ('adj_p_val', models.FloatField(help_text='Adjusted P-value (FDR corrected)')), - ('log_fc', models.FloatField(help_text='Log fold change')), - ('t_statistic', models.FloatField(help_text='t-statistic from the test')), - ('b_statistic', models.FloatField(help_text='B-statistic (log-odds of differential expression)')), - ('experiment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='results', to='differential_expression.differentialexpressionexperiment')), - ], - options={ - 'indexes': [models.Index(fields=['experiment', 'adj_p_val'], name='differentia_experim_c0aff3_idx'), models.Index(fields=['experiment', 'log_fc'], name='differentia_experim_084601_idx'), models.Index(fields=['gene'], name='differentia_gene_7c8c66_idx')], - 'unique_together': {('experiment', 'gene')}, - }, - ), - ] diff --git a/src/differential_expression/models.py b/src/differential_expression/models.py index 53dd4a04..5482a0c3 100644 --- a/src/differential_expression/models.py +++ b/src/differential_expression/models.py @@ -1,234 +1 @@ from django.db import models -from django.contrib.auth import get_user_model -from institutions.models import Institution -from django.contrib.auth.models import User -import pandas as pd -import math -from django.db.models import Q, QuerySet - -from api_service.websocket_functions import send_update_differential_expression_experiments_command -from api_service.models import ExperimentSource, ExperimentClinicalSource - - -class DifferentialExpressionExperimentState(models.IntegerChoices): - """All the possible states of a Differential Expression Experiment.""" - COMPLETED = 1 - FINISHED_WITH_ERROR = 2 - IN_PROCESS = 3 - WAITING_FOR_QUEUE = 4 - NO_SAMPLES_IN_COMMON = 5 - STOPPING = 6 - STOPPED = 7 - REACHED_ATTEMPTS_LIMIT = 8 - NO_FEATURES_FOUND = 9 - EMPTY_DATASET = 10 - TIMEOUT_EXCEEDED = 11 - - -class DifferentialExpressionTool(models.TextChoices): - """Tool choices for differential expression analysis.""" - DESEQ = 'DESEQ' - LIMMA = 'LIMMA' - - -class DifferentialExpressionExperiment(models.Model): - """ - Model to create and manage differential expression data. - """ - results: QuerySet['DifferentialExpressionExperimentResult'] - - name = models.CharField(max_length=300) - description = models.TextField(blank=True, null=True) - - # Clinical and mRNA sources - # These are used to link the experiment to the clinical and mRNA data sources - clinical_source = models.ForeignKey( - 'api_service.ExperimentClinicalSource', - on_delete=models.CASCADE, - null=False, - blank=False, - related_name='differential_expression_experiments_as_clinical' - ) - - mrna_source = models.ForeignKey( - 'api_service.ExperimentSource', - on_delete=models.CASCADE, - null=False, - blank=False, - related_name='differential_expression_experiments_as_mrna' - ) - - clinical_attribute = models.CharField(max_length=100, blank=False, null=False) - - tool = models.CharField( - max_length=10, - choices=DifferentialExpressionTool.choices, - default=DifferentialExpressionTool.DESEQ, - blank=False, - null=False, - help_text='Tool to use for differential expression analysis' - ) - - threshold_percentile = models.FloatField(default=0.15, blank=False, null=False) - - threshold = models.FloatField(default=0.0001, blank=False, null=False) - - top = models.IntegerField( - default=100, - blank=False, - null=False, - help_text='Number of significant results to keep (max 1000)' - ) - - # Celery task related fields - # This is used to track the execution of the task and its state - execution_time = models.FloatField(default=0.0, blank=True, null=True, help_text='Execution time in seconds') - task_id = models.CharField(max_length=100, blank=True, null=True, help_text='Celery Task ID') - attempt = models.PositiveSmallIntegerField(default=0, help_text='Number of attempts to prevent a buggy experiment ' - 'running forever') - state = models.IntegerField( - choices=DifferentialExpressionExperimentState.choices, - default=DifferentialExpressionExperimentState.WAITING_FOR_QUEUE, - help_text='Current state of the differential expression experiment' - ) - - # Timestamp fields - created_at = models.DateTimeField(auto_now_add=True, null=True, blank=True, - help_text='When the experiment was created') - updated_at = models.DateTimeField(auto_now=True, help_text='When the experiment was last updated') - - # User and sharing information - # This is used to track the user who created the experiment and to share it with other users or institutions - user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) - is_public = models.BooleanField(blank=False, null=False, default=False) - shared_institutions = models.ManyToManyField(Institution, related_name='shared_differential_expression', blank=True) - shared_users = models.ManyToManyField(User, blank=True, - related_name='shared_users_differential_expression') - - def __str__(self): - return f"Differential Expression Experiment: {self.name}" - - def save(self, *args, **kwargs): - """ - Every time the experiment status changes, uses websockets to update state in the frontend. - """ - if not self.name: - raise ValueError("Experiment name cannot be empty.") - - super().save(*args, **kwargs) - - # Sends a websockets message to update the experiment state in the frontend - send_update_differential_expression_experiments_command(self.user.id) - - def save_results(self, dataframe): - """ - Save the differential expression DataFrame results. - @param dataframe: pandas DataFrame with differential expression results. - """ - # Clear existing results - self.results.all().delete() - - # Create new result records - results_to_create = [] - for gene_name, row in dataframe.iterrows(): - # Handle NaN values by replacing them with appropriate defaults - def safe_float(value, default=0.0): - """Convert value to float, handling NaN and inf values.""" - try: - if pd.isna(value) or math.isinf(float(value)): - return default - return float(value) - except (ValueError, TypeError): - return default - - result = DifferentialExpressionExperimentResult( - experiment=self, - gene=str(gene_name), # The gene identifier from the DataFrame index - ave_expr=safe_float(row.get('AveExpr', 0.0), 0.0), - p_value=safe_float(row.get('P.Value', 1.0), 1.0), - adj_p_val=safe_float(row.get('adj.P.Val', 1.0), 1.0), - log_fc=safe_float(row.get('logFC', 0.0), 0.0), - t_statistic=safe_float(row.get('t', 0.0), 0.0), - b_statistic=safe_float(row.get('B', 0.0), 0.0) - ) - results_to_create.append(result) - - # Bulk create for efficiency - DifferentialExpressionExperimentResult.objects.bulk_create(results_to_create) - - def get_results_dataframe(self): - """ - Retrieve results as a pandas DataFrame. - """ - results = self.results.all() - if not results.exists(): - return None - - data = [{ - 'gene': result.gene, - 'AveExpr': result.ave_expr, - 'P.Value': result.p_value, - 'adj.P.Val': result.adj_p_val, - 'logFC': result.log_fc, - 't': result.t_statistic, - 'B': result.b_statistic - } for result in results] - - df = pd.DataFrame(data) - df.set_index('gene', inplace=True) - return df - - def get_significant_genes(self, p_value_threshold=0.05, log_fc_threshold=1.0): - """ - Get significantly differentially expressed genes. - @param p_value_threshold: Maximum adjusted p-value (default 0.05). - @param log_fc_threshold: Minimum absolute log fold change (default 1.0). - @return QuerySet of DifferentialExpressionExperimentResult objects meeting the criteria. - """ - - return self.results.filter( - Q(adj_p_val__lte=p_value_threshold) & - (Q(log_fc__gte=log_fc_threshold) | Q(log_fc__lte=-log_fc_threshold)) - ) - - def delete(self, *args, **kwargs): - """ - Deletes the instance and sends a websockets message to update state in the frontend - """ - user_id = self.user.id # Store user_id before deletion - super().delete(*args, **kwargs) - - # Sends a websockets message to update the experiment state in the frontend - send_update_differential_expression_experiments_command(user_id) - - -class DifferentialExpressionExperimentResult(models.Model): - """ - Model to store individual differential expression results for each gene. - """ - - experiment = models.ForeignKey( - 'DifferentialExpressionExperiment', - on_delete=models.CASCADE, - related_name='results' - ) - gene = models.CharField(max_length=100, help_text='Gene identifier') - ave_expr = models.FloatField(help_text='Average expression level') - p_value = models.FloatField(help_text='P-value from statistical test') - adj_p_val = models.FloatField(help_text='Adjusted P-value (FDR corrected)') - log_fc = models.FloatField(help_text='Log fold change') - t_statistic = models.FloatField(help_text='t-statistic from the test') - b_statistic = models.FloatField(help_text='B-statistic (log-odds of differential expression)') - - class Meta: - unique_together = ('experiment', 'gene') - indexes = [ - models.Index(fields=['experiment', 'adj_p_val']), - models.Index(fields=['experiment', 'log_fc']), - models.Index(fields=['gene']), - ] - - def __str__(self): - return f"{self.gene} - {self.experiment.name}" - - diff --git a/src/differential_expression/serializers.py b/src/differential_expression/serializers.py deleted file mode 100644 index 0937ce53..00000000 --- a/src/differential_expression/serializers.py +++ /dev/null @@ -1,139 +0,0 @@ -from django.contrib.auth.models import User -from rest_framework import serializers - -from api_service.serializers import ExperimentSourceSerializer, ExperimentClinicalSourceSerializer -from differential_expression.models import ( - DifferentialExpressionExperiment, - DifferentialExpressionExperimentResult -) -from institutions.models import Institution - - -class UserSimpleForDiffExpExperiments(serializers.ModelSerializer): - """Simple serializer of User model for some differential expression analysis serializers.""" - - class Meta: - model = User - fields = ['id', 'username', 'first_name', 'last_name', 'email'] - read_only_fields = ['id', 'username', 'first_name', 'last_name', 'email'] - - -class InstitutionSimpleForDiffExpExperiments(serializers.ModelSerializer): - """Simple serializer of Institution model for some differential expression analysis serializers.""" - - class Meta: - model = Institution - fields = ['id', 'name'] - read_only_fields = ['id', 'name'] - - -class DifferentialExpressionExperimentResultSerializer(serializers.ModelSerializer): - """ - Serializer for Differential Expression Experiment Results. - """ - class Meta: - model = DifferentialExpressionExperimentResult - fields = ['id', 'gene', 'ave_expr', 'p_value', 'adj_p_val', 'log_fc', 't_statistic', 'b_statistic'] - - -class DifferentialExpressionVolcanoPlotSerializer(serializers.ModelSerializer): - """ - Serializer for volcano plot visualization. - Returns minimal fields needed for plotting: id, label (gene name), log2FC, and pValue (adj.P.Val). - """ - label = serializers.CharField(source='gene', read_only=True) - log2FC = serializers.FloatField(source='log_fc', read_only=True) - pValue = serializers.FloatField(source='adj_p_val', read_only=True) - - class Meta: - model = DifferentialExpressionExperimentResult - fields = ['id', 'label', 'log2FC', 'pValue'] - - -class DifferentialExpressionExperimentListSerializer(serializers.ModelSerializer): - """ - Optimized serializer for differential expression experiments list view. - Returns essential fields for table display: id, user, name, description, created_at, - state, clinical_source, mrna_source, and is_public. - Uses compatible source serializers that match frontend DjangoExperimentSource interface. - """ - # Sources - using standard API serializers compatible with frontend interfaces - clinical_source = ExperimentClinicalSourceSerializer(read_only=True) - mrna_source = ExperimentSourceSerializer(read_only=True) - user = UserSimpleForDiffExpExperiments(read_only=True) - - # State information - state_display = serializers.CharField(source='get_state_display', read_only=True) - - class Meta: - model = DifferentialExpressionExperiment - fields = [ - 'id', # Experiment ID - 'user', # User ID (compatible with frontend expectations) - 'name', # Experiment name - 'description', # Experiment description - 'created_at', # Creation date - 'state', # Experiment state - 'state_display', # Human-readable state - 'clinical_source', # Clinical data source (ExperimentClinicalSourceSerializer) - 'mrna_source', # mRNA data source (ExperimentSourceSerializer) - 'is_public', # Public visibility flag - 'tool' - ] - read_only_fields = [ - 'id', 'created_at', 'state', 'state_display' - ] - - -class DifferentialExpressionExperimentSerializer(serializers.ModelSerializer): - """ - Serializer for Differential Expression Experiment (General view with all fields). - """ - user = UserSimpleForDiffExpExperiments(read_only=True) - clinical_source = ExperimentClinicalSourceSerializer(read_only=True) - mrna_source = ExperimentSourceSerializer(read_only=True) - - # Computed fields - has_results = serializers.SerializerMethodField(method_name='get_has_results') - results_count = serializers.SerializerMethodField(method_name='get_results_count') - significant_genes_count = serializers.SerializerMethodField(method_name='get_significant_genes_count') - state_display = serializers.CharField(source='get_state_display', read_only=True) - - class Meta: - model = DifferentialExpressionExperiment - fields = [ - 'id', 'name', 'description', 'user', 'clinical_source', 'mrna_source', - 'clinical_attribute', 'tool', 'threshold_percentile', 'threshold', 'top', 'state', 'state_display', - 'execution_time', 'created_at', 'updated_at', 'is_public', - 'has_results', 'results_count', 'significant_genes_count' - ] - read_only_fields = [ - 'id', 'execution_time', 'created_at', 'updated_at', - 'has_results', 'results_count', 'significant_genes_count' - ] - - @staticmethod - def get_has_results(obj: DifferentialExpressionExperiment) -> bool: - """Check if the experiment has results.""" - return obj.results.exists() - - @staticmethod - def get_results_count(obj: DifferentialExpressionExperiment) -> int: - """Get the total number of genes in results.""" - return obj.results.count() - - @staticmethod - def get_significant_genes_count(obj: DifferentialExpressionExperiment) -> int: - """Get the number of significant genes (adj.P.Val <= 0.05 and |logFC| >= 1.0).""" - return obj.get_significant_genes().count() - - -class DifferentialExpressionExperimentDetailSerializer(serializers.ModelSerializer): - """ - Detailed serializer for Differential Expression Experiment (Detail view). - Includes additional information like shared users and institutions. - """ - - class Meta:#DifferentialExpressionExperimentSerializer.Meta): - fields = '__all__' - model = DifferentialExpressionExperimentResult \ No newline at end of file diff --git a/src/differential_expression/service.py b/src/differential_expression/service.py deleted file mode 100644 index 6480e209..00000000 --- a/src/differential_expression/service.py +++ /dev/null @@ -1,359 +0,0 @@ -from common.exceptions import EmptyDataset -from common.typing import AbortEvent -from differential_expression.models import DifferentialExpressionExperiment -import logging -import warnings -from itertools import combinations - -import numpy as np -import pandas as pd - -# Filter R warnings BEFORE importing rpy2 -warnings.filterwarnings('ignore', - message='Environment variable "XPC_SERVICE_NAME" redefined by R', - category=UserWarning) -warnings.filterwarnings('ignore', - message='Environment variable "R_SESSION_TMPDIR" redefined by R and overriding existing variable.', - category=UserWarning) - -import rpy2.robjects as robjects -from rpy2.robjects import pandas2ri -from rpy2.robjects.conversion import get_conversion, localconverter -from rpy2.robjects.pandas2ri import converter -from rpy2.robjects.packages import importr - -class DifferentialExpressionService: - def __init__(self, experiment: DifferentialExpressionExperiment, is_aborted: AbortEvent): - self.experiment = experiment - self.is_aborted = is_aborted - - def perform_differential_expression(self) -> pd.DataFrame: - clinical_df, mrna_df = self._process_datasets() - - sample_column = 'SAMPLE_ID' - if sample_column in clinical_df.columns: - common_samples = sorted(set(mrna_df.columns) & set(clinical_df[sample_column])) - - # Check if there are samples in common - if len(common_samples) == 0: - from differential_expression.models import DifferentialExpressionExperimentState - raise ValueError("NO_SAMPLES_IN_COMMON") - - df_RNAseq_filtered = mrna_df[common_samples] - df_clinical_filtered = clinical_df[clinical_df[sample_column].isin(common_samples)] - df_clinical_filtered = df_clinical_filtered.set_index(sample_column).reindex(common_samples).reset_index() - else: - df_RNAseq_filtered = mrna_df - df_clinical_filtered = clinical_df - - # Check if we have any features (genes) left after filtering - if df_RNAseq_filtered.empty or df_RNAseq_filtered.shape[0] == 0: - raise ValueError("NO_FEATURES_FOUND") - - try: - # Import R packages - limma = importr('limma') # limma for differential expression - stats = importr('stats') # stats for model matrix - base = importr('base') # base R functions - - # 1. Validation: Ensure the clinical attribute has at least two categories - # This is necessary because differential expression requires at least two groups to compare. - unique_values = df_clinical_filtered[self.experiment.clinical_attribute].dropna().unique() - if len(unique_values) < 2: - raise ValueError(f"At least two categories are required in '{self.experiment.clinical_attribute}'.") - - # 2. Group vector creation - # Converts the clinical attribute to a categorical variable (factor in R). - # This tells the model to treat the values as groups, not as numeric values. - group_values = df_clinical_filtered[self.experiment.clinical_attribute].astype(str).values - group_levels = sorted(np.unique(group_values)) # Get all unique group names sorted - group_dict = {k: i for i, k in enumerate(group_levels)} # Map group names to indices (not strictly needed) - group_factor = pd.Categorical(group_values, categories=group_levels) - - # 3. Conversion to R objects - # Converts the filtered log-expression data (Pandas DataFrame) to an R matrix. - with localconverter(get_conversion() + converter): - r_log_data = pandas2ri.py2rpy(df_RNAseq_filtered) - # Convert the group factor to an R factor vector - r_group = robjects.FactorVector(group_factor) - - # 4. Design matrix construction (no intercept) - # The design matrix encodes the group structure for the linear model. - # Using '~ 0 + group' means no intercept: each group gets its own column. - formula = robjects.Formula('~ 0 + group') - env = robjects.Environment() - env['group'] = r_group - r_design = stats.model_matrix(formula, env) - r_design.colnames = robjects.StrVector(group_levels) # Set column names to group names - - # 5. Linear model fitting with limma - # Fit the linear model to estimate mean expression for each gene in each group. - fit = limma.lmFit(r_log_data, r_design) - - # 6. Create all possible pairwise contrasts (all-vs-all) - # For each pair of groups, create a contrast expression like 'groupB - groupA'. - pares = list(combinations(group_levels, 2)) - contrastes = [f"{b} - {a}" for a, b in pares] - contrast_matrix = limma.makeContrasts( - contrasts=robjects.StrVector(contrastes), - levels=r_design - ) - - # 7. Apply contrasts and empirical Bayes moderation - # Apply the contrasts to the fitted model, then use eBayes to stabilize variance estimates. - fit2 = limma.contrasts_fit(fit, contrast_matrix) - fit2 = limma.eBayes(fit2) - - # 8. Extract results for the first contrast - # Get the table of differential expression results for the first contrast (logFC, p-value, adjusted p-value, etc.). - results = limma.topTable( - fit2, - coef=1, # First contrast - number=robjects.r('Inf'), # All genes - adjust_method="BH" # Benjamini-Hochberg adjustment - ) - - # Convert the R data frame to a Pandas DataFrame for further analysis in Python. - results_df = pandas2ri.rpy2py(results) - - # Return top genes sorted by adjusted p-value, keeping gene names as index - top_genes = results_df.nsmallest(self.experiment.top, 'adj.P.Val') - - return top_genes - - except Exception as e: - logging.error(f"Error occurred during differential expression analysis: {e}") - raise - - def _process_datasets(self): - """Process clinical and mRNA datasets for differential expression analysis.""" - clinical_df = self.experiment.clinical_source.get_df() - mrna_df = self.experiment.mrna_source.get_df() - - if clinical_df.empty or mrna_df.empty: - raise EmptyDataset("One or both datasets are empty.") - - data_processing_service = DataProcessingService( - clinical_df=clinical_df, - mrna_df=mrna_df, - clinical_attribute=self.experiment.clinical_attribute, - threshold_percentile=self.experiment.threshold_percentile, - threshold=self.experiment.threshold - ) - - return data_processing_service.process_datasets() - - -class DataProcessingService: - """ - Service for processing clinical and RNA-Seq datasets. - """ - - def __init__(self, - clinical_df: pd.DataFrame, - mrna_df: pd.DataFrame, - clinical_attribute: str, - threshold_percentile: float = 0.15, - threshold : float = 1e-4): - - self.clinical_df = clinical_df - self.mrna_df = mrna_df - self.clinical_attribute = clinical_attribute - self.threshold_percentile = threshold_percentile - self.threshold = threshold - - def validate_datasets(self): - """Validate the clinical and mRNA datasets. - - Raises: - EmptyDataset: If either dataset is empty. - """ - if self.clinical_df.empty: - raise EmptyDataset("Clinical dataset is empty.") - if self.mrna_df.empty: - raise EmptyDataset("mRNA dataset is empty.") - - def _process_clinical_data(self) -> None: - """Process the clinical dataset. - This method filters the clinical dataset to keep only the relevant columns, - removes rows with NA values in the clinical attribute, and ensures that - the clinical attribute is in uppercase if it is of string type. - """ - - ###### Procesar datos clínicos ##### - # Reset index para convertir PATIENT_ID de índice a columna - clinical_df_reset = self.clinical_df.reset_index() - - # Create sample dataframe with SAMPLE_ID and PATIENT_ID - df_clinical_sample = clinical_df_reset[['SAMPLE_ID', 'PATIENT_ID']] - - # Create patient dataframe with PATIENT_ID and clinical attribute, removing NA values - df_clinical_patient = clinical_df_reset[['PATIENT_ID', self.clinical_attribute]].dropna(subset=[self.clinical_attribute]) - - # Si la columna es de tipo texto, limpiar y convertir a mayúsculas - if df_clinical_patient[self.clinical_attribute].dtype == 'object': - # Eliminar espacios al inicio y al final, luego convertir a mayúsculas - df_clinical_patient[self.clinical_attribute] = df_clinical_patient[self.clinical_attribute].astype( - str).str.strip().str.upper() - - # Eliminar filas duplicadas - df_clinical_patient = df_clinical_patient.drop_duplicates() - - ##### Procesar datos de muestra ##### - # df_clinical_sample = self.clinical_df[['SAMPLE_ID', 'PATIENT_ID']] - - ##### Fusionar datos clínicos y de muestra ##### - df_clinical = pd.merge(df_clinical_sample, df_clinical_patient, on='PATIENT_ID', how='inner') - df_clinical = df_clinical[['SAMPLE_ID', self.clinical_attribute]] - - df_clinical = df_clinical.drop_duplicates() - df_clinical['SAMPLE_ID'] = df_clinical['SAMPLE_ID'].str.replace('-', '.') - - self.clinical_df = df_clinical - logging.info("Clinical data processed successfully.") - - - def _process_mrna_data(self) -> None: - """Process the mRNA dataset. - This method processes the mRNA dataset by renaming columns, removing duplicates, - filtering samples based on the clinical dataset, and cleaning up NA values. - """ - - mrna_dataset = self.mrna_df.copy() - - mrna_dataset.reset_index(inplace=True) - - # SOLUCIÓN: Reemplazar guiones por puntos en los nombres de las columnas sino no puede machear con SAMPLE_ID - columns_to_rename = {} - for col in mrna_dataset.columns: - if col not in 'Standard_Symbol': - columns_to_rename[col] = col.replace('-', '.') - - mrna_dataset = mrna_dataset.rename(columns=columns_to_rename) - - mrna_dataset = mrna_dataset.drop_duplicates(subset=['Standard_Symbol'], keep='first') - - # # Si Standard_Symbol está como índice, convertirlo a columna - # if mrna_dataset.index.name == 'Standard_Symbol' in str(mrna_dataset.index.name): - # mrna_dataset = mrna_dataset.reset_index() - - # Si Standard_Symbol no está como columna pero está en el índice - # if 'Standard_Symbol' not in mrna_dataset.columns and mrna_dataset.index.name is not None: - # mrna_dataset = mrna_dataset.reset_index() - # # Renombrar la primera columna a Standard_Symbol si es necesario - # if mrna_dataset.columns[0] != 'Standard_Symbol': - # mrna_dataset = mrna_dataset.rename(columns={mrna_dataset.columns[0]: 'Standard_Symbol'}) - - - # Eliminar duplicados manteniendo la primera ocurrencia - # mrna_dataset = self.mrna_df.drop_duplicates(subset=['Standard_Symbol'], keep='first') - - # Antes de la intersección, normalizar los SAMPLE_ID extrayendo solo la parte base - self.clinical_df['SAMPLE_ID'] = self.clinical_df['SAMPLE_ID'].str.rsplit('.', n=1).str[0] - - # Normalizar también las columnas del mrna_dataset para machear con SAMPLE_ID - # Crear un mapeo de columnas normalizadas a columnas originales - normalized_columns = {} - for col in mrna_dataset.columns: - if col != 'Standard_Symbol': - # Extraer la parte base del nombre de la columna - normalized_col = col.rsplit('.', 1)[0] if '.' in col else col - normalized_columns[col] = normalized_col - - # Obtener valores de SAMPLE_ID del DataFrame de metadatos - valid_sample_ids = set(self.clinical_df['SAMPLE_ID']) - valid_columns = [col for col, norm_col in normalized_columns.items() if norm_col in valid_sample_ids] - - # Filtrar el dataset de RNA-Seq para mantener solo las columnas válidas - mrna_dataset = mrna_dataset[['Standard_Symbol'] + valid_columns] - - # Renombrar las columnas para que coincidan con los SAMPLE_ID normalizados - rename_mapping = {col: normalized_columns[col] for col in valid_columns} - mrna_dataset = mrna_dataset.rename(columns=rename_mapping) - - # Eliminar filas con NA en los datos de expresión génica - # Eliminar filas donde Standard_Symbol es NA - mrna_dataset = mrna_dataset.dropna(subset=['Standard_Symbol']) - - # Establecer Standard_Symbol como índice con el nombre Hugo_Symbol y eliminar la columna - mrna_dataset = mrna_dataset.set_index('Standard_Symbol') - mrna_dataset.index.name = 'Hugo_Symbol' - - self.mrna_df = mrna_dataset - logging.info("mRNA data processed successfully.") - - def _transform_to_log2(self) -> None: - """Transform mRNA data to log2 scale. - This method applies a log2 transformation to the mRNA dataset. - """ - - if isinstance(self.mrna_df, pd.DataFrame): - has_negative = (self.mrna_df < 0).any().any() - else: - has_negative = np.any(self.mrna_df < 0) - - if has_negative: - logging.warning("Negative values found in mRNA dataset. Skipping log2 transformation.") - - # Apply log2 transformation - mrna_df_log2 = np.log2(self.mrna_df + 1) - - self.mrna_df = mrna_df_log2 - logging.info("Log2 transformation applied to mRNA data.") - - def _filter_low_expression_genes(self) -> None: - """Filter out low-expression genes from the mRNA dataset. - This method removes genes whose maximum expression across samples is below - a specified percentile threshold. - """ - - # Calculate the average expression for each gene (across all samples) - avg_expression = self.mrna_df.mean(axis=1) - - # Calculate the threshold based on the percentile - threshold = np.percentile(avg_expression, self.threshold_percentile * 100) - - # Filter genes with average expression above or equal to the threshold - genes_to_keep = avg_expression >= threshold - self.mrna_df = self.mrna_df[genes_to_keep] - - self.mrna_df = self.mrna_df - logging.info(f"Filtered low-expression genes. Threshold: {threshold:.2f}") - - - def _filter_genes_by_variance(self) -> None: - """Filter genes based on variance. - This method removes genes whose variance across samples is below - a specified percentile threshold. - """ - - # Calculate variance for each gene (row) - variances = self.mrna_df.var(axis=1) - - # Filter out genes with variance below the threshold - genes_to_keep = variances > self.threshold - self.mrna_df = self.mrna_df[genes_to_keep] - - # Check if we still have features after filtering - if self.mrna_df.empty or self.mrna_df.shape[0] == 0: - raise ValueError("NO_FEATURES_FOUND") - - logging.info(f"Filtered genes by variance. Threshold: {self.threshold:.2f}") - - - def process_datasets(self) -> tuple[pd.DataFrame, pd.DataFrame]: - """Process the clinical and mRNA datasets. - This method orchestrates the processing steps for both datasets. - - Returns: - Tuple containing the processed clinical and mRNA datasets. - """ - - self.validate_datasets() - self._process_clinical_data() - self._process_mrna_data() - self._transform_to_log2() - self._filter_low_expression_genes() - self._filter_genes_by_variance() - - return self.clinical_df, self.mrna_df \ No newline at end of file diff --git a/src/differential_expression/tasks.py b/src/differential_expression/tasks.py deleted file mode 100644 index 4f17819e..00000000 --- a/src/differential_expression/tasks.py +++ /dev/null @@ -1,117 +0,0 @@ -import logging -import time - -from celery.contrib.abortable import AbortableTask -from celery.exceptions import SoftTimeLimitExceeded -from django.conf import settings -from multiomics_intermediate.celery import app - -from common.exceptions import EmptyDataset, ExperimentStopped -from differential_expression.models import ( - DifferentialExpressionExperiment, - DifferentialExpressionExperimentState, -) -from .service import DifferentialExpressionService - -@app.task(bind=True, base=AbortableTask, acks_late=True, reject_on_worker_lost=True, - soft_time_limit=settings.FS_SOFT_TIME_LIMIT) -def eval_differential_expression_experiment(self, experiment_pk: int, ): - """Evaluate differential expression for a given experiment. - - @param self: Self instance of the Celery task (available due to bind=True). - @param experiment_pk: Primary key of the experiment to evaluate. - """ - # Check if the experiment exists - try: - experiment : DifferentialExpressionExperiment = DifferentialExpressionExperiment.objects.get(pk=experiment_pk) - except DifferentialExpressionExperiment.DoesNotExist: - logging.error(f'DifferentialExpressionExperiment {experiment_pk} does not exist') - return - - # Check if the experiment has reached the limit of attempts - if experiment.attempt >= 3: - logging.warning(f'DifferentialExpressionExperiment {experiment.pk} has reached attempts limit.') - experiment.state = DifferentialExpressionExperimentState.REACHED_ATTEMPTS_LIMIT - experiment.save(update_fields=['state']) - return - - # Increment the attempt and set the state of the experiment to IN_PROCESS - experiment.attempt += 1 - experiment.state = DifferentialExpressionExperimentState.IN_PROCESS - experiment.save(update_fields=['attempt', 'state']) - - try: - logging.warning(f'Starting evaluation for DifferentialExpressionExperiment ID -> {experiment.pk}') - - # Compute the differential expression experiment - start = time.time() - - compute_differential_expression = DifferentialExpressionService(experiment, is_aborted=self.is_aborted) - result = compute_differential_expression.perform_differential_expression() - - total_execution_time = time.time() - start - logging.info(f'DifferentialExpressionExperiment {experiment.pk} processed in {total_execution_time:.2f} seconds.') - - # If user cancel the experiment, discard changes - if self.is_aborted(): - experiment.state = DifferentialExpressionExperimentState.STOPPING - experiment.save(update_fields=['state']) - raise ExperimentStopped - - # Save the results to the database if we got results - if result is not None: - experiment.save_results(result) - logging.info(f'Results saved for DifferentialExpressionExperiment {experiment.pk}') - - experiment.execution_time = total_execution_time - experiment.save(update_fields=['execution_time']) - - experiment.state = DifferentialExpressionExperimentState.COMPLETED - experiment.save(update_fields=['state']) - - except EmptyDataset: - logging.error(f'Empty dataset error for DifferentialExpressionExperiment {experiment.pk}') - experiment.state = DifferentialExpressionExperimentState.EMPTY_DATASET - experiment.save(update_fields=['state']) - return - - except SoftTimeLimitExceeded as e: - # If celery soft time limit is exceeded, sets the experiment as TIMEOUT_EXCEEDED - logging.warning(f'DifferentialExpressionExperiment {experiment.pk} has exceeded the soft time limit') - logging.exception(e) - experiment.state = DifferentialExpressionExperimentState.TIMEOUT_EXCEEDED - experiment.save(update_fields=['state']) - return - - except ValueError as e: - error_msg = str(e) - if error_msg == "NO_SAMPLES_IN_COMMON": - logging.error(f'No samples in common for DifferentialExpressionExperiment {experiment.pk}') - experiment.state = DifferentialExpressionExperimentState.NO_SAMPLES_IN_COMMON - experiment.save(update_fields=['state']) - return - elif error_msg == "NO_FEATURES_FOUND": - logging.error(f'No features found after filtering for DifferentialExpressionExperiment {experiment.pk}') - experiment.state = DifferentialExpressionExperimentState.NO_FEATURES_FOUND - experiment.save(update_fields=['state']) - return - else: - # Handle other ValueError cases - logging.error(f'ValueError during evaluation of DifferentialExperimentExperiment {experiment.pk}: {e}') - experiment.state = DifferentialExpressionExperimentState.FINISHED_WITH_ERROR - experiment.save(update_fields=['state']) - raise e - - except Exception as e: - logging.error(f'Error during evaluation of DifferentialExpressionExperiment {experiment.pk}: {e}') - experiment.state = DifferentialExpressionExperimentState.FINISHED_WITH_ERROR - experiment.save(update_fields=['state']) - raise e - finally: - # Ensure that the experiment is marked as finished - if self.is_aborted(): - logging.info(f'Evaluation for DifferentialExpressionExperiment {experiment.pk} was aborted.') - experiment.state = DifferentialExpressionExperimentState.STOPPED - experiment.save(update_fields=['state']) - else: - logging.info(f'Evaluation for DifferentialExpressionExperiment {experiment.pk} completed successfully.') \ No newline at end of file diff --git a/src/differential_expression/tests/__init__.py b/src/differential_expression/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/differential_expression/tests/test_integration.py b/src/differential_expression/tests/test_integration.py deleted file mode 100644 index 2f0ca64a..00000000 --- a/src/differential_expression/tests/test_integration.py +++ /dev/null @@ -1,224 +0,0 @@ -""" -Integration tests for differential expression analysis. -These tests execute the full pipeline including the Celery task and R/limma analysis. -""" -from unittest.mock import patch -from django.test import TestCase, override_settings -from django.contrib.auth.models import User -from common.tests_utils import create_user_file -from differential_expression.models import ( - DifferentialExpressionExperiment, - DifferentialExpressionExperimentState, - DifferentialExpressionExperimentResult -) -from differential_expression.tests.test_utils import ( - get_test_file_path, - create_differential_expression_source, - create_differential_expression_clinical_source, - create_test_differential_expression_experiment -) -from differential_expression.tasks import eval_differential_expression_experiment -from user_files.models_choices import FileType - - -def mock_is_aborted(): - """Mock is_aborted to always return False for testing.""" - return False - - -@override_settings( - CELERY_TASK_ALWAYS_EAGER=True, - CELERY_TASK_EAGER_PROPAGATES=True -) -@patch.object(eval_differential_expression_experiment, 'is_aborted', mock_is_aborted) -class DifferentialExpressionIntegrationTestCase(TestCase): - """ - Integration tests that execute the full differential expression pipeline. - Uses CELERY_TASK_ALWAYS_EAGER to run Celery tasks synchronously. - """ - - def setUp(self): - """Test setup""" - # Create test user - self.user = User.objects.create_user( - username='testuser', - email='test@test.com', - password='testpass123' - ) - - # Create test files with real TCGA data - self.mrna_file = create_user_file( - get_test_file_path('mrna_test.csv'), - 'mRNA Test', - FileType.MRNA, - self.user - ) - self.clinical_file = create_user_file( - get_test_file_path('clinical_test.csv'), - 'Clinical Test', - FileType.CLINICAL, - self.user - ) - - # Create sources - self.mrna_source = create_differential_expression_source(self.mrna_file) - self.clinical_source = create_differential_expression_clinical_source(self.clinical_file) - - def test_full_differential_expression_analysis_by_sex(self): - """ - Test the complete differential expression analysis pipeline. - Analyzes gene expression differences between Male and Female samples. - """ - # Create experiment with SEX as the clinical attribute - experiment = create_test_differential_expression_experiment( - name='Integration Test - Sex Analysis', - clinical_source=self.clinical_source, - mrna_source=self.mrna_source, - user=self.user, - clinical_attribute='SEX', - state=DifferentialExpressionExperimentState.WAITING_FOR_QUEUE - ) - - # Verify initial state - self.assertEqual(experiment.state, DifferentialExpressionExperimentState.WAITING_FOR_QUEUE) - self.assertEqual(experiment.attempt, 0) - - # Execute the Celery task synchronously - eval_differential_expression_experiment(experiment.pk) - - # Refresh from database - experiment.refresh_from_db() - - # Verify the experiment completed successfully - self.assertEqual( - experiment.state, - DifferentialExpressionExperimentState.COMPLETED, - f"Experiment should be COMPLETED but is {experiment.get_state_display()}" - ) - self.assertEqual(experiment.attempt, 1) - self.assertGreater(experiment.execution_time, 0) - - # Verify results were saved - results_count = experiment.results.count() - self.assertGreater(results_count, 0, "Should have differential expression results") - - # Verify result structure - first_result = experiment.results.first() - self.assertIsNotNone(first_result.gene) - self.assertIsNotNone(first_result.p_value) - self.assertIsNotNone(first_result.adj_p_val) - self.assertIsNotNone(first_result.log_fc) - - def test_differential_expression_results_dataframe(self): - """ - Test that results can be retrieved as a pandas DataFrame. - """ - # Create and run experiment - experiment = create_test_differential_expression_experiment( - name='Integration Test - DataFrame Results', - clinical_source=self.clinical_source, - mrna_source=self.mrna_source, - user=self.user, - clinical_attribute='SEX', - state=DifferentialExpressionExperimentState.WAITING_FOR_QUEUE - ) - - # Execute the task - eval_differential_expression_experiment(experiment.pk) - - # Refresh and get results as DataFrame - experiment.refresh_from_db() - results_df = experiment.get_results_dataframe() - - # Verify DataFrame structure - self.assertIsNotNone(results_df) - self.assertGreater(len(results_df), 0) - - # Check expected columns - expected_columns = ['AveExpr', 'P.Value', 'adj.P.Val', 'logFC', 't', 'B'] - for col in expected_columns: - self.assertIn(col, results_df.columns, f"Missing column: {col}") - - # Verify genes are in the index - self.assertTrue(len(results_df.index) > 0, "DataFrame should have genes as index") - - def test_differential_expression_significant_genes(self): - """ - Test filtering for significant differentially expressed genes. - """ - # Create and run experiment - experiment = create_test_differential_expression_experiment( - name='Integration Test - Significant Genes', - clinical_source=self.clinical_source, - mrna_source=self.mrna_source, - user=self.user, - clinical_attribute='SEX', - state=DifferentialExpressionExperimentState.WAITING_FOR_QUEUE - ) - - # Execute the task - eval_differential_expression_experiment(experiment.pk) - - # Refresh from database - experiment.refresh_from_db() - self.assertEqual(experiment.state, DifferentialExpressionExperimentState.COMPLETED) - - # Test get_significant_genes with relaxed thresholds for test data - significant_genes = experiment.get_significant_genes( - p_value_threshold=1.0, # Relaxed for small test dataset - log_fc_threshold=0.0 - ) - - # Should return some results (we use relaxed thresholds) - self.assertIsNotNone(significant_genes) - - def test_experiment_state_transitions(self): - """ - Test that experiment goes through correct state transitions. - """ - # Create experiment - experiment = create_test_differential_expression_experiment( - name='Integration Test - State Transitions', - clinical_source=self.clinical_source, - mrna_source=self.mrna_source, - user=self.user, - clinical_attribute='SEX', - state=DifferentialExpressionExperimentState.WAITING_FOR_QUEUE - ) - - # Initial state - self.assertEqual(experiment.state, DifferentialExpressionExperimentState.WAITING_FOR_QUEUE) - - # Execute task - eval_differential_expression_experiment(experiment.pk) - - # Final state should be COMPLETED - experiment.refresh_from_db() - self.assertEqual(experiment.state, DifferentialExpressionExperimentState.COMPLETED) - - # Attempt should be incremented - self.assertEqual(experiment.attempt, 1) - - def test_experiment_execution_time_recorded(self): - """ - Test that execution time is recorded after analysis. - """ - # Create experiment - experiment = create_test_differential_expression_experiment( - name='Integration Test - Execution Time', - clinical_source=self.clinical_source, - mrna_source=self.mrna_source, - user=self.user, - clinical_attribute='SEX', - state=DifferentialExpressionExperimentState.WAITING_FOR_QUEUE - ) - - # Initial execution time should be 0 - self.assertEqual(experiment.execution_time, 0.0) - - # Execute task - eval_differential_expression_experiment(experiment.pk) - - # Execution time should be recorded - experiment.refresh_from_db() - self.assertGreater(experiment.execution_time, 0) \ No newline at end of file diff --git a/src/differential_expression/tests/test_models.py b/src/differential_expression/tests/test_models.py deleted file mode 100644 index 6a312dfe..00000000 --- a/src/differential_expression/tests/test_models.py +++ /dev/null @@ -1,294 +0,0 @@ -from django.test import TestCase -from django.contrib.auth.models import User -from common.tests_utils import create_user_file -from differential_expression.models import ( - DifferentialExpressionExperiment, - DifferentialExpressionExperimentState, - DifferentialExpressionExperimentResult -) -from differential_expression.tests.test_utils import ( - get_test_file_path, - create_differential_expression_source, - create_differential_expression_clinical_source, - create_test_differential_expression_experiment -) -from user_files.models_choices import FileType -import pandas as pd - - -class DifferentialExpressionExperimentModelTestCase(TestCase): - """Tests for DifferentialExpressionExperiment model""" - - def setUp(self): - """Test setup""" - # Create test user - self.user = User.objects.create_user( - username='testuser', - email='test@test.com', - password='testpass123' - ) - - # Create test files - self.mrna_file = create_user_file( - get_test_file_path('mrna_test.csv'), - 'mRNA Test', - FileType.MRNA, - self.user - ) - self.clinical_file = create_user_file( - get_test_file_path('clinical_test.csv'), - 'Clinical Test', - FileType.CLINICAL, - self.user - ) - - # Create sources - self.mrna_source = create_differential_expression_source(self.mrna_file) - self.clinical_source = create_differential_expression_clinical_source(self.clinical_file) - - def test_create_experiment(self): - """Test creating a differential expression experiment""" - experiment = create_test_differential_expression_experiment( - name='Test Experiment', - clinical_source=self.clinical_source, - mrna_source=self.mrna_source, - user=self.user - ) - - self.assertIsNotNone(experiment.id) - self.assertEqual(experiment.name, 'Test Experiment') - self.assertEqual(experiment.state, DifferentialExpressionExperimentState.WAITING_FOR_QUEUE) - self.assertEqual(experiment.user, self.user) - - def test_experiment_str_representation(self): - """Test string representation of experiment""" - experiment = create_test_differential_expression_experiment( - name='Test Experiment', - clinical_source=self.clinical_source, - mrna_source=self.mrna_source, - user=self.user - ) - - self.assertEqual( - str(experiment), - 'Differential Expression Experiment: Test Experiment' - ) - - def test_experiment_default_values(self): - """Test experiment default values""" - experiment = create_test_differential_expression_experiment( - name='Test Experiment', - clinical_source=self.clinical_source, - mrna_source=self.mrna_source, - user=self.user - ) - - self.assertEqual(experiment.threshold_percentile, 0.15) - self.assertEqual(experiment.threshold, 0.0001) - self.assertEqual(experiment.top, 100) - self.assertEqual(experiment.attempt, 0) - self.assertEqual(experiment.execution_time, 0.0) - self.assertFalse(experiment.is_public) - - def test_save_and_retrieve_results(self): - """Test saving and retrieving differential expression results""" - experiment = create_test_differential_expression_experiment( - name='Test Experiment', - clinical_source=self.clinical_source, - mrna_source=self.mrna_source, - user=self.user, - state=DifferentialExpressionExperimentState.COMPLETED - ) - - # Create test results dataframe - results_data = { - 'AveExpr': [5.2, 6.1, 7.3], - 'P.Value': [0.001, 0.05, 0.0001], - 'adj.P.Val': [0.01, 0.1, 0.001], - 'logFC': [2.5, -1.8, 3.2], - 't': [4.5, -3.2, 5.1], - 'B': [2.1, 1.5, 3.2] - } - results_df = pd.DataFrame(results_data, index=['GENE_1', 'GENE_2', 'GENE_3']) - - # Save results - experiment.save_results(results_df) - - # Retrieve results - retrieved_df = experiment.get_results_dataframe() - - # Assert results were saved correctly - self.assertIsNotNone(retrieved_df) - self.assertEqual(len(retrieved_df), 3) - self.assertEqual(set(retrieved_df.index), {'GENE_1', 'GENE_2', 'GENE_3'}) - - # Check specific values - self.assertAlmostEqual(retrieved_df.loc['GENE_1', 'AveExpr'], 5.2, places=1) - self.assertAlmostEqual(retrieved_df.loc['GENE_1', 'logFC'], 2.5, places=1) - - def test_get_significant_genes(self): - """Test getting significant genes""" - experiment = create_test_differential_expression_experiment( - name='Test Experiment', - clinical_source=self.clinical_source, - mrna_source=self.mrna_source, - user=self.user, - state=DifferentialExpressionExperimentState.COMPLETED - ) - - # Create test results - DifferentialExpressionExperimentResult.objects.create( - experiment=experiment, - gene='GENE_1', - ave_expr=5.2, - p_value=0.001, - adj_p_val=0.01, - log_fc=2.5, - t_statistic=4.5, - b_statistic=2.1 - ) - DifferentialExpressionExperimentResult.objects.create( - experiment=experiment, - gene='GENE_2', - ave_expr=6.1, - p_value=0.05, - adj_p_val=0.1, - log_fc=-0.5, - t_statistic=-1.2, - b_statistic=1.5 - ) - DifferentialExpressionExperimentResult.objects.create( - experiment=experiment, - gene='GENE_3', - ave_expr=7.3, - p_value=0.0001, - adj_p_val=0.001, - log_fc=3.2, - t_statistic=5.1, - b_statistic=3.2 - ) - - # Get significant genes with default thresholds (p_value <= 0.05, |log_fc| >= 1.0) - significant_genes = experiment.get_significant_genes( - p_value_threshold=0.05, - log_fc_threshold=1.0 - ) - - # Should return GENE_1 and GENE_3 (both have adj_p_val <= 0.05 and |log_fc| >= 1.0) - self.assertEqual(significant_genes.count(), 2) - gene_names = [result.gene for result in significant_genes] - self.assertIn('GENE_1', gene_names) - self.assertIn('GENE_3', gene_names) - - def test_delete_experiment_cascades(self): - """Test that deleting experiment cascades to results""" - experiment = create_test_differential_expression_experiment( - name='Test Experiment', - clinical_source=self.clinical_source, - mrna_source=self.mrna_source, - user=self.user, - state=DifferentialExpressionExperimentState.COMPLETED - ) - - # Create test results - DifferentialExpressionExperimentResult.objects.create( - experiment=experiment, - gene='GENE_1', - ave_expr=5.2, - p_value=0.001, - adj_p_val=0.01, - log_fc=2.5, - t_statistic=4.5, - b_statistic=2.1 - ) - - experiment_id = experiment.id - - # Delete experiment - experiment.delete() - - # Assert experiment and results were deleted - self.assertFalse( - DifferentialExpressionExperiment.objects.filter(pk=experiment_id).exists() - ) - self.assertFalse( - DifferentialExpressionExperimentResult.objects.filter( - experiment_id=experiment_id - ).exists() - ) - - -class DifferentialExpressionSourceModelTestCase(TestCase): - """Tests for DifferentialExpressionSource models""" - - def setUp(self): - """Test setup""" - # Create test user - self.user = User.objects.create_user( - username='testuser', - email='test@test.com', - password='testpass123' - ) - - # Create test files - self.mrna_file = create_user_file( - get_test_file_path('mrna_test.csv'), - 'mRNA Test', - FileType.MRNA, - self.user - ) - self.clinical_file = create_user_file( - get_test_file_path('clinical_test.csv'), - 'Clinical Test', - FileType.CLINICAL, - self.user - ) - - def test_create_mrna_source(self): - """Test creating an mRNA source""" - source = create_differential_expression_source(self.mrna_file) - - self.assertIsNotNone(source.id) - self.assertEqual(source.user_file, self.mrna_file) - - def test_create_clinical_source(self): - """Test creating a clinical source""" - source = create_differential_expression_clinical_source(self.clinical_file) - - self.assertIsNotNone(source.id) - self.assertEqual(source.user_file, self.clinical_file) - - def test_get_samples_from_mrna_source(self): - """Test getting samples from mRNA source""" - source = create_differential_expression_source(self.mrna_file) - - samples = source.get_samples() - - # Based on mrna_test.csv (6 TCGA samples as columns) - self.assertEqual(len(samples), 6) - self.assertIn('TCGA-OR-A5J1-01', samples) - self.assertIn('TCGA-OR-A5J8-01', samples) - - def test_get_samples_from_clinical_source(self): - """Test getting samples from clinical source""" - source = create_differential_expression_clinical_source(self.clinical_file) - - samples = source.get_samples() - - # Based on clinical_test.csv (6 TCGA samples as row indices - PATIENT_ID) - self.assertEqual(len(samples), 6) - self.assertIn('TCGA-OR-A5J1', samples) - self.assertIn('TCGA-OR-A5J8', samples) - - def test_get_clinical_attributes(self): - """Test getting clinical attributes""" - source = create_differential_expression_clinical_source(self.clinical_file) - - attributes = source.get_attributes() - - # Based on clinical_test.csv (OTHER_PATIENT_ID, SEX, OS_STATUS, OS_MONTHS, SAMPLE_ID, OTHER_SAMPLE_ID) - self.assertEqual(len(attributes), 6) - self.assertIn('SEX', attributes) - self.assertIn('OS_STATUS', attributes) - self.assertIn('OS_MONTHS', attributes) - self.assertIn('SAMPLE_ID', attributes) diff --git a/src/differential_expression/tests/test_utils.py b/src/differential_expression/tests/test_utils.py deleted file mode 100644 index dbd59d63..00000000 --- a/src/differential_expression/tests/test_utils.py +++ /dev/null @@ -1,90 +0,0 @@ -import os -from typing import Optional -from django.contrib.auth.models import User -from api_service.models import ExperimentSource, ExperimentClinicalSource -from common.tests_utils import create_user_file -from differential_expression.models import ( - DifferentialExpressionExperiment, - DifferentialExpressionExperimentState, - DifferentialExpressionTool -) -from user_files.models import UserFile -from user_files.models_choices import FileType - - -def get_test_file_path(filename: str) -> str: - """ - Gets the absolute file's path in test folder - @param filename: File's name - @return: Absolute path to the file in test folder - """ - dir_name = os.path.dirname(__file__) - file_path = os.path.join(dir_name, f'tests_files/{filename}') - return file_path - - -def create_differential_expression_source(user_file: UserFile) -> ExperimentSource: - """ - Creates a new instance of ExperimentSource saved in DB - @param user_file: UserFile to create the ExperimentSource - @return: ExperimentSource saved instance - """ - source = ExperimentSource.objects.create(user_file=user_file) - return source - - -def create_differential_expression_clinical_source(user_file: UserFile) -> ExperimentClinicalSource: - """ - Creates a new instance of ExperimentClinicalSource saved in DB - @param user_file: UserFile to create the ExperimentClinicalSource - @return: ExperimentClinicalSource saved instance - """ - source = ExperimentClinicalSource.objects.create(user_file=user_file) - return source - - -def create_test_differential_expression_experiment( - name: str, - clinical_source: ExperimentClinicalSource, - mrna_source: ExperimentSource, - user: User, - state: DifferentialExpressionExperimentState = DifferentialExpressionExperimentState.WAITING_FOR_QUEUE, - description: str = 'Test experiment', - clinical_attribute: str = 'SEX', - tool: DifferentialExpressionTool = DifferentialExpressionTool.DESEQ, - threshold_percentile: float = 0.15, - threshold: float = 0.0001, - top: int = 100, - task_id: Optional[str] = None -) -> DifferentialExpressionExperiment: - """ - Create a test DifferentialExpressionExperiment object - @param name: Experiment name - @param clinical_source: Clinical data source - @param mrna_source: mRNA data source - @param user: User who owns the experiment - @param state: Initial experiment state - @param description: Experiment description - @param clinical_attribute: Clinical attribute to use for analysis - @param tool: Tool to use for differential expression analysis - @param threshold_percentile: Threshold percentile for filtering - @param threshold: Threshold for filtering - @param top: Number of top results to keep - @param task_id: Optional Celery task ID - @return: Saved DifferentialExpressionExperiment instance - """ - experiment = DifferentialExpressionExperiment.objects.create( - name=name, - description=description, - clinical_source=clinical_source, - mrna_source=mrna_source, - clinical_attribute=clinical_attribute, - tool=tool, - threshold_percentile=threshold_percentile, - threshold=threshold, - top=top, - state=state, - user=user, - task_id=task_id - ) - return experiment diff --git a/src/differential_expression/tests/test_views.py b/src/differential_expression/tests/test_views.py deleted file mode 100644 index 1d7f17ae..00000000 --- a/src/differential_expression/tests/test_views.py +++ /dev/null @@ -1,718 +0,0 @@ -from django.test import TestCase -from django.contrib.auth.models import User -from rest_framework.test import APIClient -from rest_framework import status -from unittest.mock import patch, MagicMock -from common.tests_utils import create_user_file -from differential_expression.models import ( - DifferentialExpressionExperiment, - DifferentialExpressionExperimentState -) -from differential_expression.tests.test_utils import ( - get_test_file_path, - create_differential_expression_source, - create_differential_expression_clinical_source, - create_test_differential_expression_experiment -) -from user_files.models_choices import FileType - - -class DifferentialExpressionDeleteTestCase(TestCase): - """Tests for DifferentialExpressionDelete endpoint""" - - def setUp(self): - """Test setup""" - # Create test users - self.owner = User.objects.create_user( - username='owner', - email='owner@test.com', - password='testpass123' - ) - self.other_user = User.objects.create_user( - username='other', - email='other@test.com', - password='testpass123' - ) - - # Create test files - self.mrna_file = create_user_file( - get_test_file_path('mrna_test.csv'), - 'mRNA Test', - FileType.MRNA, - self.owner - ) - self.clinical_file = create_user_file( - get_test_file_path('clinical_test.csv'), - 'Clinical Test', - FileType.CLINICAL, - self.owner - ) - - # Create sources - self.mrna_source = create_differential_expression_source(self.mrna_file) - self.clinical_source = create_differential_expression_clinical_source(self.clinical_file) - - # Create API client - self.client = APIClient() - - def test_delete_experiment_success(self): - """Test successfully deleting a completed experiment""" - # Create a completed experiment - experiment = create_test_differential_expression_experiment( - name='Test Experiment', - clinical_source=self.clinical_source, - mrna_source=self.mrna_source, - user=self.owner, - state=DifferentialExpressionExperimentState.COMPLETED - ) - - # Authenticate as owner - self.client.force_authenticate(user=self.owner) - - # Delete the experiment - url = f'/differential-expression/delete/{experiment.pk}/' - response = self.client.delete(url) - - # Assert response - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(response.data['ok']) - - # Assert experiment was deleted - self.assertFalse( - DifferentialExpressionExperiment.objects.filter(pk=experiment.pk).exists() - ) - - def test_delete_experiment_not_owner(self): - """Test that non-owner cannot delete experiment""" - # Create experiment owned by owner - experiment = create_test_differential_expression_experiment( - name='Test Experiment', - clinical_source=self.clinical_source, - mrna_source=self.mrna_source, - user=self.owner, - state=DifferentialExpressionExperimentState.COMPLETED - ) - - # Authenticate as other user - self.client.force_authenticate(user=self.other_user) - - # Try to delete the experiment - url = f'/differential-expression/delete/{experiment.pk}/' - response = self.client.delete(url) - - # Assert forbidden - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertFalse(response.data['ok']) - - # Assert experiment still exists - self.assertTrue( - DifferentialExpressionExperiment.objects.filter(pk=experiment.pk).exists() - ) - - def test_delete_experiment_running_in_process(self): - """Test that running experiment cannot be deleted""" - # Create a running experiment - experiment = create_test_differential_expression_experiment( - name='Running Experiment', - clinical_source=self.clinical_source, - mrna_source=self.mrna_source, - user=self.owner, - state=DifferentialExpressionExperimentState.IN_PROCESS, - task_id='test-task-id' - ) - - # Authenticate as owner - self.client.force_authenticate(user=self.owner) - - # Try to delete the experiment - url = f'/differential-expression/delete/{experiment.pk}/' - response = self.client.delete(url) - - # Assert bad request - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertFalse(response.data['ok']) - self.assertIn('running', response.data['detail'].lower()) - - # Assert experiment still exists - self.assertTrue( - DifferentialExpressionExperiment.objects.filter(pk=experiment.pk).exists() - ) - - def test_delete_experiment_waiting_for_queue(self): - """Test that experiment waiting for queue cannot be deleted""" - # Create experiment waiting for queue - experiment = create_test_differential_expression_experiment( - name='Waiting Experiment', - clinical_source=self.clinical_source, - mrna_source=self.mrna_source, - user=self.owner, - state=DifferentialExpressionExperimentState.WAITING_FOR_QUEUE, - task_id='test-task-id' - ) - - # Authenticate as owner - self.client.force_authenticate(user=self.owner) - - # Try to delete the experiment - url = f'/differential-expression/delete/{experiment.pk}/' - response = self.client.delete(url) - - # Assert bad request - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertFalse(response.data['ok']) - - # Assert experiment still exists - self.assertTrue( - DifferentialExpressionExperiment.objects.filter(pk=experiment.pk).exists() - ) - - def test_delete_experiment_stopping(self): - """Test that experiment being stopped cannot be deleted""" - # Create experiment being stopped - experiment = create_test_differential_expression_experiment( - name='Stopping Experiment', - clinical_source=self.clinical_source, - mrna_source=self.mrna_source, - user=self.owner, - state=DifferentialExpressionExperimentState.STOPPING, - task_id='test-task-id' - ) - - # Authenticate as owner - self.client.force_authenticate(user=self.owner) - - # Try to delete the experiment - url = f'/differential-expression/delete/{experiment.pk}/' - response = self.client.delete(url) - - # Assert bad request - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertFalse(response.data['ok']) - - # Assert experiment still exists - self.assertTrue( - DifferentialExpressionExperiment.objects.filter(pk=experiment.pk).exists() - ) - - def test_delete_experiment_stopped_success(self): - """Test that stopped experiment can be deleted""" - # Create stopped experiment - experiment = create_test_differential_expression_experiment( - name='Stopped Experiment', - clinical_source=self.clinical_source, - mrna_source=self.mrna_source, - user=self.owner, - state=DifferentialExpressionExperimentState.STOPPED - ) - - # Authenticate as owner - self.client.force_authenticate(user=self.owner) - - # Delete the experiment - url = f'/differential-expression/delete/{experiment.pk}/' - response = self.client.delete(url) - - # Assert success - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(response.data['ok']) - - # Assert experiment was deleted - self.assertFalse( - DifferentialExpressionExperiment.objects.filter(pk=experiment.pk).exists() - ) - - def test_delete_experiment_with_error_success(self): - """Test that experiment with error can be deleted""" - # Create experiment with error - experiment = create_test_differential_expression_experiment( - name='Error Experiment', - clinical_source=self.clinical_source, - mrna_source=self.mrna_source, - user=self.owner, - state=DifferentialExpressionExperimentState.FINISHED_WITH_ERROR - ) - - # Authenticate as owner - self.client.force_authenticate(user=self.owner) - - # Delete the experiment - url = f'/differential-expression/delete/{experiment.pk}/' - response = self.client.delete(url) - - # Assert success - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(response.data['ok']) - - # Assert experiment was deleted - self.assertFalse( - DifferentialExpressionExperiment.objects.filter(pk=experiment.pk).exists() - ) - - def test_delete_experiment_not_authenticated(self): - """Test that unauthenticated user cannot delete experiment""" - # Create experiment - experiment = create_test_differential_expression_experiment( - name='Test Experiment', - clinical_source=self.clinical_source, - mrna_source=self.mrna_source, - user=self.owner, - state=DifferentialExpressionExperimentState.COMPLETED - ) - - # Don't authenticate - - # Try to delete the experiment - url = f'/differential-expression/delete/{experiment.pk}/' - response = self.client.delete(url) - - # Assert unauthorized - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - # Assert experiment still exists - self.assertTrue( - DifferentialExpressionExperiment.objects.filter(pk=experiment.pk).exists() - ) - - def test_delete_experiment_not_found(self): - """Test deleting non-existent experiment returns 404""" - # Authenticate as owner - self.client.force_authenticate(user=self.owner) - - # Try to delete non-existent experiment - url = '/differential-expression/delete/99999/' - response = self.client.delete(url) - - # Assert not found - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - @patch('differential_expression.views.AbortableAsyncResult') - def test_delete_experiment_with_active_task(self, mock_async_result): - """Test that experiment with active Celery task gets aborted before deletion""" - # Mock the AbortableAsyncResult - mock_result = MagicMock() - mock_result.state = 'STARTED' - mock_result.abort.return_value = True - mock_async_result.return_value = mock_result - - # Create completed experiment with task_id - experiment = create_test_differential_expression_experiment( - name='Test Experiment with Task', - clinical_source=self.clinical_source, - mrna_source=self.mrna_source, - user=self.owner, - state=DifferentialExpressionExperimentState.COMPLETED, - task_id='active-task-id' - ) - - # Authenticate as owner - self.client.force_authenticate(user=self.owner) - - # Delete the experiment - url = f'/differential-expression/delete/{experiment.pk}/' - response = self.client.delete(url) - - # Assert success - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(response.data['ok']) - - # Assert AbortableAsyncResult was called - mock_async_result.assert_called_once_with('active-task-id') - mock_result.abort.assert_called_once() - - # Assert experiment was deleted - self.assertFalse( - DifferentialExpressionExperiment.objects.filter(pk=experiment.pk).exists() - ) - - -class DifferentialExpressionListTestCase(TestCase): - """Tests for DifferentialExpressionList endpoint""" - - def setUp(self): - """Test setup""" - # Create test user - self.user = User.objects.create_user( - username='testuser', - email='test@test.com', - password='testpass123' - ) - - # Create test files - self.mrna_file = create_user_file( - get_test_file_path('mrna_test.csv'), - 'mRNA Test', - FileType.MRNA, - self.user - ) - self.clinical_file = create_user_file( - get_test_file_path('clinical_test.csv'), - 'Clinical Test', - FileType.CLINICAL, - self.user - ) - - # Create sources - self.mrna_source = create_differential_expression_source(self.mrna_file) - self.clinical_source = create_differential_expression_clinical_source(self.clinical_file) - - # Create API client - self.client = APIClient() - - def test_list_experiments_authenticated(self): - """Test listing experiments when authenticated""" - # Create some experiments - create_test_differential_expression_experiment( - name='Experiment 1', - clinical_source=self.clinical_source, - mrna_source=self.mrna_source, - user=self.user, - state=DifferentialExpressionExperimentState.COMPLETED - ) - create_test_differential_expression_experiment( - name='Experiment 2', - clinical_source=self.clinical_source, - mrna_source=self.mrna_source, - user=self.user, - state=DifferentialExpressionExperimentState.IN_PROCESS - ) - - # Authenticate - self.client.force_authenticate(user=self.user) - - # Get list - response = self.client.get('/differential-expression/') - - # Assert success - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['count'], 2) - - def test_list_experiments_not_authenticated(self): - """Test that unauthenticated users cannot list experiments""" - # Don't authenticate - response = self.client.get('/differential-expression/') - - # Assert forbidden (DRF returns 403 for unauthenticated requests) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - -class DifferentialExpressionUpdateTestCase(TestCase): - """Tests for DifferentialExpressionUpdate endpoint""" - - def setUp(self): - """Test setup""" - # Create test users - self.owner = User.objects.create_user( - username='owner', - email='owner@test.com', - password='testpass123' - ) - self.other_user = User.objects.create_user( - username='other', - email='other@test.com', - password='testpass123' - ) - - # Create test files - self.mrna_file = create_user_file( - get_test_file_path('mrna_test.csv'), - 'mRNA Test', - FileType.MRNA, - self.owner - ) - self.clinical_file = create_user_file( - get_test_file_path('clinical_test.csv'), - 'Clinical Test', - FileType.CLINICAL, - self.owner - ) - - # Create sources - self.mrna_source = create_differential_expression_source(self.mrna_file) - self.clinical_source = create_differential_expression_clinical_source(self.clinical_file) - - # Create API client - self.client = APIClient() - - def test_update_experiment_name_and_description_success(self): - """Test successfully updating both name and description""" - # Create experiment - experiment = create_test_differential_expression_experiment( - name='Original Name', - description='Original Description', - clinical_source=self.clinical_source, - mrna_source=self.mrna_source, - user=self.owner, - state=DifferentialExpressionExperimentState.COMPLETED - ) - - # Authenticate as owner - self.client.force_authenticate(user=self.owner) - - # Update the experiment - url = f'/differential-expression/update/{experiment.pk}/' - data = { - 'name': 'Updated Name', - 'description': 'Updated Description' - } - response = self.client.patch(url, data, format='json') - - # Assert response - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(response.data['ok']) - self.assertEqual(response.data['data']['name'], 'Updated Name') - self.assertEqual(response.data['data']['description'], 'Updated Description') - - # Assert experiment was updated in database - experiment.refresh_from_db() - self.assertEqual(experiment.name, 'Updated Name') - self.assertEqual(experiment.description, 'Updated Description') - - def test_update_experiment_name_only_success(self): - """Test successfully updating only the name""" - # Create experiment - experiment = create_test_differential_expression_experiment( - name='Original Name', - description='Original Description', - clinical_source=self.clinical_source, - mrna_source=self.mrna_source, - user=self.owner, - state=DifferentialExpressionExperimentState.COMPLETED - ) - - # Authenticate as owner - self.client.force_authenticate(user=self.owner) - - # Update only the name - url = f'/differential-expression/update/{experiment.pk}/' - data = {'name': 'New Name Only'} - response = self.client.patch(url, data, format='json') - - # Assert response - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(response.data['ok']) - self.assertEqual(response.data['data']['name'], 'New Name Only') - self.assertEqual(response.data['data']['description'], 'Original Description') - - # Assert experiment was updated in database - experiment.refresh_from_db() - self.assertEqual(experiment.name, 'New Name Only') - self.assertEqual(experiment.description, 'Original Description') - - def test_update_experiment_description_only_success(self): - """Test successfully updating only the description""" - # Create experiment - experiment = create_test_differential_expression_experiment( - name='Original Name', - description='Original Description', - clinical_source=self.clinical_source, - mrna_source=self.mrna_source, - user=self.owner, - state=DifferentialExpressionExperimentState.COMPLETED - ) - - # Authenticate as owner - self.client.force_authenticate(user=self.owner) - - # Update only the description - url = f'/differential-expression/update/{experiment.pk}/' - data = {'description': 'New Description Only'} - response = self.client.patch(url, data, format='json') - - # Assert response - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(response.data['ok']) - self.assertEqual(response.data['data']['name'], 'Original Name') - self.assertEqual(response.data['data']['description'], 'New Description Only') - - # Assert experiment was updated in database - experiment.refresh_from_db() - self.assertEqual(experiment.name, 'Original Name') - self.assertEqual(experiment.description, 'New Description Only') - - def test_update_experiment_not_owner(self): - """Test that non-owner cannot update experiment""" - # Create experiment owned by owner - experiment = create_test_differential_expression_experiment( - name='Original Name', - description='Original Description', - clinical_source=self.clinical_source, - mrna_source=self.mrna_source, - user=self.owner, - state=DifferentialExpressionExperimentState.COMPLETED - ) - - # Authenticate as other user - self.client.force_authenticate(user=self.other_user) - - # Try to update the experiment - url = f'/differential-expression/update/{experiment.pk}/' - data = {'name': 'Hacked Name'} - response = self.client.patch(url, data, format='json') - - # Assert forbidden - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertFalse(response.data['ok']) - - # Assert experiment was not updated - experiment.refresh_from_db() - self.assertEqual(experiment.name, 'Original Name') - - def test_update_experiment_no_fields_provided(self): - """Test that update fails when no fields are provided""" - # Create experiment - experiment = create_test_differential_expression_experiment( - name='Original Name', - description='Original Description', - clinical_source=self.clinical_source, - mrna_source=self.mrna_source, - user=self.owner, - state=DifferentialExpressionExperimentState.COMPLETED - ) - - # Authenticate as owner - self.client.force_authenticate(user=self.owner) - - # Try to update with no fields - url = f'/differential-expression/update/{experiment.pk}/' - data = {} - response = self.client.patch(url, data, format='json') - - # Assert bad request - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertFalse(response.data['ok']) - self.assertIn('at least one field', response.data['detail'].lower()) - - # Assert experiment was not updated - experiment.refresh_from_db() - self.assertEqual(experiment.name, 'Original Name') - self.assertEqual(experiment.description, 'Original Description') - - def test_update_experiment_not_authenticated(self): - """Test that unauthenticated user cannot update experiment""" - # Create experiment - experiment = create_test_differential_expression_experiment( - name='Original Name', - description='Original Description', - clinical_source=self.clinical_source, - mrna_source=self.mrna_source, - user=self.owner, - state=DifferentialExpressionExperimentState.COMPLETED - ) - - # Don't authenticate - - # Try to update the experiment - url = f'/differential-expression/update/{experiment.pk}/' - data = {'name': 'Hacked Name'} - response = self.client.patch(url, data, format='json') - - # Assert unauthorized - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - # Assert experiment was not updated - experiment.refresh_from_db() - self.assertEqual(experiment.name, 'Original Name') - - def test_update_experiment_not_found(self): - """Test updating non-existent experiment returns 404""" - # Authenticate as owner - self.client.force_authenticate(user=self.owner) - - # Try to update non-existent experiment - url = '/differential-expression/update/99999/' - data = {'name': 'New Name'} - response = self.client.patch(url, data, format='json') - - # Assert not found - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - def test_update_experiment_empty_name(self): - """Test that updating experiment with empty name fails""" - # Create experiment - experiment = create_test_differential_expression_experiment( - name='Original Name', - description='Original Description', - clinical_source=self.clinical_source, - mrna_source=self.mrna_source, - user=self.owner, - state=DifferentialExpressionExperimentState.COMPLETED - ) - - # Authenticate as owner - self.client.force_authenticate(user=self.owner) - - # Try to update with empty name - url = f'/differential-expression/update/{experiment.pk}/' - data = {'name': ''} - response = self.client.patch(url, data, format='json') - - # Assert bad request - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertFalse(response.data['ok']) - self.assertIn('cannot be empty', response.data['detail'].lower()) - - # Assert experiment was not updated - experiment.refresh_from_db() - self.assertEqual(experiment.name, 'Original Name') - - def test_update_experiment_empty_description(self): - """Test that updating experiment with empty description succeeds""" - # Create experiment - experiment = create_test_differential_expression_experiment( - name='Original Name', - description='Original Description', - clinical_source=self.clinical_source, - mrna_source=self.mrna_source, - user=self.owner, - state=DifferentialExpressionExperimentState.COMPLETED - ) - - # Authenticate as owner - self.client.force_authenticate(user=self.owner) - - # Update with empty description (should be allowed) - url = f'/differential-expression/update/{experiment.pk}/' - data = {'description': ''} - response = self.client.patch(url, data, format='json') - - # Assert success (empty description is valid) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(response.data['ok']) - self.assertEqual(response.data['data']['name'], 'Original Name') - self.assertEqual(response.data['data']['description'], '') - - # Assert experiment was updated in database - experiment.refresh_from_db() - self.assertEqual(experiment.name, 'Original Name') - self.assertEqual(experiment.description, '') - - def test_update_running_experiment(self): - """Test that running experiment can be updated (only name/description, not execution)""" - # Create a running experiment - experiment = create_test_differential_expression_experiment( - name='Running Experiment', - description='Running Description', - clinical_source=self.clinical_source, - mrna_source=self.mrna_source, - user=self.owner, - state=DifferentialExpressionExperimentState.IN_PROCESS, - task_id='test-task-id' - ) - - # Authenticate as owner - self.client.force_authenticate(user=self.owner) - - # Update the experiment - url = f'/differential-expression/update/{experiment.pk}/' - data = {'name': 'Updated Name While Running'} - response = self.client.patch(url, data, format='json') - - # Assert success (name/description can be updated even while running) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(response.data['ok']) - self.assertEqual(response.data['data']['name'], 'Updated Name While Running') - - # Assert experiment was updated in database but state is still IN_PROCESS - experiment.refresh_from_db() - self.assertEqual(experiment.name, 'Updated Name While Running') - self.assertEqual(experiment.state, DifferentialExpressionExperimentState.IN_PROCESS) diff --git a/src/differential_expression/tests/tests_files/clinical_test.csv b/src/differential_expression/tests/tests_files/clinical_test.csv deleted file mode 100644 index 87d9867a..00000000 --- a/src/differential_expression/tests/tests_files/clinical_test.csv +++ /dev/null @@ -1,7 +0,0 @@ -PATIENT_ID,OTHER_PATIENT_ID,SEX,OS_STATUS,OS_MONTHS,SAMPLE_ID,OTHER_SAMPLE_ID -TCGA-OR-A5J1,B3164F7B-C826-4E08-9EE6-8FF96D29B913,Male,1:DECEASED,44.51,TCGA-OR-A5J1-01,E4038EBB-6E6D-44B1-84AD-E35AAFCA7B70 -TCGA-OR-A5J2,8E7C2E31-D085-4B75-A970-162526DD07A0,Female,1:DECEASED,55.09,TCGA-OR-A5J2-01,46B7EB7C-E5F7-476D-A68C-5972F947445F -TCGA-OR-A5J3,DFD687BC-6E69-42F7-AF94-D17FC150D1A1,Female,0:LIVING,68.69,TCGA-OR-A5J3-01,1FB59B6F-53C0-4B14-82CC-77CD55C67AD6 -TCGA-OR-A5J5,802DBD0D-EF07-4C91-AB8D-1DD39532E947,Male,1:DECEASED,11.99,TCGA-OR-A5J5-01,C6214F9B-35C8-424C-B6FE-BEC1A9317C0A -TCGA-OR-A5J6,C8898B42-B704-45A0-9829-144B98F416E0,Female,0:LIVING,88.8,TCGA-OR-A5J6-01,C0C9DD1B-98B1-4A20-8927-3C0B75A4559E -TCGA-OR-A5J8,08E0D412-D4D8-4D13-B792-A4DD0BD9EC2B,Male,1:DECEASED,19.02,TCGA-OR-A5J8-01,2F47F06E-8F76-440E-BBCE-B7C90635A59E diff --git a/src/differential_expression/tests/tests_files/mrna_test.csv b/src/differential_expression/tests/tests_files/mrna_test.csv deleted file mode 100644 index ac8c756e..00000000 --- a/src/differential_expression/tests/tests_files/mrna_test.csv +++ /dev/null @@ -1,11 +0,0 @@ -Standard_Symbol,TCGA-OR-A5J1-01,TCGA-OR-A5J2-01,TCGA-OR-A5J3-01,TCGA-OR-A5J5-01,TCGA-OR-A5J6-01,TCGA-OR-A5J8-01 -LOC100130426,0.0,0.0,0.0,0.0,0.0,0.0 -UBE2Q2P3,3.2661,2.6815,1.7301,0.0,0.0,1.4422 -HMGB1P1,149.1354,81.0777,86.4879,53.9117,66.9063,94.9316 -TIMM23,2034.1018,1304.9251,1054.6586,2350.8908,1257.9864,995.0269 -MOXD2,0.0,0.0,0.0,0.0,0.0,0.0 -LOC155060,274.2555,199.302,348.3928,439.1944,149.2147,377.9528 -RNU12-2P,1.4409,0.0,0.5925,0.7746,0.0,1.6577 -EZHIP,17.7714,0.4026,0.5925,0.7746,0.0,1.2433 -EFCAB8,0.0,0.4026,0.5925,0.7746,2.7943,0.8288 -SRP14P1,11.5274,5.2342,7.7026,6.1967,10.6183,5.3875 diff --git a/src/differential_expression/urls.py b/src/differential_expression/urls.py deleted file mode 100644 index 19c38e54..00000000 --- a/src/differential_expression/urls.py +++ /dev/null @@ -1,21 +0,0 @@ -from django.urls import path -from . import views - -urlpatterns = [ - path('', views.DifferentialExpressionList.as_view(), name='differential_expression_list'), - path('results/', views.DifferentialExpressionDetail.as_view(), name='differential_expression_results'), - path('results//', views.DifferentialExpressionResults.as_view()), - path('volcano-data', views.DifferentialExpressionVolcanoData.as_view(), name='differential_expression_volcano_data'), - path('volcano-data//', views.DifferentialExpressionVolcanoData.as_view()), - path('submit-experiment', views.DifferentialExpressionSubmit.as_view(), name='differential_expression_submit'), - path('stop-experiment', views.DifferentialExpressionStop.as_view(), name='differential_expression_stop'), - path('update', views.DifferentialExpressionUpdate.as_view(), name='differential_expression_update'), - path('update//', views.DifferentialExpressionUpdate.as_view()), - path('delete', views.DifferentialExpressionDelete.as_view(), name='differential_expression_delete'), - path('delete//', views.DifferentialExpressionDelete.as_view()), - path('download-results', views.download_differential_expression_results, name='download_differential_expression_results'), - path('download-results//', views.download_differential_expression_results), - path('get-common-samples-differential-experiment', views.GetCommonSamplesDifferentialExperiment.as_view(), name='get_common_samples_differential_experiment'), - path('get-common-samples-one-front-differential-experiment', views.GetCommonSamplesDifferentialOneFrontExperiment.as_view(), name='get_common_samples_one_front_differential_experiment'), - path('switch-institution-public-view', views.ToggleDiffExperimentPublicView.as_view(), name='switch-diff-experiment-public-view'), - ] \ No newline at end of file diff --git a/src/differential_expression/views.py b/src/differential_expression/views.py index 61f6a995..200df7ba 100644 --- a/src/differential_expression/views.py +++ b/src/differential_expression/views.py @@ -1,839 +1 @@ -import logging -from typing import Optional, Dict, Tuple, List - -import numpy as np -import pandas as pd -from celery.contrib.abortable import AbortableAsyncResult -from django.contrib.auth.decorators import login_required -from django.core.files.base import ContentFile -from django.db import transaction -from django.db.models import Q -from django.http import HttpResponse -from django_filters.rest_framework import DjangoFilterBackend -from rest_framework import filters, generics, permissions, status -from rest_framework.exceptions import ValidationError -from rest_framework.generics import get_object_or_404 -from rest_framework.request import Request -from rest_framework.response import Response -from rest_framework.views import APIView - -from api_service.enums import SourceType, CommonSamplesStatusErrorCode -from api_service.utils import get_cgds_dataset -from common.enums import ResponseCode -from common.functions import get_enum_from_value, get_intersection, encode_json_response_status -from common.pagination import StandardResultsSetPagination -from common.response import ResponseStatus -from api_service.models import ExperimentClinicalSource, ExperimentSource -from datasets_synchronization.models import CGDSDataset -from datasets_synchronization.models import CGDSStudy -from differential_expression.models import ( - DifferentialExpressionExperiment, - DifferentialExpressionExperimentState, -) -from differential_expression.serializers import ( - DifferentialExpressionExperimentDetailSerializer, - DifferentialExpressionExperimentResultSerializer, - DifferentialExpressionExperimentListSerializer, - DifferentialExpressionVolcanoPlotSerializer, -) -from user_files.models import UserFile -from user_files.models_choices import FileType -from user_files.views import get_an_user_file -from .tasks import eval_differential_expression_experiment - - -def create_differential_expression_source( - source_type: int, - request: Request, - file_type: FileType, - prefix: str -) -> tuple[ - ExperimentSource | ExperimentClinicalSource | None, ExperimentClinicalSource | None -]: - """ - Creates a Source object for differential expression experiments. - """ - is_clinical = prefix == 'clinical' - source = ExperimentClinicalSource() if is_clinical else ExperimentSource() - clinical_source = None - - if source_type == SourceType.NEW_DATASET.value: - # Adds a new User's file and uses it - source_file = getattr(request, 'FILES', {}).get(f'{prefix}File') - if source_file is None: - return None, None - - user_file = UserFile( - name=source_file.name, - description=None, - file_obj=source_file, - file_type=file_type, - user=request.user - ) - user_file.save() - user_file.compute_post_saved_field() - source.user_file = user_file - - elif source_type == SourceType.CGDS.value: - # Gets the CGDS Study - post_data = getattr(request, 'data', {}) or getattr(request, 'POST', {}) - cgds_study_pk = post_data.get(f'{prefix}CGDSStudyPk') - if not cgds_study_pk: - return None, None - - cgds_study = CGDSStudy.objects.get(pk=int(cgds_study_pk)) - - if is_clinical: - # For clinical sources, we need both datasets - if cgds_study.clinical_patient_dataset and cgds_study.clinical_sample_dataset: - source.cgds_dataset = cgds_study.clinical_patient_dataset - source.extra_cgds_dataset = cgds_study.clinical_sample_dataset - else: - return None, None - else: - cgds_dataset = get_cgds_dataset(cgds_study, file_type) - if cgds_dataset: - source.cgds_dataset = cgds_dataset - else: - return None, None - - else: - # Uses an existing User's file - post_data = getattr(request, 'data', {}) or getattr(request, 'POST', {}) - existing_file_pk = post_data.get(f'{prefix}ExistingFilePk') - if not existing_file_pk: - return None, None - - user_file = get_an_user_file(user=request.user, user_file_pk=int(existing_file_pk)) - source.user_file = user_file - - source.save() - return source, clinical_source - - -class DifferentialExpressionDetail(generics.RetrieveAPIView): - """ - Endpoint to retrieve a differential expression experiment. - """ - - def get_queryset(self) -> DifferentialExpressionExperiment: - """ - Retrieve the differential expression experiment by its ID. - """ - user = self.request.user - experiment_id = self.kwargs.get('pk') - experiment = get_object_or_404(DifferentialExpressionExperiment, pk=experiment_id) - # Check if the user has access to the experiment - if not (experiment.is_public or - experiment.user == user or - experiment.shared_institutions.filter(institutionadministration__user=user).exists() or - experiment.shared_users.filter(id=user.id).exists()): - raise ValidationError('You do not have permission to access this experiment.') - - return experiment.results.all() - filter_backends = [filters.SearchFilter, filters.OrderingFilter, DjangoFilterBackend] - search_fields = ['gene'] - serializer_class = DifferentialExpressionExperimentDetailSerializer - permission_classes = [permissions.IsAuthenticated] - - -class DifferentialExpressionList(generics.ListAPIView): - """ - Endpoint to list all differential expression experiments. - Returns only the fields: id, Name, Description, Date, State, Sources and if it is public. - """ - - def get_queryset(self): - """ - Endpoint to list all differential expression experiments. - """ - user = self.request.user - experiments = DifferentialExpressionExperiment.objects.filter( - Q(is_public=True) | - Q(user=user) | - Q(shared_institutions__institutionadministration__user=user) | - Q(shared_users=user) - ).distinct() - - return experiments - - serializer_class = DifferentialExpressionExperimentListSerializer - permission_classes = [permissions.IsAuthenticated] - pagination_class = StandardResultsSetPagination - filter_backends = [filters.OrderingFilter, filters.SearchFilter, DjangoFilterBackend] - search_fields = ['name', 'description'] - - -class DifferentialExpressionRetrieve(generics.RetrieveAPIView): - """ - Endpoint to retrieve a single differential expression experiment by ID. - """ - serializer_class = DifferentialExpressionExperimentListSerializer - permission_classes = [permissions.IsAuthenticated] - - def get_queryset(self): - """ - Returns experiments the user has access to. - """ - user = self.request.user - return DifferentialExpressionExperiment.objects.filter( - Q(is_public=True) | - Q(user=user) | - Q(shared_institutions__institutionadministration__user=user) | - Q(shared_users=user) - ).distinct() - - def get_object(self): - """ - Retrieves the experiment and checks permissions. - """ - experiment = get_object_or_404(DifferentialExpressionExperiment, pk=self.kwargs.get('pk')) - user = self.request.user - - # Check if the user has access to the experiment - if not (experiment.is_public or - experiment.user == user or - experiment.shared_institutions.filter(institutionadministration__user=user).exists() or - experiment.shared_users.filter(id=user.id).exists()): - raise ValidationError('You do not have permission to access this experiment.') - - return experiment - filterset_fields = ['tool'] - - -class DifferentialExpressionSubmit(APIView): - """ - Endpoint to submit a differential expression experiment. - """ - - permission_classes = [permissions.IsAuthenticated] - - @staticmethod - def post(request: Request): - """ - Endpoint to submit a differential expression experiment. - """ - with transaction.atomic(): - # Get basic experiment data - post_data = getattr(request, 'data', {}) or getattr(request, 'POST', {}) - - name = post_data.get('name', 'Differential Expression Experiment') - description = post_data.get('description', '') - - # Clinical source - clinical_source_type = post_data.get('clinicalType') - - if clinical_source_type: - clinical_source_type = int(clinical_source_type) - clinical_source, clinical_aux = create_differential_expression_source( - clinical_source_type, request, FileType.CLINICAL, 'clinical') - # Select the valid one (if it's a CGDSStudy it needs clinical_aux as it has both needed CGDSDatasets) - clinical_source = clinical_aux if clinical_aux is not None else clinical_source - else: - clinical_source = None - - if clinical_source is None: - raise ValidationError('Invalid clinical source') - - # mRNA source - mrna_source_type = post_data.get('mRNAType') - if mrna_source_type: - mrna_source_type = int(mrna_source_type) - mrna_source, _mrna_clinical = create_differential_expression_source( - mrna_source_type, request, FileType.MRNA, 'mRNA') - else: - mrna_source = None - - if mrna_source is None: - raise ValidationError('Invalid mRNA source') - - # Clinical attribute - clinical_attribute = post_data.get('clinicalAttribute') - if not clinical_attribute: - raise ValidationError('Clinical attribute is required') - - # Threshold percentile - try: - threshold_percentile_str = post_data.get('thresholdPercentile', '0.15') - threshold_percentile = float(threshold_percentile_str) - except (ValueError, TypeError) as exc: - raise ValidationError('Invalid threshold percentile value') from exc - - if threshold_percentile < 0 or threshold_percentile > 1: - raise ValidationError('Threshold percentile must be between 0 and 1') - - # Threshold - try: - threshold_str = post_data.get('threshold', '0.0001') - threshold = float(threshold_str) - except (ValueError, TypeError) as exc: - raise ValidationError('Invalid threshold value') from exc - - if threshold < 0 or threshold > 1: - raise ValidationError('Threshold must be between 0 and 1') - - # Top parameter - try: - top_str = post_data.get('top', '100') - top = int(top_str) - except (ValueError, TypeError) as exc: - raise ValidationError('Invalid top value') from exc - - if top < 1 or top > 1000: - raise ValidationError('Top must be between 1 and 1000') - - # Create the differential expression experiment - experiment = DifferentialExpressionExperiment.objects.create( - name=name, - description=description, - clinical_source=clinical_source, - mrna_source=mrna_source, - clinical_attribute=clinical_attribute, - threshold_percentile=threshold_percentile, - threshold=threshold, - top=top, - user=request.user, - ) - - # Start the async task - async_res = eval_differential_expression_experiment.apply_async( - (experiment.pk,), queue='differential_expression') - - experiment.task_id = async_res.task_id - experiment.save(update_fields=['task_id']) - - return Response({'ok': True}) - - -class DifferentialExpressionResults(generics.ListAPIView): - """ - Endpoint to get results for a differential expression experiment. - Supports filtering by p-value and fold-change thresholds via query parameters. - """ - serializer_class = DifferentialExpressionExperimentResultSerializer - permission_classes = [permissions.IsAuthenticated] - pagination_class = StandardResultsSetPagination - filter_backends = [filters.SearchFilter, filters.OrderingFilter, DjangoFilterBackend] - search_fields = ['gene'] - ordering_fields = ['adj_p_val', 'log_fc', 'p_value', 'ave_expr'] - ordering = ['adj_p_val'] # Default ordering by adjusted p-value - - def get_queryset(self): - experiment_id = self.kwargs.get('pk') - experiment = get_object_or_404(DifferentialExpressionExperiment, pk=experiment_id) - - # Check permissions - user = self.request.user - if not (experiment.is_public or - experiment.user == user or - experiment.shared_institutions.filter(institutionadministration__user=user).exists() or - experiment.shared_users.filter(id=user.id).exists()): - raise ValidationError('You do not have permission to access this experiment.') - - # Get query parameters for filtering (optional) - p_threshold = self.request.query_params.get('p_threshold') - fc_threshold = self.request.query_params.get('fc_threshold') - - # If filtering parameters are provided, apply filtering - if p_threshold is not None or fc_threshold is not None: - try: - p_threshold = float(p_threshold) if p_threshold is not None else 0.05 - fc_threshold = float(fc_threshold) if fc_threshold is not None else 2.0 - except ValueError as exc: - raise ValidationError('Invalid threshold values. Must be numeric.') from exc - - # Validate thresholds - if p_threshold < 0 or p_threshold > 1: - raise ValidationError('p_threshold must be between 0 and 1') - if fc_threshold < 0: - raise ValidationError('fc_threshold must be positive') - - return experiment.get_significant_genes(p_threshold, fc_threshold) - - # Return all results if no filtering parameters - return experiment.results.all() - - -class DifferentialExpressionVolcanoData(generics.ListAPIView): - """ - Endpoint to get all results for a volcano plot visualization. - Returns all results without pagination in a format suitable for volcano plots. - Format: [{id, label, log2FC, pValue}, ...] - """ - serializer_class = DifferentialExpressionVolcanoPlotSerializer - permission_classes = [permissions.IsAuthenticated] - pagination_class = None # Disable pagination for volcano plot - - def get_queryset(self): - """Get all results for the specified experiment with permission checks.""" - pk = self.kwargs.get('pk') - experiment = get_object_or_404(DifferentialExpressionExperiment, pk=pk) - - # Check permissions - user = self.request.user - if not (experiment.is_public or - experiment.user == user or - experiment.shared_institutions.filter(institutionadministration__user=user).exists() or - experiment.shared_users.filter(id=user.id).exists()): - raise ValidationError('You do not have permission to access this experiment.') - - # Return all results (no pagination) - return experiment.results.all() - - -class DifferentialExpressionStop(APIView): - """ - Endpoint to stop a differential expression experiment. - """ - permission_classes = [permissions.IsAuthenticated] - - @staticmethod - def get(request: Request): - experiment_id = request.GET.get('experimentId') - if not experiment_id: - raise ValidationError('experimentId is required.') - - experiment = get_object_or_404(DifferentialExpressionExperiment, pk=experiment_id) - - user = request.user - if not (experiment.is_public or - experiment.user == user or - experiment.shared_institutions.filter(institutionadministration__user=user).exists() or - experiment.shared_users.filter(id=user.id).exists()): - raise ValidationError('You do not have permission to access this experiment.') - - # Task state and validation - if not experiment.task_id: - return Response({'ok': False, 'detail': 'The experiment does not have an associated task.'}) - - # If it's no running, there's nothing to stop - if experiment.state in [ - DifferentialExpressionExperimentState.COMPLETED, - DifferentialExpressionExperimentState.STOPPED, - DifferentialExpressionExperimentState.FINISHED_WITH_ERROR, - DifferentialExpressionExperimentState.TIMEOUT_EXCEEDED, - DifferentialExpressionExperimentState.REACHED_ATTEMPTS_LIMIT, - DifferentialExpressionExperimentState.EMPTY_DATASET, - DifferentialExpressionExperimentState.NO_SAMPLES_IN_COMMON, - DifferentialExpressionExperimentState.NO_FEATURES_FOUND, - ]: - return Response({'ok': False, 'detail': f'The experiment is not running. Status: {experiment.state}'}) - - # Try to abort the AbortableTask - try: - async_res = AbortableAsyncResult(experiment.task_id) - aborted = async_res.abort() # Signals the task to abort (self.is_aborted() == True) - except Exception as e: - return Response({'ok': False, 'detail': f'The task could not be stopped: {e}'}) - - # Mark the experiment as STOPPING; the task will mark it as STOPPED in the finally block - experiment.state = DifferentialExpressionExperimentState.STOPPING - experiment.save(update_fields=['state']) - - return Response({'ok': bool(aborted)}) - - -class GetCommonSamplesDifferentialExperiment(APIView): - """Gets the number of in common samples between two datasets""" - - permission_classes = [permissions.IsAuthenticated] - - @staticmethod - def get(request: Request): - mrna_source_id = request.GET.get('mRNASourceId') - mrna_source_type = request.GET.get('mRNASourceType') - clinical_source_id = request.GET.get('clinicalSourceId') - clinical_source_type = request.GET.get('clinicalSourceType') - if None in [mrna_source_id, mrna_source_type, clinical_source_id, - clinical_source_type]: - response = { - 'status': ResponseStatus( - ResponseCode.ERROR, - message='Invalid request params', - internal_code=CommonSamplesStatusErrorCode.INVALID_PARAMS - ), - } - else: - # Cast parameters - mrna_source_id = int(mrna_source_id) - mrna_source_type = get_enum_from_value( - int(mrna_source_type), SourceType) - - clinical_source_id = int(clinical_source_id) - clinical_source_type = get_enum_from_value(int(clinical_source_type), SourceType) - - # Gets df - samples_list_mrna, response = get_samples_list( - mrna_source_id, - mrna_source_type, - FileType.MRNA, - request.user - ) - - # Response will be != None if an error occurred - if response is None: - samples_list_clinical, response = get_samples_list( - clinical_source_id, - clinical_source_type, - FileType.CLINICAL, - request.user - ) - intersection = get_intersection(samples_list_mrna, samples_list_clinical) - # Gets intersection - - if response is None: - response = { - 'status': ResponseStatus(ResponseCode.SUCCESS), - 'data': { - 'number_samples_mrna': len(samples_list_mrna) if samples_list_mrna is not None else 0, - 'number_samples_clinical': len( - samples_list_clinical) if samples_list_clinical is not None else 0, - 'number_samples_in_common': intersection.size - } - } - - # Formats to JSON the ResponseStatus object - return encode_json_response_status(response) - - -class GetCommonSamplesDifferentialOneFrontExperiment(APIView): - permission_classes = [permissions.IsAuthenticated] - """Gets the number of in common samples between two datasets, one in the backend and other in the frontend""" - - @staticmethod - def post(request: Request): - post_data = getattr(request, 'data', {}) or getattr(request, 'POST', {}) - headers_in_front: Optional[List[str]] = post_data.get('headersColumnsNames') - other_source_id = post_data.get('otherSourceId') - other_source_type = post_data.get('otherSourceType') - other_source_file_type = post_data.get('otherSourceFileType') - - if headers_in_front is None or other_source_id is None or other_source_type is None or other_source_file_type is None: - response = { - 'status': ResponseStatus( - ResponseCode.ERROR, - message='Invalid request params', - internal_code=CommonSamplesStatusErrorCode.INVALID_PARAMS - ), - } - else: - # Cast parameters - other_source_id = int(other_source_id) - other_source_type = get_enum_from_value( - int(other_source_type), SourceType) - - # Gets df - samples_list_1, response = get_samples_list( - other_source_id, - other_source_type, - other_source_file_type, - request.user - ) - - # Response will be != None if an error occurred - if response is None: - intersection: np.ndarray = get_intersection( - samples_list_1, headers_in_front) - response = { - 'status': ResponseStatus(ResponseCode.SUCCESS), - 'data': { - 'number_samples_backend': len(samples_list_1) if samples_list_1 else 0, - 'number_samples_in_common': intersection.size - } - } - - # Formats to JSON the ResponseStatus object - return encode_json_response_status(response) - - -class ToggleDiffExperimentPublicView(APIView): - """ - API endpoint to toggle the 'is_public' field of an experiment. - Only the owner of the experiment can perform this action. - """ - permission_classes = [permissions.IsAuthenticated] - - @staticmethod - def post(request): - """ - Toggle the 'is_public' field of the experiment. - """ - data = request.data - experiment_id = data.get('experimentId') - experiment = get_object_or_404(DifferentialExpressionExperiment, id=experiment_id) - if experiment.user.id != request.user.id: - return Response( - {"error": "You do not have permission to modify this experiment."}, - status=status.HTTP_403_FORBIDDEN - ) - - experiment.is_public = not experiment.is_public - experiment.save(update_fields=['is_public']) - - return Response( - {"id": experiment.id, "is_public": experiment.is_public} - ) - - -class DifferentialExpressionUpdate(APIView): - """ - Endpoint to update name and description of a differential expression experiment. - Only the owner can update the experiment. - """ - permission_classes = [permissions.IsAuthenticated] - - @staticmethod - def patch(request: Request, pk: int): - """ - Update name and/or description of a differential expression experiment. - """ - experiment = get_object_or_404(DifferentialExpressionExperiment, pk=pk) - - # Only the owner can update the experiment - if experiment.user.id != request.user.id: - return Response( - {'ok': False, 'detail': 'You do not have permission to update this experiment.'}, - status=403 - ) - - # Get the data from request - data = request.data - name = data.get('name') - description = data.get('description') - - # Validate that at least one field is provided - if name is None and description is None: - return Response( - {'ok': False, 'detail': 'At least one field (name or description) must be provided.'}, - status=400 - ) - - # Validate that name is not empty if provided - if name is not None and not name: - return Response( - {'ok': False, 'detail': 'Experiment name cannot be empty.'}, - status=400 - ) - - # Update fields if provided - fields_to_update = [] - if name is not None: - experiment.name = name - fields_to_update.append('name') - if description is not None: - experiment.description = description - fields_to_update.append('description') - - # Save the experiment - experiment.save(update_fields=fields_to_update) - - return Response({ - 'ok': True, - 'data': { - 'id': experiment.id, - 'name': experiment.name, - 'description': experiment.description - } - }) - - -class DifferentialExpressionDelete(APIView): - """ - Endpoint to delete a differential expression experiment. - Only the owner can delete the experiment. - The experiment must not be running (must be completed, stopped, or in an error state). - """ - permission_classes = [permissions.IsAuthenticated] - - @staticmethod - def delete(request: Request, pk: int): - """ - Delete a differential expression experiment. - """ - experiment = get_object_or_404(DifferentialExpressionExperiment, pk=pk) - - # Only the owner can delete the experiment - if experiment.user.id != request.user.id: - return Response( - {'ok': False, 'detail': 'You do not have permission to delete this experiment.'}, - status=403 - ) - - # Check if the experiment is currently running - running_states = [ - DifferentialExpressionExperimentState.IN_PROCESS, - DifferentialExpressionExperimentState.WAITING_FOR_QUEUE, - DifferentialExpressionExperimentState.STOPPING, - ] - - if experiment.state in running_states: - return Response( - { - 'ok': False, - 'detail': f'Cannot delete experiment while it is running. Current state: {experiment.get_state_display()}. Please stop the experiment first.' - }, - status=400 - ) - - # If the experiment has a task_id and is somehow still active, abort it - if experiment.task_id: - try: - async_res = AbortableAsyncResult(experiment.task_id) - if async_res.state in ['PENDING', 'STARTED', 'RETRY']: - async_res.abort() - except Exception as ex: - # If we can't abort, continue with deletion anyway since state shows it's not running - logging.exception(ex) - - # Delete the experiment (cascade will delete related objects) - experiment.delete() - - return Response({'ok': True}) - - -def get_samples_list( - id_source: int, - type_source: Optional[SourceType], - file_type: Optional[FileType], - user -) -> Tuple[Optional[List[str]], Optional[Dict]]: - """ - Gets a DataFrame from the file retrieve from DB or MongoDB with an id and SourceType. - @param id_source: ID of the UserFile/CGDSDataset to retrieve. - @param type_source: Source type to check if it's a UserFile or a CGDSDataset. - @param file_type: FileType (mRNA, miRNA, etc.) to get the corresponding CGDSDataset. - @param user: Current logged user to retrieve only his datasets. - @return: A DataFrame (if corresponds) and a Response dict (the dataset doesn't exist). - """ - list_of_samples = None - response = None - if type_source is None: - response = { - 'status': ResponseStatus( - ResponseCode.ERROR, - message=f'The source type {type_source} does not exist', - internal_code=CommonSamplesStatusErrorCode.SOURCE_TYPE_DOES_NOT_EXISTS - ), - } - elif type_source == SourceType.UPLOADED_DATASETS: - try: - user_file = get_an_user_file(user=user, user_file_pk=id_source) - if file_type == FileType.CLINICAL: - list_of_samples = user_file.get_first_column_of_all_rows() - else: - list_of_samples = user_file.get_column_names() - except UserFile.DoesNotExist: - response = { - 'status': ResponseStatus( - ResponseCode.ERROR, - message=f'The UserFile with id = {id_source} does not exist', - internal_code=CommonSamplesStatusErrorCode.DATASET_DOES_NOT_EXISTS - ), - } - elif type_source == SourceType.CGDS: - try: - # Gets the CGDS Study - cgds_study = CGDSStudy.objects.get(pk=id_source) - - # Gets the corresponding Study's Dataset - cgds_dataset = get_cgds_dataset(cgds_study, file_type) - - list_of_samples = cgds_dataset.get_column_names() - - except CGDSDataset.DoesNotExist: - response = { - 'status': ResponseStatus( - ResponseCode.ERROR, - message=f'The CGDS dataset with id = {id_source} does not exist', - internal_code=CommonSamplesStatusErrorCode.DATASET_DOES_NOT_EXISTS - ), - } - - return list_of_samples, response - - -@login_required -def download_differential_expression_results(request, pk: int): - """ - Downloads all the differential expression results for a specific experiment. - Returns a TSV file with all results (no pagination). - Supports optional filtering via query parameters: - - adj_p_val: adjusted p-value threshold (returns genes with adj_p_val <= adj_p_val) - - log_fc: log fold change threshold (returns genes with |log_fc| >= log_fc) - """ - experiment = get_object_or_404(DifferentialExpressionExperiment, pk=pk) - - # Check permissions - user = request.user - if not (experiment.is_public or - experiment.user == user or - experiment.shared_institutions.filter(institutionadministration__user=user).exists() or - experiment.shared_users.filter(id=user.id).exists()): - return HttpResponse('Unauthorized', status=401) - - # Get filter parameters from query string - adj_p_val = request.GET.get('adj_p_val') - log_fc = request.GET.get('log_fc') - - results = experiment.results.all() - filename_suffix = '' - - # Apply adj_p_val filter if provided - if adj_p_val is not None: - try: - adj_p_val = float(adj_p_val) - except ValueError: - return HttpResponse('Invalid adj_p_val. Must be numeric.', status=400) - - if adj_p_val < 0 or adj_p_val > 1: - return HttpResponse('adj_p_val must be between 0 and 1', status=400) - - results = results.filter(adj_p_val__lte=adj_p_val) - filename_suffix += f'_p{adj_p_val}' - - # Apply log_fc filter if provided - if log_fc is not None: - try: - log_fc = float(log_fc) - except ValueError: - return HttpResponse('Invalid log_fc. Must be numeric.', status=400) - - if log_fc < 0: - return HttpResponse('log_fc must be >= 0', status=400) - - results = results.filter( - Q(log_fc__gte=log_fc) | Q(log_fc__lte=-log_fc) - ) - filename_suffix += f'_fc{log_fc}' - - if not filename_suffix: - filename_suffix = '_all' - - results = results.order_by('adj_p_val') - - if not results.exists(): - return HttpResponse('No results found for this experiment', status=404) - - # Convert results to list of dictionaries - results_data = [] - for result in results: - results_data.append({ - 'gene': result.gene, - 'log_fc': result.log_fc, - 'ave_expr': result.ave_expr, - 't_statistic': result.t_statistic, - 'p_value': result.p_value, - 'adj_p_val': result.adj_p_val, - 'b_statistic': result.b_statistic - }) - - # Create DataFrame and convert to TSV - df = pd.DataFrame(results_data) - file_to_send = ContentFile(df.to_csv(sep='\t', decimal='.', index=False)) - - # Generate HTTP response for file download - response = HttpResponse(file_to_send, 'text/csv') - response['Content-Length'] = file_to_send.size - response['Content-Disposition'] = f'attachment; filename="{experiment.name}_results{filename_suffix}.tsv"' - - return response +from django.shortcuts import render diff --git a/src/feature_selection/tasks.py b/src/feature_selection/tasks.py index 9da116da..f2e8edd8 100644 --- a/src/feature_selection/tasks.py +++ b/src/feature_selection/tasks.py @@ -62,7 +62,7 @@ def eval_feature_selection_experiment(self, experiment_pk: int, fit_fun_enum: Fi start = time.time() molecules_temp_file_path, clinical_temp_file_path, running_in_spark = prepare_and_compute_fs_experiment( experiment, fit_fun_enum, fitness_function_parameters, algorithm_parameters, - cross_validation_parameters, is_aborted=self.is_aborted + cross_validation_parameters, self.is_aborted ) total_execution_time = time.time() - start logging.warning(f'FSExperiment {experiment.pk} total time -> {total_execution_time} seconds') diff --git a/src/frontend/static/frontend/package-lock.json b/src/frontend/static/frontend/package-lock.json index b6e3a75c..72148eca 100644 --- a/src/frontend/static/frontend/package-lock.json +++ b/src/frontend/static/frontend/package-lock.json @@ -23,13 +23,12 @@ "ky": "^1.7.5", "lodash": "^4.17.21", "neo-react-semantic-ui-range": "^0.3.6", - "plotly.js": "^3.3.0", "react": "^18.3.1", "react-apexcharts": "^1.4.1", "react-avatar": "^5.0.3", "react-colorful": "^5.6.1", "react-dom": "^18.3.1", - "react-plotly.js": "^2.6.0", + "react-intl": "^7.1.14", "react-virtualized": "^9.22.6", "recharts": "^2.15.1", "semantic-ui-react": "^2.1.5", @@ -56,6 +55,7 @@ "@types/validator": "^13.12.3", "babel-loader": "^10.0.0", "babel-plugin-react-compiler": "^19.1.0-rc.2", + "baseline-browser-mapping": "^2.10.0", "css-loader": "^7.1.2", "eslint": "^9.27.0", "eslint-plugin-jsdoc": "^50.6.9", @@ -128,7 +128,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -2125,7 +2124,6 @@ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", "license": "MIT", - "peer": true, "engines": { "node": ">=6.9.0" } @@ -2180,24 +2178,6 @@ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" }, - "node_modules/@choojs/findup": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@choojs/findup/-/findup-0.2.1.tgz", - "integrity": "sha512-YstAqNb0MCN8PjdLCDfRsBcGVRN41f3vgLvaI0IrIcBp4AqILRSS0DeWNGkicC+f/zRIPJLc+9RURVSepwvfBw==", - "license": "MIT", - "dependencies": { - "commander": "^2.15.1" - }, - "bin": { - "findup": "bin/findup.js" - } - }, - "node_modules/@choojs/findup/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT" - }, "node_modules/@cnakazawa/watch": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", @@ -2564,6 +2544,102 @@ "react-dom": "^16.8.0 || ^17 || ^18" } }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz", + "integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==", + "dependencies": { + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/intl-localematcher": "0.6.2", + "decimal.js": "^10.4.3", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/ecma402-abstract/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz", + "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/fast-memoize/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz", + "integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "@formatjs/icu-skeleton-parser": "1.8.16", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.16", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz", + "integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@formatjs/intl": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-3.1.8.tgz", + "integrity": "sha512-LWXgwI5zTMatvR8w8kCNh/priDTOF/ZssokMBHJ7ZWXFoYLVOYo0EJERD9Eajv+xsfQO1QkuAt77KWQ1OI4mOQ==", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/icu-messageformat-parser": "2.11.4", + "intl-messageformat": "10.7.18", + "tslib": "^2.8.0" + }, + "peerDependencies": { + "typescript": "^5.6.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz", + "integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/intl-localematcher/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@formatjs/intl/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, "node_modules/@gamestdio/websocket": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@gamestdio/websocket/-/websocket-0.3.2.tgz", @@ -3698,122 +3774,6 @@ "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", "dev": true }, - "node_modules/@mapbox/geojson-rewind": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz", - "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==", - "license": "ISC", - "dependencies": { - "get-stream": "^6.0.1", - "minimist": "^1.2.6" - }, - "bin": { - "geojson-rewind": "geojson-rewind" - } - }, - "node_modules/@mapbox/geojson-rewind/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@mapbox/geojson-types": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@mapbox/geojson-types/-/geojson-types-1.0.2.tgz", - "integrity": "sha512-e9EBqHHv3EORHrSfbR9DqecPNn+AmuAoQxV6aL8Xu30bJMJR1o8PZLZzpk1Wq7/NfCbuhmakHTPYRhoqLsXRnw==", - "license": "ISC" - }, - "node_modules/@mapbox/jsonlint-lines-primitives": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", - "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@mapbox/mapbox-gl-supported": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-1.5.0.tgz", - "integrity": "sha512-/PT1P6DNf7vjEEiPkVIRJkvibbqWtqnyGaBz3nfRdcxclNSnSdaLU5tfAgcD7I8Yt5i+L19s406YLl1koLnLbg==", - "license": "BSD-3-Clause", - "peerDependencies": { - "mapbox-gl": ">=0.32.1 <2.0.0" - } - }, - "node_modules/@mapbox/point-geometry": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", - "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==", - "license": "ISC" - }, - "node_modules/@mapbox/tiny-sdf": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-1.2.5.tgz", - "integrity": "sha512-cD8A/zJlm6fdJOk6DqPUV8mcpyJkRz2x2R+/fYcWDYG3oWbG7/L7Yl/WqQ1VZCjnL9OTIMAn6c+BC5Eru4sQEw==", - "license": "BSD-2-Clause" - }, - "node_modules/@mapbox/unitbezier": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz", - "integrity": "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==", - "license": "BSD-2-Clause" - }, - "node_modules/@mapbox/vector-tile": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz", - "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==", - "license": "BSD-3-Clause", - "dependencies": { - "@mapbox/point-geometry": "~0.1.0" - } - }, - "node_modules/@mapbox/whoots-js": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", - "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==", - "license": "ISC", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@maplibre/maplibre-gl-style-spec": { - "version": "20.4.0", - "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-20.4.0.tgz", - "integrity": "sha512-AzBy3095fTFPjDjmWpR2w6HVRAZJ6hQZUCwk5Plz6EyfnfuQW1odeW5i2Ai47Y6TBA2hQnC+azscjBSALpaWgw==", - "license": "ISC", - "dependencies": { - "@mapbox/jsonlint-lines-primitives": "~2.0.2", - "@mapbox/unitbezier": "^0.0.1", - "json-stringify-pretty-compact": "^4.0.0", - "minimist": "^1.2.8", - "quickselect": "^2.0.0", - "rw": "^1.3.3", - "tinyqueue": "^3.0.0" - }, - "bin": { - "gl-style-format": "dist/gl-style-format.mjs", - "gl-style-migrate": "dist/gl-style-migrate.mjs", - "gl-style-validate": "dist/gl-style-validate.mjs" - } - }, - "node_modules/@maplibre/maplibre-gl-style-spec/node_modules/@mapbox/unitbezier": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", - "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", - "license": "BSD-2-Clause" - }, - "node_modules/@maplibre/maplibre-gl-style-spec/node_modules/tinyqueue": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", - "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", - "license": "ISC" - }, "node_modules/@module-federation/error-codes": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.11.2.tgz", @@ -3954,92 +3914,6 @@ "url": "https://opencollective.com/unts" } }, - "node_modules/@plotly/d3": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/@plotly/d3/-/d3-3.8.2.tgz", - "integrity": "sha512-wvsNmh1GYjyJfyEBPKJLTMzgf2c2bEbSIL50lmqVUi+o1NHaLPi1Lb4v7VxXXJn043BhNyrxUrWI85Q+zmjOVA==", - "license": "BSD-3-Clause" - }, - "node_modules/@plotly/d3-sankey": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@plotly/d3-sankey/-/d3-sankey-0.7.2.tgz", - "integrity": "sha512-2jdVos1N3mMp3QW0k2q1ph7Gd6j5PY1YihBrwpkFnKqO+cqtZq3AdEYUeSGXMeLsBDQYiqTVcihYfk8vr5tqhw==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "1", - "d3-collection": "1", - "d3-shape": "^1.2.0" - } - }, - "node_modules/@plotly/d3-sankey-circular": { - "version": "0.33.1", - "resolved": "https://registry.npmjs.org/@plotly/d3-sankey-circular/-/d3-sankey-circular-0.33.1.tgz", - "integrity": "sha512-FgBV1HEvCr3DV7RHhDsPXyryknucxtfnLwPtCKKxdolKyTFYoLX/ibEfX39iFYIL7DYbVeRtP43dbFcrHNE+KQ==", - "license": "MIT", - "dependencies": { - "d3-array": "^1.2.1", - "d3-collection": "^1.0.4", - "d3-shape": "^1.2.0", - "elementary-circuits-directed-graph": "^1.0.4" - } - }, - "node_modules/@plotly/mapbox-gl": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/@plotly/mapbox-gl/-/mapbox-gl-1.13.4.tgz", - "integrity": "sha512-sR3/Pe5LqT/fhYgp4rT4aSFf1rTsxMbGiH6Hojc7PH36ny5Bn17iVFUjpzycafETURuFbLZUfjODO8LvSI+5zQ==", - "license": "SEE LICENSE IN LICENSE.txt", - "dependencies": { - "@mapbox/geojson-rewind": "^0.5.2", - "@mapbox/geojson-types": "^1.0.2", - "@mapbox/jsonlint-lines-primitives": "^2.0.2", - "@mapbox/mapbox-gl-supported": "^1.5.0", - "@mapbox/point-geometry": "^0.1.0", - "@mapbox/tiny-sdf": "^1.1.1", - "@mapbox/unitbezier": "^0.0.0", - "@mapbox/vector-tile": "^1.3.1", - "@mapbox/whoots-js": "^3.1.0", - "csscolorparser": "~1.0.3", - "earcut": "^2.2.2", - "geojson-vt": "^3.2.1", - "gl-matrix": "^3.2.1", - "grid-index": "^1.1.0", - "murmurhash-js": "^1.0.0", - "pbf": "^3.2.1", - "potpack": "^1.0.1", - "quickselect": "^2.0.0", - "rw": "^1.3.3", - "supercluster": "^7.1.0", - "tinyqueue": "^2.0.3", - "vt-pbf": "^3.1.1" - }, - "engines": { - "node": ">=6.4.0" - } - }, - "node_modules/@plotly/point-cluster": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/@plotly/point-cluster/-/point-cluster-3.1.9.tgz", - "integrity": "sha512-MwaI6g9scKf68Orpr1pHZ597pYx9uP8UEFXLPbsCmuw3a84obwz6pnMXGc90VhgDNeNiLEdlmuK7CPo+5PIxXw==", - "license": "MIT", - "dependencies": { - "array-bounds": "^1.0.1", - "binary-search-bounds": "^2.0.4", - "clamp": "^1.0.1", - "defined": "^1.0.0", - "dtype": "^2.0.0", - "flatten-vertex-data": "^1.0.2", - "is-obj": "^1.0.1", - "math-log2": "^1.0.1", - "parse-rect": "^1.2.0", - "pick-by-alias": "^1.2.0" - } - }, - "node_modules/@plotly/regl": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@plotly/regl/-/regl-2.1.2.tgz", - "integrity": "sha512-Mdk+vUACbQvjd0m/1JJjOOafmkp/EpmHjISsopEz5Av44CBq7rPC05HHNbYGKVyNUF2zmEoBS/TT0pd0SPFFyw==", - "license": "MIT" - }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.4.3.tgz", @@ -4104,7 +3978,6 @@ "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -4423,7 +4296,6 @@ "resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.3.2.tgz", "integrity": "sha512-QbEn1SkNW3b89KTlSkp6OHdvw3DhpL6tSdDhsOlldw3LoRBy4fx80Z9W9lmg+g+8DjTAs1Z1ysElEFtAN69AZg==", "dev": true, - "peer": true, "dependencies": { "@module-federation/runtime-tools": "0.11.2", "@rspack/binding": "1.3.2", @@ -4474,7 +4346,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -5113,101 +4984,6 @@ "node": ">= 6" } }, - "node_modules/@turf/area": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@turf/area/-/area-7.3.1.tgz", - "integrity": "sha512-9nSiwt4zB5QDMcSoTxF28WpK1f741MNKcpUJDiHVRX08CZ4qfGWGV9ZIPQ8TVEn5RE4LyYkFuQ47Z9pdEUZE9Q==", - "license": "MIT", - "dependencies": { - "@turf/helpers": "7.3.1", - "@turf/meta": "7.3.1", - "@types/geojson": "^7946.0.10", - "tslib": "^2.8.1" - }, - "funding": { - "url": "https://opencollective.com/turf" - } - }, - "node_modules/@turf/area/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/@turf/bbox": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@turf/bbox/-/bbox-7.3.1.tgz", - "integrity": "sha512-/IyMKoS7P9B0ch5PIlQ6gMfoE8gRr48+cSbzlyexvEjuDuaAV1VURjH1jAthS0ipFG8RrFxFJKnp7TLL1Skong==", - "license": "MIT", - "dependencies": { - "@turf/helpers": "7.3.1", - "@turf/meta": "7.3.1", - "@types/geojson": "^7946.0.10", - "tslib": "^2.8.1" - }, - "funding": { - "url": "https://opencollective.com/turf" - } - }, - "node_modules/@turf/bbox/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/@turf/centroid": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@turf/centroid/-/centroid-7.3.1.tgz", - "integrity": "sha512-hRnsDdVBH4pX9mAjYympb2q5W8TCMUMNEjcRrAF7HTCyjIuRmjJf8vUtlzf7TTn9RXbsvPc1vtm3kLw20Jm8DQ==", - "license": "MIT", - "dependencies": { - "@turf/helpers": "7.3.1", - "@turf/meta": "7.3.1", - "@types/geojson": "^7946.0.10", - "tslib": "^2.8.1" - }, - "funding": { - "url": "https://opencollective.com/turf" - } - }, - "node_modules/@turf/centroid/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/@turf/helpers": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-7.3.1.tgz", - "integrity": "sha512-zkL34JVhi5XhsuMEO0MUTIIFEJ8yiW1InMu4hu/oRqamlY4mMoZql0viEmH6Dafh/p+zOl8OYvMJ3Vm3rFshgg==", - "license": "MIT", - "dependencies": { - "@types/geojson": "^7946.0.10", - "tslib": "^2.8.1" - }, - "funding": { - "url": "https://opencollective.com/turf" - } - }, - "node_modules/@turf/helpers/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/@turf/meta": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@turf/meta/-/meta-7.3.1.tgz", - "integrity": "sha512-NWsfOE5RVtWpLQNkfOF/RrYvLRPwwruxhZUV0UFIzHqfiRJ50aO9Y6uLY4bwCUe2TumLJQSR4yaoA72Rmr2mnQ==", - "license": "MIT", - "dependencies": { - "@turf/helpers": "7.3.1", - "@types/geojson": "^7946.0.10" - }, - "funding": { - "url": "https://opencollective.com/turf" - } - }, "node_modules/@tybys/wasm-util": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", @@ -5559,15 +5335,16 @@ "version": "3.7.7", "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "peer": true, "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==" + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" }, "node_modules/@types/express": { "version": "4.17.21", @@ -5610,15 +5387,6 @@ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==" }, - "node_modules/@types/geojson-vt": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@types/geojson-vt/-/geojson-vt-3.2.5.tgz", - "integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==", - "license": "MIT", - "dependencies": { - "@types/geojson": "*" - } - }, "node_modules/@types/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", @@ -5636,6 +5404,17 @@ "@types/node": "*" } }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz", + "integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==", + "dependencies": { + "hoist-non-react-statics": "^3.3.0" + }, + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/@types/html-minifier-terser": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz", @@ -5697,28 +5476,10 @@ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz", "integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==" }, - "node_modules/@types/mapbox__point-geometry": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz", - "integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==", - "license": "MIT" - }, - "node_modules/@types/mapbox__vector-tile": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz", - "integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==", - "license": "MIT", - "dependencies": { - "@types/geojson": "*", - "@types/mapbox__point-geometry": "*", - "@types/pbf": "*" - } - }, "node_modules/@types/markdown-it": { "version": "12.2.3", "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", - "peer": true, "dependencies": { "@types/linkify-it": "*", "@types/mdurl": "*" @@ -5769,12 +5530,6 @@ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" }, - "node_modules/@types/pbf": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz", - "integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==", - "license": "MIT" - }, "node_modules/@types/prettier": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", @@ -5806,7 +5561,6 @@ "version": "18.3.20", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.20.tgz", "integrity": "sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -5898,15 +5652,6 @@ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==" }, - "node_modules/@types/supercluster": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz", - "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==", - "license": "MIT", - "dependencies": { - "@types/geojson": "*" - } - }, "node_modules/@types/tapable": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.8.tgz", @@ -5938,7 +5683,6 @@ "version": "4.41.32", "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.32.tgz", "integrity": "sha512-cb+0ioil/7oz5//7tZUSwbrSAN/NWHrQylz5cW8G0dWTcF/g+/dSdMlKVZspBYuMAN1+WnwHrkxiRrLcwd0Heg==", - "peer": true, "dependencies": { "@types/node": "*", "@types/tapable": "^1", @@ -6047,7 +5791,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.28.0.tgz", "integrity": "sha512-LPcw1yHD3ToaDEoljFEfQ9j2xShY367h7FZ1sq5NJT9I3yj4LHer1Xd1yRSOdYy9BpsrxU7R+eoDokChYM53lQ==", "dev": true, - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.28.0", "@typescript-eslint/types": "8.28.0", @@ -6758,6 +6501,7 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "peer": true, "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", @@ -6767,12 +6511,14 @@ "node_modules/@webassemblyjs/helper-numbers/node_modules/@webassemblyjs/floating-point-hex-parser": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==" + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "peer": true }, "node_modules/@webassemblyjs/helper-numbers/node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==" + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "peer": true }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { "version": "1.9.0", @@ -6905,12 +6651,6 @@ "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==" }, - "node_modules/abs-svg-path": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz", - "integrity": "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==", - "license": "MIT" - }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -6927,7 +6667,6 @@ "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7007,7 +6746,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -7164,10 +6902,9 @@ } }, "node_modules/apexcharts": { - "version": "3.52.0", - "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.52.0.tgz", - "integrity": "sha512-7dg0ADKs8AA89iYMZMe2sFDG0XK5PfqllKV9N+i3hKHm3vEtdhwz8AlXGm+/b0nJ6jKiaXsqci5LfVxNhtB+dA==", - "peer": true, + "version": "3.54.1", + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.54.1.tgz", + "integrity": "sha512-E4et0h/J1U3r3EwS/WlqJCQIbepKbp6wGUmaAwJOMjHUP4Ci0gxanLa7FR3okx6p9coi4st6J853/Cb1NP0vpA==", "dependencies": { "@yr/monotone-cubic-spline": "^1.0.3", "svg.draggable.js": "^2.2.2", @@ -7234,12 +6971,6 @@ "node": ">=0.10.0" } }, - "node_modules/array-bounds": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-bounds/-/array-bounds-1.0.1.tgz", - "integrity": "sha512-8wdW3ZGk6UjMPJx/glyEt0sLzzwAE1bhToPsO1W2pbpR2gULyxe3BjSiuJFheP50T/GgODVPz2fuMUmIywt8cQ==", - "license": "MIT" - }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", @@ -7255,15 +6986,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/array-flatten": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", @@ -7288,27 +7010,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-normalize": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array-normalize/-/array-normalize-1.1.4.tgz", - "integrity": "sha512-fCp0wKFLjvSPmCn4F5Tiw4M3lpMZoHlCjfcs7nNzuj3vqQQ1/a8cgB9DXcpDSn18c+coLnaW7rqfcYCvKbyJXg==", - "license": "MIT", - "dependencies": { - "array-bounds": "^1.0.0" - } - }, - "node_modules/array-range": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-range/-/array-range-1.0.1.tgz", - "integrity": "sha512-shdaI1zT3CVNL2hnx9c0JMc0ZogGaxDs5e85akgHWKYa0yVbIyp06Ind3dVkTj/uuFrzaHBOyqFzo+VV6aXgtA==", - "license": "MIT" - }, - "node_modules/array-rearrange": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/array-rearrange/-/array-rearrange-2.2.2.tgz", - "integrity": "sha512-UfobP5N12Qm4Qu4fwLDIi2v6+wZsSf6snYSxAMeKhrh37YGnNWZPRmVEKc/2wfms53TLQnzfpG8wCx2Y/6NG1w==", - "license": "MIT" - }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -7661,7 +7362,6 @@ "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", "deprecated": "babel-eslint is now @babel/eslint-parser. This package will no longer receive updates.", - "peer": true, "dependencies": { "@babel/code-frame": "^7.0.0", "@babel/parser": "^7.7.0", @@ -8088,15 +7788,6 @@ "node": ">=0.10.0" } }, - "node_modules/base64-arraybuffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", - "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -8116,6 +7807,17 @@ } ] }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -8152,12 +7854,6 @@ "node": ">=8" } }, - "node_modules/binary-search-bounds": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/binary-search-bounds/-/binary-search-bounds-2.0.5.tgz", - "integrity": "sha512-H0ea4Fd3lS1+sTEB2TgcLoK21lLhwEJzlQv3IN47pJS976Gx4zoWe0ak3q+uYh60ppQxg9F16Ri4tS1sfD4+jA==", - "license": "MIT" - }, "node_modules/bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", @@ -8167,28 +7863,6 @@ "file-uri-to-path": "1.0.0" } }, - "node_modules/bit-twiddle": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bit-twiddle/-/bit-twiddle-1.0.2.tgz", - "integrity": "sha512-B9UhK0DKFZhoTFcfvAzhqsjStvGJp9vYWf3+6SNTtdSQnvIgfkHbgHrg/e4+TH71N2GDu8tpmCVoyfrL1d7ntA==", - "license": "MIT" - }, - "node_modules/bitmap-sdf": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/bitmap-sdf/-/bitmap-sdf-1.0.4.tgz", - "integrity": "sha512-1G3U4n5JE6RAiALMxu0p1XmeZkTeCwGKykzsLTCqVzfSDaN6S7fKnkIkfejogz+iwqBWc0UYAIKnKHNN7pSfDg==", - "license": "MIT" - }, - "node_modules/bl": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz", - "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==", - "license": "MIT", - "dependencies": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" - } - }, "node_modules/bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -8447,9 +8121,9 @@ } }, "node_modules/browserslist": { - "version": "4.25.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", + "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", "funding": [ { "type": "opencollective", @@ -8464,13 +8138,12 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", - "peer": true, "dependencies": { - "caniuse-lite": "^1.0.30001726", - "electron-to-chromium": "^1.5.173", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.8.19", + "caniuse-lite": "^1.0.30001751", + "electron-to-chromium": "^1.5.238", + "node-releases": "^2.0.26", + "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" @@ -8745,9 +8418,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001727", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", - "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "version": "1.0.30001751", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", + "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", "funding": [ { "type": "opencollective", @@ -8761,17 +8434,7 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ], - "license": "CC-BY-4.0" - }, - "node_modules/canvas-fit": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/canvas-fit/-/canvas-fit-1.5.0.tgz", - "integrity": "sha512-onIcjRpz69/Hx5bB5HGbYKUF2uC6QT6Gp+pfpGm3A7mPfcluSLV5v4Zu+oflDUwLdUw0rLIBhUbi0v8hM4FJQQ==", - "license": "MIT", - "dependencies": { - "element-size": "^1.1.1" - } + ] }, "node_modules/capture-exit": { "version": "2.0.0", @@ -8896,12 +8559,6 @@ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz", "integrity": "sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw==" }, - "node_modules/clamp": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/clamp/-/clamp-1.0.1.tgz", - "integrity": "sha512-kgMuFyE78OC6Dyu3Dy7vcx4uy97EIbVxJB/B0eJ3bUNAkwdNcxYzgKltnyADiYwsR7SEqkkUPsEUT//OVS6XMA==", - "license": "MIT" - }, "node_modules/class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -9025,24 +8682,6 @@ "color-string": "^1.6.0" } }, - "node_modules/color-alpha": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/color-alpha/-/color-alpha-1.0.4.tgz", - "integrity": "sha512-lr8/t5NPozTSqli+duAN+x+no/2WaKTeWvxhHGN+aXT6AJ8vPlzLa7UriyjWak0pSC2jHol9JgjBYnnHsGha9A==", - "license": "MIT", - "dependencies": { - "color-parse": "^1.3.8" - } - }, - "node_modules/color-alpha/node_modules/color-parse": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/color-parse/-/color-parse-1.4.3.tgz", - "integrity": "sha512-BADfVl/FHkQkyo8sRBwMYBqemqsgnu7JZAwUgvBvuwwuNUZAhSvLTbsEErS5bQXzOjDR0dWzJ4vXN2Q+QoPx0A==", - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0" - } - }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -9051,75 +8690,11 @@ "color-name": "1.1.3" } }, - "node_modules/color-id": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/color-id/-/color-id-1.1.0.tgz", - "integrity": "sha512-2iRtAn6dC/6/G7bBIo0uupVrIne1NsQJvJxZOBCzQOfk7jRq97feaDZ3RdzuHakRXXnHGNwglto3pqtRx1sX0g==", - "license": "MIT", - "dependencies": { - "clamp": "^1.0.1" - } - }, "node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, - "node_modules/color-normalize": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/color-normalize/-/color-normalize-1.5.0.tgz", - "integrity": "sha512-rUT/HDXMr6RFffrR53oX3HGWkDOP9goSAQGBkUaAYKjOE2JxozccdGyufageWDlInRAjm/jYPrf/Y38oa+7obw==", - "license": "MIT", - "dependencies": { - "clamp": "^1.0.1", - "color-rgba": "^2.1.1", - "dtype": "^2.0.0" - } - }, - "node_modules/color-normalize/node_modules/color-parse": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/color-parse/-/color-parse-1.4.3.tgz", - "integrity": "sha512-BADfVl/FHkQkyo8sRBwMYBqemqsgnu7JZAwUgvBvuwwuNUZAhSvLTbsEErS5bQXzOjDR0dWzJ4vXN2Q+QoPx0A==", - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0" - } - }, - "node_modules/color-normalize/node_modules/color-rgba": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/color-rgba/-/color-rgba-2.4.0.tgz", - "integrity": "sha512-Nti4qbzr/z2LbUWySr7H9dk3Rl7gZt7ihHAxlgT4Ho90EXWkjtkL1avTleu9yeGuqrt/chxTB6GKK8nZZ6V0+Q==", - "license": "MIT", - "dependencies": { - "color-parse": "^1.4.2", - "color-space": "^2.0.0" - } - }, - "node_modules/color-parse": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/color-parse/-/color-parse-2.0.0.tgz", - "integrity": "sha512-g2Z+QnWsdHLppAbrpcFWo629kLOnOPtpxYV69GCqm92gqSgyXbzlfyN3MXs0412fPBkFmiuS+rXposgBgBa6Kg==", - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0" - } - }, - "node_modules/color-rgba": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/color-rgba/-/color-rgba-3.0.0.tgz", - "integrity": "sha512-PPwZYkEY3M2THEHHV6Y95sGUie77S7X8v+h1r6LSAPF3/LL2xJ8duUXSrkic31Nzc4odPwHgUbiX/XuTYzQHQg==", - "license": "MIT", - "dependencies": { - "color-parse": "^2.0.0", - "color-space": "^2.0.0" - } - }, - "node_modules/color-space": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/color-space/-/color-space-2.3.2.tgz", - "integrity": "sha512-BcKnbOEsOarCwyoLstcoEztwT0IJxqqQkNwDuA3a65sICvvHL2yoeV13psoDFh5IuiOMnIOKdQDwB4Mk3BypiA==", - "license": "Unlicense" - }, "node_modules/color-string": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", @@ -9395,10 +8970,9 @@ } }, "node_modules/core-js-pure": { - "version": "3.23.2", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.23.2.tgz", - "integrity": "sha512-t6u7H4Ff/yZNk+zqTr74UjCcZ3k8ApBryeLLV4rYQd9aF3gqmjjGjjR44ENfeBMH8VVvSynIjAJ0mUuFhzQtrA==", - "deprecated": "core-js-pure@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js-pure.", + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.46.0.tgz", + "integrity": "sha512-NMCW30bHNofuhwLhYPt66OLOKTMbOhgTTatKVbaQC3KRHpTCiRIBYvtshr+NBYSnBxwAFhjW/RfJ0XbIjS16rw==", "hasInstallScript": true, "peer": true, "funding": { @@ -9426,12 +9000,6 @@ "node": ">=10" } }, - "node_modules/country-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/country-regex/-/country-regex-1.1.0.tgz", - "integrity": "sha512-iSPlClZP8vX7MC3/u6s3lrDuoQyhQukh5LyABJ3hvfzbQ3Yyayd4fp04zjLnfi267B/B2FkumcWWgrbban7sSA==", - "license": "MIT" - }, "node_modules/create-ecdh": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", @@ -9578,53 +9146,6 @@ "node": ">4" } }, - "node_modules/css-font": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/css-font/-/css-font-1.2.0.tgz", - "integrity": "sha512-V4U4Wps4dPDACJ4WpgofJ2RT5Yqwe1lEH6wlOOaIxMi0gTjdIijsc5FmxQlZ7ZZyKQkkutqqvULOp07l9c7ssA==", - "license": "MIT", - "dependencies": { - "css-font-size-keywords": "^1.0.0", - "css-font-stretch-keywords": "^1.0.1", - "css-font-style-keywords": "^1.0.1", - "css-font-weight-keywords": "^1.0.0", - "css-global-keywords": "^1.0.1", - "css-system-font-keywords": "^1.0.0", - "pick-by-alias": "^1.2.0", - "string-split-by": "^1.0.0", - "unquote": "^1.1.0" - } - }, - "node_modules/css-font-size-keywords": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-font-size-keywords/-/css-font-size-keywords-1.0.0.tgz", - "integrity": "sha512-Q+svMDbMlelgCfH/RVDKtTDaf5021O486ZThQPIpahnIjUkMUslC+WuOQSWTgGSrNCH08Y7tYNEmmy0hkfMI8Q==", - "license": "MIT" - }, - "node_modules/css-font-stretch-keywords": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/css-font-stretch-keywords/-/css-font-stretch-keywords-1.0.1.tgz", - "integrity": "sha512-KmugPO2BNqoyp9zmBIUGwt58UQSfyk1X5DbOlkb2pckDXFSAfjsD5wenb88fNrD6fvS+vu90a/tsPpb9vb0SLg==", - "license": "MIT" - }, - "node_modules/css-font-style-keywords": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/css-font-style-keywords/-/css-font-style-keywords-1.0.1.tgz", - "integrity": "sha512-0Fn0aTpcDktnR1RzaBYorIxQily85M2KXRpzmxQPgh8pxUN9Fcn00I8u9I3grNr1QXVgCl9T5Imx0ZwKU973Vg==", - "license": "MIT" - }, - "node_modules/css-font-weight-keywords": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-font-weight-keywords/-/css-font-weight-keywords-1.0.0.tgz", - "integrity": "sha512-5So8/NH+oDD+EzsnF4iaG4ZFHQ3vaViePkL1ZbZ5iC/KrsCY+WHq/lvOgrtmuOQ9pBBZ1ADGpaf+A4lj1Z9eYA==", - "license": "MIT" - }, - "node_modules/css-global-keywords": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/css-global-keywords/-/css-global-keywords-1.0.1.tgz", - "integrity": "sha512-X1xgQhkZ9n94WDwntqst5D/FKkmiU0GlJSFZSV3kLvyJ1WC5VeyoXDOuleUD+SIuH9C7W05is++0Woh0CGfKjQ==", - "license": "MIT" - }, "node_modules/css-has-pseudo": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-0.10.0.tgz", @@ -9736,7 +9257,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", @@ -9852,12 +9372,6 @@ "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==" }, - "node_modules/css-system-font-keywords": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-system-font-keywords/-/css-system-font-keywords-1.0.0.tgz", - "integrity": "sha512-1umTtVd/fXS25ftfjB71eASCrYhilmEsvDEI6wG/QplnmlfmVM5HkZ/ZX46DT5K3eblFPgLUHt5BRCb0YXkSFA==", - "license": "MIT" - }, "node_modules/css-tree": { "version": "1.0.0-alpha.37", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", @@ -9897,12 +9411,6 @@ "node": ">=0.10.0" } }, - "node_modules/csscolorparser": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz", - "integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==", - "license": "MIT" - }, "node_modules/cssdb": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-4.4.0.tgz", @@ -10178,12 +9686,6 @@ "node": ">=12" } }, - "node_modules/d3-array": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", - "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", - "license": "BSD-3-Clause" - }, "node_modules/d3-axis": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", @@ -10218,12 +9720,6 @@ "node": ">=12" } }, - "node_modules/d3-collection": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", - "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==", - "license": "BSD-3-Clause" - }, "node_modules/d3-color": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", @@ -10377,40 +9873,6 @@ "node": ">=12" } }, - "node_modules/d3-geo-projection": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/d3-geo-projection/-/d3-geo-projection-2.9.0.tgz", - "integrity": "sha512-ZULvK/zBn87of5rWAfFMc9mJOipeSo57O+BBitsKIXmU4rTVAnX1kSsJkE0R+TxY8pGNoM1nbyRRE7GYHhdOEQ==", - "license": "BSD-3-Clause", - "dependencies": { - "commander": "2", - "d3-array": "1", - "d3-geo": "^1.12.0", - "resolve": "^1.1.10" - }, - "bin": { - "geo2svg": "bin/geo2svg", - "geograticule": "bin/geograticule", - "geoproject": "bin/geoproject", - "geoquantize": "bin/geoquantize", - "geostitch": "bin/geostitch" - } - }, - "node_modules/d3-geo-projection/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT" - }, - "node_modules/d3-geo-projection/node_modules/d3-geo": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.12.1.tgz", - "integrity": "sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "1" - } - }, "node_modules/d3-geo/node_modules/d3-array": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.3.tgz", @@ -10487,7 +9949,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", - "peer": true, "engines": { "node": ">=12" } @@ -10981,15 +10442,6 @@ "node": ">=0.10.0" } }, - "node_modules/defined": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", - "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/del": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", @@ -11072,12 +10524,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/detect-kerning": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-kerning/-/detect-kerning-2.1.2.tgz", - "integrity": "sha512-I3JIbrnKPAntNLl1I6TpSQQdQ4AutYzv/sKMFKbepawV/hlH0GmYKhUoOEMd4xqaUHT+Bm0f4127lh5qs1m1tw==", - "license": "MIT" - }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -11329,25 +10775,6 @@ "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==" }, - "node_modules/draw-svg-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/draw-svg-path/-/draw-svg-path-1.0.0.tgz", - "integrity": "sha512-P8j3IHxcgRMcY6sDzr0QvJDLzBnJJqpTG33UZ2Pvp8rw0apCHhJCWqYprqrXjrgHnJ6tuhP1iTJSAodPDHxwkg==", - "license": "MIT", - "dependencies": { - "abs-svg-path": "~0.1.1", - "normalize-svg-path": "~0.1.0" - } - }, - "node_modules/dtype": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dtype/-/dtype-2.0.0.tgz", - "integrity": "sha512-s2YVcLKdFGS0hpFqJaTwscsyt0E8nNFdmo73Ocd81xNPj4URI4rj6D60A+vFMIw7BXWlb4yRkEwfBqcZzPGiZg==", - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -11361,12 +10788,6 @@ "node": ">= 0.4" } }, - "node_modules/dup": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dup/-/dup-1.0.0.tgz", - "integrity": "sha512-Bz5jxMMC0wgp23Zm15ip1x8IhYRqJvF3nFC0UInJUDkN1z4uNPk9jTnfCUJXbOGiQ1JbXLQsiV41Fb+HXcj5BA==", - "license": "MIT" - }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -11383,12 +10804,6 @@ "stream-shift": "^1.0.0" } }, - "node_modules/earcut": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", - "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==", - "license": "ISC" - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -11404,25 +10819,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.188", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.188.tgz", - "integrity": "sha512-pfEx5CBFAocOKNrc+i5fSvhDaI1Vr9R9aT5uX1IzM3hhdL6k649wfuUcdUd9EZnmbE1xdfA51CwqQ61CO3Xl3g==", - "license": "ISC" - }, - "node_modules/element-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/element-size/-/element-size-1.1.1.tgz", - "integrity": "sha512-eaN+GMOq/Q+BIWy0ybsgpcYImjGIdNLyjLFJU4XsLHXYQao5jCNb36GyN6C2qwmDDYSfIBmKpPpr4VnBdLCsPQ==", - "license": "MIT" - }, - "node_modules/elementary-circuits-directed-graph": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/elementary-circuits-directed-graph/-/elementary-circuits-directed-graph-1.3.1.tgz", - "integrity": "sha512-ZEiB5qkn2adYmpXGnJKkxT8uJHlW/mxmBpmeqawEHzPxh9HkLD4/1mFYX5l0On+f6rcPIt8/EWlRU2Vo3fX6dQ==", - "license": "MIT", - "dependencies": { - "strongly-connected-components": "^1.0.1" - } + "version": "1.5.240", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.240.tgz", + "integrity": "sha512-OBwbZjWgrCOH+g6uJsA2/7Twpas2OlepS9uvByJjR2datRDuKGYeD+nP8lBBks2qnB7bGJNHDUx7c/YLaT3QMQ==" }, "node_modules/elliptic": { "version": "6.6.1", @@ -11766,18 +11165,6 @@ "ext": "^1.1.2" } }, - "node_modules/es6-weak-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", - "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", - "license": "ISC", - "dependencies": { - "d": "1", - "es5-ext": "^0.10.46", - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.1" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -11800,14 +11187,14 @@ } }, "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "license": "BSD-2-Clause", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", + "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", - "esutils": "^2.0.2" + "esutils": "^2.0.2", + "optionator": "^0.8.1" }, "bin": { "escodegen": "bin/escodegen.js", @@ -11842,7 +11229,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -12033,7 +11419,6 @@ "version": "2.31.0", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", @@ -12067,7 +11452,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.9.3.tgz", "integrity": "sha512-NrPUarxpFzGpQVXdVWkGttDD8WIxBuM/dRNw5kKFxrlGdjAJ3l8ma0LK5hsK5Qp79GBGM+HY1zYVbHqateTklA==", "dev": true, - "peer": true, "dependencies": { "@types/doctrine": "^0.0.9", "@typescript-eslint/utils": "^8.28.0", @@ -12238,7 +11622,6 @@ "version": "6.10.2", "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", - "peer": true, "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", @@ -12272,7 +11655,6 @@ "version": "7.37.4", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz", "integrity": "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==", - "peer": true, "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -13232,25 +12614,6 @@ "node": ">=0.10.0" } }, - "node_modules/falafel": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/falafel/-/falafel-2.2.5.tgz", - "integrity": "sha512-HuC1qF9iTnHDnML9YZAdCDQwT0yKl/U55K4XSUXqGAA2GLoafFgWRqdAbhWJxXaYD4pyoVxAJ8wH670jMpI9DQ==", - "license": "MIT", - "dependencies": { - "acorn": "^7.1.1", - "isarray": "^2.0.1" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/falafel/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -13279,15 +12642,6 @@ "node": ">=8.6.0" } }, - "node_modules/fast-isnumeric": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/fast-isnumeric/-/fast-isnumeric-1.1.4.tgz", - "integrity": "sha512-1mM8qOr2LYz8zGaUdmiqRDiuue00Dxjgcb1NQR7TnhLVh6sQyngP9xvLo7Sl7LZpP/sk5eb+bcyWXw530NTBZw==", - "license": "MIT", - "dependencies": { - "is-string-blank": "^1.0.1" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -13485,15 +12839,6 @@ "integrity": "sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==", "deprecated": "flatten is deprecated in favor of utility frameworks such as lodash." }, - "node_modules/flatten-vertex-data": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/flatten-vertex-data/-/flatten-vertex-data-1.0.2.tgz", - "integrity": "sha512-BvCBFK2NZqerFTdMDgqfHBwxYWnxeCkwONsw6PvBMcUXqo8U/KDWwmXhqx1x2kLIg7DqIsJfOaJFOmlua3Lxuw==", - "license": "MIT", - "dependencies": { - "dtype": "^2.0.0" - } - }, "node_modules/flush-write-stream": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", @@ -13530,24 +12875,6 @@ "jquery": "^3.4.0" } }, - "node_modules/font-atlas": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/font-atlas/-/font-atlas-2.1.0.tgz", - "integrity": "sha512-kP3AmvX+HJpW4w3d+PiPR2X6E1yvsBXt2yhuCw+yReO9F1WYhvZwx3c95DGZGwg9xYzDGrgJYa885xmVA+28Cg==", - "license": "MIT", - "dependencies": { - "css-font": "^1.0.0" - } - }, - "node_modules/font-measure": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/font-measure/-/font-measure-1.2.2.tgz", - "integrity": "sha512-mRLEpdrWzKe9hbfaF3Qpr06TAjquuBVP5cHy4b3hyeNdjc9i0PO6HniGsX5vjL5OWv7+Bd++NiooNpT/s8BvIA==", - "license": "MIT", - "dependencies": { - "css-font": "^1.2.0" - } - }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -13870,12 +13197,6 @@ "node": ">=6.9.0" } }, - "node_modules/geojson-vt": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz", - "integrity": "sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==", - "license": "ISC" - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -13884,12 +13205,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-canvas-context": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-canvas-context/-/get-canvas-context-1.0.2.tgz", - "integrity": "sha512-LnpfLf/TNzr9zVOGiIY6aKCz8EKuXmlYNV7CM2pUjBa/B+c2I15tS7KLySep75+FuerJdmArvJLcsAXWEy2H0A==", - "license": "MIT" - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -13988,58 +13303,6 @@ "node": ">=0.10.0" } }, - "node_modules/gl-mat4": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gl-mat4/-/gl-mat4-1.2.0.tgz", - "integrity": "sha512-sT5C0pwB1/e9G9AvAoLsoaJtbMGjfd/jfxo8jMCKqYYEnjZuFvqV5rehqar0538EmssjdDeiEWnKyBSTw7quoA==", - "license": "Zlib" - }, - "node_modules/gl-matrix": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz", - "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==", - "license": "MIT" - }, - "node_modules/gl-text": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/gl-text/-/gl-text-1.4.0.tgz", - "integrity": "sha512-o47+XBqLCj1efmuNyCHt7/UEJmB9l66ql7pnobD6p+sgmBUdzfMZXIF0zD2+KRfpd99DJN+QXdvTFAGCKCVSmQ==", - "license": "MIT", - "dependencies": { - "bit-twiddle": "^1.0.2", - "color-normalize": "^1.5.0", - "css-font": "^1.2.0", - "detect-kerning": "^2.1.2", - "es6-weak-map": "^2.0.3", - "flatten-vertex-data": "^1.0.2", - "font-atlas": "^2.1.0", - "font-measure": "^1.2.2", - "gl-util": "^3.1.2", - "is-plain-obj": "^1.1.0", - "object-assign": "^4.1.1", - "parse-rect": "^1.2.0", - "parse-unit": "^1.0.1", - "pick-by-alias": "^1.2.0", - "regl": "^2.0.0", - "to-px": "^1.0.1", - "typedarray-pool": "^1.1.0" - } - }, - "node_modules/gl-util": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/gl-util/-/gl-util-3.1.3.tgz", - "integrity": "sha512-dvRTggw5MSkJnCbh74jZzSoTOGnVYK+Bt+Ckqm39CVcl6+zSsxqWk4lr5NKhkqXHL6qvZAU9h17ZF8mIskY9mA==", - "license": "MIT", - "dependencies": { - "is-browser": "^2.0.1", - "is-firefox": "^1.0.3", - "is-plain-obj": "^1.1.0", - "number-is-integer": "^1.0.1", - "object-assign": "^4.1.0", - "pick-by-alias": "^1.2.0", - "weak-map": "^1.0.5" - } - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -14073,7 +13336,8 @@ "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "peer": true }, "node_modules/global-modules": { "version": "2.0.0", @@ -14145,216 +13409,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/glsl-inject-defines": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/glsl-inject-defines/-/glsl-inject-defines-1.0.3.tgz", - "integrity": "sha512-W49jIhuDtF6w+7wCMcClk27a2hq8znvHtlGnrYkSWEr8tHe9eA2dcnohlcAmxLYBSpSSdzOkRdyPTrx9fw49+A==", - "license": "MIT", - "dependencies": { - "glsl-token-inject-block": "^1.0.0", - "glsl-token-string": "^1.0.1", - "glsl-tokenizer": "^2.0.2" - } - }, - "node_modules/glsl-resolve": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/glsl-resolve/-/glsl-resolve-0.0.1.tgz", - "integrity": "sha512-xxFNsfnhZTK9NBhzJjSBGX6IOqYpvBHxxmo+4vapiljyGNCY0Bekzn0firQkQrazK59c1hYxMDxYS8MDlhw4gA==", - "license": "MIT", - "dependencies": { - "resolve": "^0.6.1", - "xtend": "^2.1.2" - } - }, - "node_modules/glsl-resolve/node_modules/resolve": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-0.6.3.tgz", - "integrity": "sha512-UHBY3viPlJKf85YijDUcikKX6tmF4SokIDp518ZDVT92JNDcG5uKIthaT/owt3Sar0lwtOafsQuwrg22/v2Dwg==", - "license": "MIT" - }, - "node_modules/glsl-resolve/node_modules/xtend": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.2.0.tgz", - "integrity": "sha512-SLt5uylT+4aoXxXuwtQp5ZnMMzhDb1Xkg4pEqc00WUJCQifPfV9Ub1VrNhp9kXkrjZD2I2Hl8WnjP37jzZLPZw==", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/glsl-token-assignments": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/glsl-token-assignments/-/glsl-token-assignments-2.0.2.tgz", - "integrity": "sha512-OwXrxixCyHzzA0U2g4btSNAyB2Dx8XrztY5aVUCjRSh4/D0WoJn8Qdps7Xub3sz6zE73W3szLrmWtQ7QMpeHEQ==", - "license": "MIT" - }, - "node_modules/glsl-token-defines": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/glsl-token-defines/-/glsl-token-defines-1.0.0.tgz", - "integrity": "sha512-Vb5QMVeLjmOwvvOJuPNg3vnRlffscq2/qvIuTpMzuO/7s5kT+63iL6Dfo2FYLWbzuiycWpbC0/KV0biqFwHxaQ==", - "license": "MIT", - "dependencies": { - "glsl-tokenizer": "^2.0.0" - } - }, - "node_modules/glsl-token-depth": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/glsl-token-depth/-/glsl-token-depth-1.1.2.tgz", - "integrity": "sha512-eQnIBLc7vFf8axF9aoi/xW37LSWd2hCQr/3sZui8aBJnksq9C7zMeUYHVJWMhFzXrBU7fgIqni4EhXVW4/krpg==", - "license": "MIT" - }, - "node_modules/glsl-token-descope": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/glsl-token-descope/-/glsl-token-descope-1.0.2.tgz", - "integrity": "sha512-kS2PTWkvi/YOeicVjXGgX5j7+8N7e56srNDEHDTVZ1dcESmbmpmgrnpjPcjxJjMxh56mSXYoFdZqb90gXkGjQw==", - "license": "MIT", - "dependencies": { - "glsl-token-assignments": "^2.0.0", - "glsl-token-depth": "^1.1.0", - "glsl-token-properties": "^1.0.0", - "glsl-token-scope": "^1.1.0" - } - }, - "node_modules/glsl-token-inject-block": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/glsl-token-inject-block/-/glsl-token-inject-block-1.1.0.tgz", - "integrity": "sha512-q/m+ukdUBuHCOtLhSr0uFb/qYQr4/oKrPSdIK2C4TD+qLaJvqM9wfXIF/OOBjuSA3pUoYHurVRNao6LTVVUPWA==", - "license": "MIT" - }, - "node_modules/glsl-token-properties": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/glsl-token-properties/-/glsl-token-properties-1.0.1.tgz", - "integrity": "sha512-dSeW1cOIzbuUoYH0y+nxzwK9S9O3wsjttkq5ij9ZGw0OS41BirKJzzH48VLm8qLg+au6b0sINxGC0IrGwtQUcA==", - "license": "MIT" - }, - "node_modules/glsl-token-scope": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/glsl-token-scope/-/glsl-token-scope-1.1.2.tgz", - "integrity": "sha512-YKyOMk1B/tz9BwYUdfDoHvMIYTGtVv2vbDSLh94PT4+f87z21FVdou1KNKgF+nECBTo0fJ20dpm0B1vZB1Q03A==", - "license": "MIT" - }, - "node_modules/glsl-token-string": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/glsl-token-string/-/glsl-token-string-1.0.1.tgz", - "integrity": "sha512-1mtQ47Uxd47wrovl+T6RshKGkRRCYWhnELmkEcUAPALWGTFe2XZpH3r45XAwL2B6v+l0KNsCnoaZCSnhzKEksg==", - "license": "MIT" - }, - "node_modules/glsl-token-whitespace-trim": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/glsl-token-whitespace-trim/-/glsl-token-whitespace-trim-1.0.0.tgz", - "integrity": "sha512-ZJtsPut/aDaUdLUNtmBYhaCmhIjpKNg7IgZSfX5wFReMc2vnj8zok+gB/3Quqs0TsBSX/fGnqUUYZDqyuc2xLQ==", - "license": "MIT" - }, - "node_modules/glsl-tokenizer": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/glsl-tokenizer/-/glsl-tokenizer-2.1.5.tgz", - "integrity": "sha512-XSZEJ/i4dmz3Pmbnpsy3cKh7cotvFlBiZnDOwnj/05EwNp2XrhQ4XKJxT7/pDt4kp4YcpRSKz8eTV7S+mwV6MA==", - "license": "MIT", - "dependencies": { - "through2": "^0.6.3" - } - }, - "node_modules/glsl-tokenizer/node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", - "license": "MIT" - }, - "node_modules/glsl-tokenizer/node_modules/readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "node_modules/glsl-tokenizer/node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", - "license": "MIT" - }, - "node_modules/glsl-tokenizer/node_modules/through2": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", - "integrity": "sha512-RkK/CCESdTKQZHdmKICijdKKsCRVHs5KsLZ6pACAmF/1GPUQhonHSXWNERctxEp7RmvjdNbZTL5z9V7nSCXKcg==", - "license": "MIT", - "dependencies": { - "readable-stream": ">=1.0.33-1 <1.1.0-0", - "xtend": ">=4.0.0 <4.1.0-0" - } - }, - "node_modules/glslify": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/glslify/-/glslify-7.1.1.tgz", - "integrity": "sha512-bud98CJ6kGZcP9Yxcsi7Iz647wuDz3oN+IZsjCRi5X1PI7t/xPKeL0mOwXJjo+CRZMqvq0CkSJiywCcY7kVYog==", - "license": "MIT", - "dependencies": { - "bl": "^2.2.1", - "concat-stream": "^1.5.2", - "duplexify": "^3.4.5", - "falafel": "^2.1.0", - "from2": "^2.3.0", - "glsl-resolve": "0.0.1", - "glsl-token-whitespace-trim": "^1.0.0", - "glslify-bundle": "^5.0.0", - "glslify-deps": "^1.2.5", - "minimist": "^1.2.5", - "resolve": "^1.1.5", - "stack-trace": "0.0.9", - "static-eval": "^2.0.5", - "through2": "^2.0.1", - "xtend": "^4.0.0" - }, - "bin": { - "glslify": "bin.js" - } - }, - "node_modules/glslify-bundle": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glslify-bundle/-/glslify-bundle-5.1.1.tgz", - "integrity": "sha512-plaAOQPv62M1r3OsWf2UbjN0hUYAB7Aph5bfH58VxJZJhloRNbxOL9tl/7H71K7OLJoSJ2ZqWOKk3ttQ6wy24A==", - "license": "MIT", - "dependencies": { - "glsl-inject-defines": "^1.0.1", - "glsl-token-defines": "^1.0.0", - "glsl-token-depth": "^1.1.1", - "glsl-token-descope": "^1.0.2", - "glsl-token-scope": "^1.1.1", - "glsl-token-string": "^1.0.1", - "glsl-token-whitespace-trim": "^1.0.0", - "glsl-tokenizer": "^2.0.2", - "murmurhash-js": "^1.0.0", - "shallow-copy": "0.0.1" - } - }, - "node_modules/glslify-deps": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/glslify-deps/-/glslify-deps-1.3.2.tgz", - "integrity": "sha512-7S7IkHWygJRjcawveXQjRXLO2FTjijPDYC7QfZyAQanY+yGLCFHYnPtsGT9bdyHiwPTw/5a1m1M9hamT2aBpag==", - "license": "ISC", - "dependencies": { - "@choojs/findup": "^0.2.0", - "events": "^3.2.0", - "glsl-resolve": "0.0.1", - "glsl-tokenizer": "^2.0.0", - "graceful-fs": "^4.1.2", - "inherits": "^2.0.1", - "map-limit": "0.0.1", - "resolve": "^1.0.0" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/graceful-fs": { @@ -14368,12 +13431,6 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "node_modules/grid-index": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz", - "integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==", - "license": "ISC" - }, "node_modules/growly": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", @@ -14437,24 +13494,6 @@ "node": ">=4" } }, - "node_modules/has-hover": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-hover/-/has-hover-1.0.1.tgz", - "integrity": "sha512-0G6w7LnlcpyDzpeGUTuT0CEw05+QlMuGVk1IHNAlHrGJITGodjZu3x8BNDUMfKJSZXNB2ZAclqc1bvrd+uUpfg==", - "license": "MIT", - "dependencies": { - "is-browser": "^2.0.1" - } - }, - "node_modules/has-passive-events": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-passive-events/-/has-passive-events-1.0.0.tgz", - "integrity": "sha512-2vSj6IeIsgvsRMyeQ0JaCX5Q3lX4zMn5HpoVc7MEhQ6pv8Iq9rsXjsp+E5ZwaT7T0xhMT0KmU8gtt1EFVdbJiw==", - "license": "MIT", - "dependencies": { - "is-browser": "^2.0.1" - } - }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", @@ -14668,6 +13707,14 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/hoopy": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", @@ -15308,6 +14355,22 @@ "node": ">=10.13.0" } }, + "node_modules/intl-messageformat": { + "version": "10.7.18", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.18.tgz", + "integrity": "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/icu-messageformat-parser": "2.11.4", + "tslib": "^2.8.0" + } + }, + "node_modules/intl-messageformat/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, "node_modules/ip": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", @@ -15455,12 +14518,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-browser": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-browser/-/is-browser-2.1.0.tgz", - "integrity": "sha512-F5rTJxDQ2sW81fcfOR1GnCXT6sVJC104fCyfj+mjpwNEwaPYSn5fte5jiHmBg3DHsIoL/l8Kvw5VN5SsTRcRFQ==", - "license": "MIT" - }, "node_modules/is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", @@ -15650,27 +14707,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-finite": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", - "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-firefox": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-firefox/-/is-firefox-1.0.3.tgz", - "integrity": "sha512-6Q9ITjvWIm0Xdqv+5U12wgOKEM2KoBw4Y926m0OFkvlCxnbG94HKAsVz8w3fWcfAS5YA2fJORXX1dLrkprCCxA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -15715,15 +14751,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-iexplorer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-iexplorer/-/is-iexplorer-1.0.0.tgz", - "integrity": "sha512-YeLzceuwg3K6O0MLM3UyUUjKAlyULetwryFp1mHy1I5PfArK0AEqlfa+MR4gkJjcbuJXoDJCvXbyqZVf5CR2Sg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-inside-container": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", @@ -15768,12 +14795,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-mobile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-4.0.0.tgz", - "integrity": "sha512-mlcHZA84t1qLSuWkt2v0I2l61PYdyQDt4aG1mLIXF5FDMm4+haBCxCPYSr/uwqQNRk1MiTizn0ypEuRAOLRAew==", - "license": "MIT" - }, "node_modules/is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", @@ -15978,18 +14999,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-string-blank": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-string-blank/-/is-string-blank-1.0.1.tgz", - "integrity": "sha512-9H+ZBCVs3L9OYqv8nuUAzpcT9OTgMD1yAWrG7ihlnibdkbtB850heAmYWxHuXc4CHy4lKeK69tN+ny1K7gBIrw==", - "license": "MIT" - }, - "node_modules/is-svg-path": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-svg-path/-/is-svg-path-1.0.2.tgz", - "integrity": "sha512-Lj4vePmqpPR1ZnRctHv8ltSh1OrSxHkhUkd7wi+VQdcdP15/KvQFyk7LhNuM7ZW0EVbJz8kZLVmL9quLrfq4Kg==", - "license": "MIT" - }, "node_modules/is-symbol": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", @@ -16242,7 +15251,6 @@ "version": "26.6.0", "resolved": "https://registry.npmjs.org/jest/-/jest-26.6.0.tgz", "integrity": "sha512-jxTmrvuecVISvKFFhOkjsWRZV7sFqdSUAd1ajOKY+/QE/aLBVstsJ/dX8GczLzwiT6ZEwwmZqtCUHLHHQVzcfA==", - "peer": true, "dependencies": { "@jest/core": "^26.6.0", "import-local": "^3.0.2", @@ -18693,12 +17701,6 @@ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" }, - "node_modules/json-stringify-pretty-compact": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", - "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", - "license": "MIT" - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -18735,12 +17737,6 @@ "node": ">=4.0" } }, - "node_modules/kdbush": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", - "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==", - "license": "ISC" - }, "node_modules/keyboard-key": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/keyboard-key/-/keyboard-key-1.1.0.tgz", @@ -18864,6 +17860,18 @@ "node": ">=6" } }, + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -19077,24 +18085,6 @@ "node": ">=0.10.0" } }, - "node_modules/map-limit": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/map-limit/-/map-limit-0.0.1.tgz", - "integrity": "sha512-pJpcfLPnIF/Sk3taPW21G/RQsEEirGaFpCW3oXRwH9dnFHPHNGjNyvh++rdmC2fNqEaTw2MhYJraoJWAHx8kEg==", - "license": "MIT", - "dependencies": { - "once": "~1.3.0" - } - }, - "node_modules/map-limit/node_modules/once": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", - "integrity": "sha512-6vaNInhu+CHxtONf3zw3vq4SP2DOQhjBvIa3rNcG0+P7eKWlYH6Peu7rHizSloRU2EwMz6GraLieis9Ac9+p1w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/map-obj": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", @@ -19117,184 +18107,10 @@ "node": ">=0.10.0" } }, - "node_modules/mapbox-gl": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-1.13.3.tgz", - "integrity": "sha512-p8lJFEiqmEQlyv+DQxFAOG/XPWN0Wp7j/Psq93Zywz7qt9CcUKFYDBOoOEKzqe6gudHVJY8/Bhqw6VDpX2lSBg==", - "license": "SEE LICENSE IN LICENSE.txt", - "peer": true, - "dependencies": { - "@mapbox/geojson-rewind": "^0.5.2", - "@mapbox/geojson-types": "^1.0.2", - "@mapbox/jsonlint-lines-primitives": "^2.0.2", - "@mapbox/mapbox-gl-supported": "^1.5.0", - "@mapbox/point-geometry": "^0.1.0", - "@mapbox/tiny-sdf": "^1.1.1", - "@mapbox/unitbezier": "^0.0.0", - "@mapbox/vector-tile": "^1.3.1", - "@mapbox/whoots-js": "^3.1.0", - "csscolorparser": "~1.0.3", - "earcut": "^2.2.2", - "geojson-vt": "^3.2.1", - "gl-matrix": "^3.2.1", - "grid-index": "^1.1.0", - "murmurhash-js": "^1.0.0", - "pbf": "^3.2.1", - "potpack": "^1.0.1", - "quickselect": "^2.0.0", - "rw": "^1.3.3", - "supercluster": "^7.1.0", - "tinyqueue": "^2.0.3", - "vt-pbf": "^3.1.1" - }, - "engines": { - "node": ">=6.4.0" - } - }, - "node_modules/maplibre-gl": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.7.1.tgz", - "integrity": "sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==", - "license": "BSD-3-Clause", - "dependencies": { - "@mapbox/geojson-rewind": "^0.5.2", - "@mapbox/jsonlint-lines-primitives": "^2.0.2", - "@mapbox/point-geometry": "^0.1.0", - "@mapbox/tiny-sdf": "^2.0.6", - "@mapbox/unitbezier": "^0.0.1", - "@mapbox/vector-tile": "^1.3.1", - "@mapbox/whoots-js": "^3.1.0", - "@maplibre/maplibre-gl-style-spec": "^20.3.1", - "@types/geojson": "^7946.0.14", - "@types/geojson-vt": "3.2.5", - "@types/mapbox__point-geometry": "^0.1.4", - "@types/mapbox__vector-tile": "^1.3.4", - "@types/pbf": "^3.0.5", - "@types/supercluster": "^7.1.3", - "earcut": "^3.0.0", - "geojson-vt": "^4.0.2", - "gl-matrix": "^3.4.3", - "global-prefix": "^4.0.0", - "kdbush": "^4.0.2", - "murmurhash-js": "^1.0.0", - "pbf": "^3.3.0", - "potpack": "^2.0.0", - "quickselect": "^3.0.0", - "supercluster": "^8.0.1", - "tinyqueue": "^3.0.0", - "vt-pbf": "^3.1.3" - }, - "engines": { - "node": ">=16.14.0", - "npm": ">=8.1.0" - }, - "funding": { - "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1" - } - }, - "node_modules/maplibre-gl/node_modules/@mapbox/tiny-sdf": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.7.tgz", - "integrity": "sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug==", - "license": "BSD-2-Clause" - }, - "node_modules/maplibre-gl/node_modules/@mapbox/unitbezier": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", - "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", - "license": "BSD-2-Clause" - }, - "node_modules/maplibre-gl/node_modules/earcut": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", - "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", - "license": "ISC" - }, - "node_modules/maplibre-gl/node_modules/geojson-vt": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz", - "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==", - "license": "ISC" - }, - "node_modules/maplibre-gl/node_modules/global-prefix": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-4.0.0.tgz", - "integrity": "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==", - "license": "MIT", - "dependencies": { - "ini": "^4.1.3", - "kind-of": "^6.0.3", - "which": "^4.0.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/maplibre-gl/node_modules/ini": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", - "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/maplibre-gl/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "license": "ISC", - "engines": { - "node": ">=16" - } - }, - "node_modules/maplibre-gl/node_modules/potpack": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz", - "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==", - "license": "ISC" - }, - "node_modules/maplibre-gl/node_modules/quickselect": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", - "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", - "license": "ISC" - }, - "node_modules/maplibre-gl/node_modules/supercluster": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", - "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==", - "license": "ISC", - "dependencies": { - "kdbush": "^4.0.2" - } - }, - "node_modules/maplibre-gl/node_modules/tinyqueue": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", - "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", - "license": "ISC" - }, - "node_modules/maplibre-gl/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", - "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^16.13.0 || >=18.0.0" - } - }, "node_modules/markdown-it": { "version": "12.3.2", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", - "peer": true, "dependencies": { "argparse": "^2.0.1", "entities": "~2.1.0", @@ -19334,15 +18150,6 @@ "node": ">= 0.4" } }, - "node_modules/math-log2": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/math-log2/-/math-log2-1.0.1.tgz", - "integrity": "sha512-9W0yGtkaMAkf74XGYVy4Dqw3YUMnTNB2eeiw9aQbUl4A3KmuCEHTt2DgAB07ENzOYAjsYSAYufkAq0Zd+jU7zA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/md5": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", @@ -19486,8 +18293,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "peer": true + "dev": true }, "node_modules/memory-fs": { "version": "0.4.1", @@ -19666,13 +18472,9 @@ } }, "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, "node_modules/minimist-options": { "version": "4.1.0", @@ -19797,38 +18599,6 @@ "node": ">=10" } }, - "node_modules/mouse-change": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/mouse-change/-/mouse-change-1.4.0.tgz", - "integrity": "sha512-vpN0s+zLL2ykyyUDh+fayu9Xkor5v/zRD9jhSqjRS1cJTGS0+oakVZzNm5n19JvvEj0you+MXlYTpNxUDQUjkQ==", - "license": "MIT", - "dependencies": { - "mouse-event": "^1.0.0" - } - }, - "node_modules/mouse-event": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/mouse-event/-/mouse-event-1.0.5.tgz", - "integrity": "sha512-ItUxtL2IkeSKSp9cyaX2JLUuKk2uMoxBg4bbOWVd29+CskYJR9BGsUqtXenNzKbnDshvupjUewDIYVrOB6NmGw==", - "license": "MIT" - }, - "node_modules/mouse-event-offset": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mouse-event-offset/-/mouse-event-offset-3.0.2.tgz", - "integrity": "sha512-s9sqOs5B1Ykox3Xo8b3Ss2IQju4UwlW6LSR+Q5FXWpprJ5fzMLefIIItr3PH8RwzfGy6gxs/4GAmiNuZScE25w==", - "license": "MIT" - }, - "node_modules/mouse-wheel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mouse-wheel/-/mouse-wheel-1.2.0.tgz", - "integrity": "sha512-+OfYBiUOCTWcTECES49neZwL5AoGkXE+lFjIvzwNCnYRlso+EnfvovcBxGoyQ0yQt806eSPjS675K0EwWknXmw==", - "license": "MIT", - "dependencies": { - "right-now": "^1.0.0", - "signum": "^1.0.0", - "to-px": "^1.0.1" - } - }, "node_modules/move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -19895,12 +18665,6 @@ "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", "integrity": "sha512-cnAsSVxIDsYt0v7HmC0hWZFwwXSh+E6PgCrREDuN/EsjgLwA5XRmlMHhSiDPrt6HxY1gTivEa/Zh7GtODoLevQ==" }, - "node_modules/murmurhash-js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", - "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==", - "license": "MIT" - }, "node_modules/nan": { "version": "2.16.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.16.0.tgz", @@ -19946,12 +18710,6 @@ "node": ">=0.10.0" } }, - "node_modules/native-promise-only": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz", - "integrity": "sha512-zkVhZUA3y8mbz652WrL5x0fB0ehrBkulWT3TomAQ9iDtyXZvzKeEA6GPxAItBYeNYl5yngKRX612qHOhvMkDeg==", - "license": "MIT" - }, "node_modules/native-url": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/native-url/-/native-url-0.2.6.tgz", @@ -19965,32 +18723,6 @@ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" }, - "node_modules/needle": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.9.1.tgz", - "integrity": "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==", - "license": "MIT", - "dependencies": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - }, - "bin": { - "needle": "bin/needle" - }, - "engines": { - "node": ">= 4.4.x" - } - }, - "node_modules/needle/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -20117,7 +18849,6 @@ "version": "4.33.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz", "integrity": "sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg==", - "peer": true, "dependencies": { "@typescript-eslint/experimental-utils": "4.33.0", "@typescript-eslint/scope-manager": "4.33.0", @@ -20189,7 +18920,6 @@ "version": "4.33.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.33.0.tgz", "integrity": "sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "4.33.0", "@typescript-eslint/types": "4.33.0", @@ -20584,7 +19314,6 @@ "version": "7.32.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", - "peer": true, "dependencies": { "@babel/code-frame": "7.12.11", "@eslint/eslintrc": "^0.4.3", @@ -20673,7 +19402,6 @@ "version": "5.10.0", "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-5.10.0.tgz", "integrity": "sha512-vcz32f+7TP+kvTUyMXZmCnNujBQZDNmcqPImw8b9PZ+16w1Qdm6ryRuYZYVaG9xRqqmAPr2Cs9FAX5gN+x/bjw==", - "peer": true, "dependencies": { "lodash": "^4.17.15", "string-natural-compare": "^3.0.1" @@ -20689,7 +19417,6 @@ "version": "24.7.0", "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-24.7.0.tgz", "integrity": "sha512-wUxdF2bAZiYSKBclsUMrYHH6WxiBreNjyDxbRv345TIvPeoCEgPNEn3Sa+ZrSqsf1Dl9SqqSREXMHExlMMu1DA==", - "peer": true, "dependencies": { "@typescript-eslint/experimental-utils": "^4.0.1" }, @@ -20710,7 +19437,6 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", - "peer": true, "engines": { "node": ">=10" }, @@ -20722,7 +19448,6 @@ "version": "3.10.2", "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-3.10.2.tgz", "integrity": "sha512-WAmOCt7EbF1XM8XfbCKAEzAPnShkNSwcIsAD2jHdsMUT9mZJPjLCG7pMzbcC8kK366NOuGip8HKLDC+Xk4yIdA==", - "peer": true, "dependencies": { "@typescript-eslint/experimental-utils": "^3.10.1" }, @@ -21222,7 +19947,6 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -21516,6 +20240,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/neo-react-semantic-ui-range/node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "optional": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/neo-react-semantic-ui-range/node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -21528,7 +20266,6 @@ "version": "4.44.2", "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.44.2.tgz", "integrity": "sha512-6KJVGlCxYdISyurpQ0IPTklv+DULv05rs2hseIXer6D7KrUicRDLFb4IUM1S6LUAKypPM/nSiVSuv8jHu1m3/Q==", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.9.0", "@webassemblyjs/helper-module-context": "1.9.0", @@ -22132,9 +20869,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==" + "version": "2.0.26", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz", + "integrity": "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==" }, "node_modules/normalize-package-data": { "version": "3.0.3", @@ -22166,12 +20903,6 @@ "node": ">=0.10.0" } }, - "node_modules/normalize-svg-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/normalize-svg-path/-/normalize-svg-path-0.1.0.tgz", - "integrity": "sha512-1/kmYej2iedi5+ROxkRESL/pI02pkg0OBnaR4hJkSIX6+ORzepwbuUXfrdZaPjysTsJInj0Rj5NuX027+dMBvA==", - "license": "MIT" - }, "node_modules/normalize-url": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", @@ -22221,18 +20952,6 @@ "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", "integrity": "sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg==" }, - "node_modules/number-is-integer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-integer/-/number-is-integer-1.0.1.tgz", - "integrity": "sha512-Dq3iuiFBkrbmuQjGFFF3zckXNCQoSD37/SdSbgcBailUx6knDvDwb5CympBgcoWHy36sfS12u74MHYkXyHq6bg==", - "license": "MIT", - "dependencies": { - "is-finite": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/nwsapi": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.4.tgz", @@ -22523,6 +21242,22 @@ "node": ">=4" } }, + "node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/os-browserify": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", @@ -22664,12 +21399,6 @@ "node": ">=6" } }, - "node_modules/parenthesis": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/parenthesis/-/parenthesis-3.1.8.tgz", - "integrity": "sha512-KF/U8tk54BgQewkJPvB4s/US3VQY68BRDpH638+7O/n58TpnwiwnOtGIOsT2/i+M78s61BBpeC83STB88d8sqw==", - "license": "MIT" - }, "node_modules/parse-asn1": { "version": "5.1.7", "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz", @@ -22750,27 +21479,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parse-rect": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/parse-rect/-/parse-rect-1.2.0.tgz", - "integrity": "sha512-4QZ6KYbnE6RTwg9E0HpLchUM9EZt6DnDxajFZZDSV4p/12ZJEvPO702DZpGvRYEPo00yKDys7jASi+/w7aO8LA==", - "license": "MIT", - "dependencies": { - "pick-by-alias": "^1.2.0" - } - }, - "node_modules/parse-svg-path": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz", - "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==", - "license": "MIT" - }, - "node_modules/parse-unit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parse-unit/-/parse-unit-1.0.1.tgz", - "integrity": "sha512-hrqldJHokR3Qj88EIlV/kAyAi/G5R2+R56TBANxNMy0uPlYcttx0jnMW6Yx5KsKPSbC3KddM/7qQm3+0wEXKxg==", - "license": "MIT" - }, "node_modules/parse5": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", @@ -22863,19 +21571,6 @@ "node": ">=8" } }, - "node_modules/pbf": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz", - "integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==", - "license": "BSD-3-Clause", - "dependencies": { - "ieee754": "^1.1.12", - "resolve-protobuf-schema": "^2.1.0" - }, - "bin": { - "pbf": "bin/pbf" - } - }, "node_modules/pbkdf2": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", @@ -22905,12 +21600,6 @@ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" }, - "node_modules/pick-by-alias": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pick-by-alias/-/pick-by-alias-1.2.0.tgz", - "integrity": "sha512-ESj2+eBxhGrcA1azgHs7lARG5+5iLakc/6nlfbpjcLl00HuuUOIuORhYXN4D1HfvMSKuVtFQjAlnwi1JHEeDIw==", - "license": "MIT" - }, "node_modules/picocolors": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", @@ -23096,107 +21785,6 @@ "node": ">=4" } }, - "node_modules/plotly.js": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-3.3.0.tgz", - "integrity": "sha512-3PT9dW7IbIfN7JWGr4YxxFQnbN5MRaB36qIKF/eF0iC9l0/MuGSlMlgRgI7Uu8vYuGxX6AjLwsBBRYTPG7NFSA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@plotly/d3": "3.8.2", - "@plotly/d3-sankey": "0.7.2", - "@plotly/d3-sankey-circular": "0.33.1", - "@plotly/mapbox-gl": "1.13.4", - "@plotly/regl": "^2.1.2", - "@turf/area": "^7.1.0", - "@turf/bbox": "^7.1.0", - "@turf/centroid": "^7.1.0", - "base64-arraybuffer": "^1.0.2", - "canvas-fit": "^1.5.0", - "color-alpha": "1.0.4", - "color-normalize": "1.5.0", - "color-parse": "2.0.0", - "color-rgba": "3.0.0", - "country-regex": "^1.1.0", - "d3-force": "^1.2.1", - "d3-format": "^1.4.5", - "d3-geo": "^1.12.1", - "d3-geo-projection": "^2.9.0", - "d3-hierarchy": "^1.1.9", - "d3-interpolate": "^3.0.1", - "d3-time": "^1.1.0", - "d3-time-format": "^2.2.3", - "fast-isnumeric": "^1.1.4", - "gl-mat4": "^1.2.0", - "gl-text": "^1.4.0", - "has-hover": "^1.0.1", - "has-passive-events": "^1.0.0", - "is-mobile": "^4.0.0", - "maplibre-gl": "^4.7.1", - "mouse-change": "^1.4.0", - "mouse-event-offset": "^3.0.2", - "mouse-wheel": "^1.2.0", - "native-promise-only": "^0.8.1", - "parse-svg-path": "^0.1.2", - "point-in-polygon": "^1.1.0", - "polybooljs": "^1.2.2", - "probe-image-size": "^7.2.3", - "regl-error2d": "^2.0.12", - "regl-line2d": "^3.1.3", - "regl-scatter2d": "^3.3.1", - "regl-splom": "^1.0.14", - "strongly-connected-components": "^1.0.1", - "superscript-text": "^1.0.0", - "svg-path-sdf": "^1.1.3", - "tinycolor2": "^1.4.2", - "to-px": "1.0.1", - "topojson-client": "^3.1.0", - "webgl-context": "^2.2.0", - "world-calendars": "^1.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/plotly.js/node_modules/d3-dispatch": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz", - "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==", - "license": "BSD-3-Clause" - }, - "node_modules/plotly.js/node_modules/d3-force": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz", - "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-collection": "1", - "d3-dispatch": "1", - "d3-quadtree": "1", - "d3-timer": "1" - } - }, - "node_modules/plotly.js/node_modules/d3-geo": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.12.1.tgz", - "integrity": "sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "1" - } - }, - "node_modules/plotly.js/node_modules/d3-hierarchy": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", - "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==", - "license": "BSD-3-Clause" - }, - "node_modules/plotly.js/node_modules/d3-quadtree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.7.tgz", - "integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==", - "license": "BSD-3-Clause" - }, "node_modules/pnp-webpack-plugin": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz", @@ -23208,18 +21796,6 @@ "node": ">=6" } }, - "node_modules/point-in-polygon": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/point-in-polygon/-/point-in-polygon-1.1.0.tgz", - "integrity": "sha512-3ojrFwjnnw8Q9242TzgXuTD+eKiutbzyslcq1ydfu82Db2y+Ogbmyrkpv0Hgj31qwT3lbS9+QAAO/pIQM35XRw==", - "license": "MIT" - }, - "node_modules/polybooljs": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/polybooljs/-/polybooljs-1.2.2.tgz", - "integrity": "sha512-ziHW/02J0XuNuUtmidBc6GXE8YohYydp3DWPWXYsd7O721TjcmN+k6ezjdwkDqep+gnWnFY+yqZHvzElra2oCg==", - "license": "MIT" - }, "node_modules/portfinder": { "version": "1.0.32", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", @@ -24474,11 +23050,13 @@ "node": ">=0.10.0" } }, - "node_modules/potpack": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", - "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", - "license": "ISC" + "node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "engines": { + "node": ">= 0.8.0" + } }, "node_modules/prepend-http": { "version": "1.0.4", @@ -24557,17 +23135,6 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, - "node_modules/probe-image-size": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/probe-image-size/-/probe-image-size-7.2.3.tgz", - "integrity": "sha512-HubhG4Rb2UH8YtV4ba0Vp5bQ7L78RTONYu/ujmCu5nBI8wGv24s4E9xSKBi0N1MowRpxk76pFCpJtW0KPzOK0w==", - "license": "MIT", - "dependencies": { - "lodash.merge": "^4.6.2", - "needle": "^2.5.2", - "stream-parser": "~0.3.1" - } - }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -24618,19 +23185,12 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "peer": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, - "node_modules/protocol-buffers-schema": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", - "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==", - "license": "MIT" - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -24799,12 +23359,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/quickselect": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", - "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==", - "license": "ISC" - }, "node_modules/raf": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", @@ -24864,7 +23418,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -25159,7 +23712,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -25186,6 +23738,35 @@ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" }, + "node_modules/react-intl": { + "version": "7.1.14", + "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-7.1.14.tgz", + "integrity": "sha512-VE/0Wi/lHJlBC7APQpCzLUdIt3GB5B0GZrRW8Q+ACbkHI4j+Wwgg9J1TniN6zmLHmPH5gxXcMy+fkSPfw5p1WQ==", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "@formatjs/icu-messageformat-parser": "2.11.4", + "@formatjs/intl": "3.1.8", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/react": "16 || 17 || 18 || 19", + "hoist-non-react-statics": "^3.3.2", + "intl-messageformat": "10.7.18", + "tslib": "^2.8.0" + }, + "peerDependencies": { + "react": "16 || 17 || 18 || 19", + "typescript": "^5.6.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/react-intl/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -25196,19 +23777,6 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, - "node_modules/react-plotly.js": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/react-plotly.js/-/react-plotly.js-2.6.0.tgz", - "integrity": "sha512-g93xcyhAVCSt9kV1svqG1clAEdL6k3U+jjuSzfTV7owaSU9Go6Ph8bl25J+jKfKvIGAEYpe4qj++WHJuc9IaeA==", - "license": "MIT", - "dependencies": { - "prop-types": "^15.8.1" - }, - "peerDependencies": { - "plotly.js": ">1.34.0", - "react": ">0.13.0" - } - }, "node_modules/react-popper": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", @@ -25227,7 +23795,6 @@ "version": "0.8.3", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz", "integrity": "sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -25600,132 +24167,34 @@ "node": ">=4" } }, - "node_modules/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", - "license": "MIT" - }, - "node_modules/regjsparser": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", - "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", - "license": "BSD-2-Clause", - "dependencies": { - "jsesc": "~3.0.2" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/regl": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/regl/-/regl-2.1.1.tgz", - "integrity": "sha512-+IOGrxl3FZ8ZM9ixCWQZzFRiRn7Rzn9bu3iFHwg/yz4tlOUQgbO4PHLgG+1ZT60zcIV8tief6Qrmyl8qcoJP0g==", - "license": "MIT" - }, - "node_modules/regl-error2d": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/regl-error2d/-/regl-error2d-2.0.12.tgz", - "integrity": "sha512-r7BUprZoPO9AbyqM5qlJesrSRkl+hZnVKWKsVp7YhOl/3RIpi4UDGASGJY0puQ96u5fBYw/OlqV24IGcgJ0McA==", - "license": "MIT", - "dependencies": { - "array-bounds": "^1.0.1", - "color-normalize": "^1.5.0", - "flatten-vertex-data": "^1.0.2", - "object-assign": "^4.1.1", - "pick-by-alias": "^1.2.0", - "to-float32": "^1.1.0", - "update-diff": "^1.1.0" - } - }, - "node_modules/regl-line2d": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/regl-line2d/-/regl-line2d-3.1.3.tgz", - "integrity": "sha512-fkgzW+tTn4QUQLpFKsUIE0sgWdCmXAM3ctXcCgoGBZTSX5FE2A0M7aynz7nrZT5baaftLrk9te54B+MEq4QcSA==", - "license": "MIT", - "dependencies": { - "array-bounds": "^1.0.1", - "array-find-index": "^1.0.2", - "array-normalize": "^1.1.4", - "color-normalize": "^1.5.0", - "earcut": "^2.1.5", - "es6-weak-map": "^2.0.3", - "flatten-vertex-data": "^1.0.2", - "object-assign": "^4.1.1", - "parse-rect": "^1.2.0", - "pick-by-alias": "^1.2.0", - "to-float32": "^1.1.0" - } - }, - "node_modules/regl-scatter2d": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/regl-scatter2d/-/regl-scatter2d-3.3.1.tgz", - "integrity": "sha512-seOmMIVwaCwemSYz/y4WE0dbSO9svNFSqtTh5RE57I7PjGo3tcUYKtH0MTSoshcAsreoqN8HoCtnn8wfHXXfKQ==", - "license": "MIT", - "dependencies": { - "@plotly/point-cluster": "^3.1.9", - "array-range": "^1.0.1", - "array-rearrange": "^2.2.2", - "clamp": "^1.0.1", - "color-id": "^1.1.0", - "color-normalize": "^1.5.0", - "color-rgba": "^2.1.1", - "flatten-vertex-data": "^1.0.2", - "glslify": "^7.0.0", - "is-iexplorer": "^1.0.0", - "object-assign": "^4.1.1", - "parse-rect": "^1.2.0", - "pick-by-alias": "^1.2.0", - "to-float32": "^1.1.0", - "update-diff": "^1.1.0" - } - }, - "node_modules/regl-scatter2d/node_modules/color-parse": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/color-parse/-/color-parse-1.4.3.tgz", - "integrity": "sha512-BADfVl/FHkQkyo8sRBwMYBqemqsgnu7JZAwUgvBvuwwuNUZAhSvLTbsEErS5bQXzOjDR0dWzJ4vXN2Q+QoPx0A==", - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0" - } + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "license": "MIT" }, - "node_modules/regl-scatter2d/node_modules/color-rgba": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/color-rgba/-/color-rgba-2.4.0.tgz", - "integrity": "sha512-Nti4qbzr/z2LbUWySr7H9dk3Rl7gZt7ihHAxlgT4Ho90EXWkjtkL1avTleu9yeGuqrt/chxTB6GKK8nZZ6V0+Q==", - "license": "MIT", + "node_modules/regjsparser": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", + "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "license": "BSD-2-Clause", "dependencies": { - "color-parse": "^1.4.2", - "color-space": "^2.0.0" + "jsesc": "~3.0.2" + }, + "bin": { + "regjsparser": "bin/parser" } }, - "node_modules/regl-splom": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/regl-splom/-/regl-splom-1.0.14.tgz", - "integrity": "sha512-OiLqjmPRYbd7kDlHC6/zDf6L8lxgDC65BhC8JirhP4ykrK4x22ZyS+BnY8EUinXKDeMgmpRwCvUmk7BK4Nweuw==", + "node_modules/regjsparser/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", "license": "MIT", - "dependencies": { - "array-bounds": "^1.0.1", - "array-range": "^1.0.1", - "color-alpha": "^1.0.4", - "flatten-vertex-data": "^1.0.2", - "parse-rect": "^1.2.0", - "pick-by-alias": "^1.2.0", - "raf": "^3.4.1", - "regl-scatter2d": "^3.2.3" + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" } }, "node_modules/relateurl": { @@ -25870,15 +24339,6 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/resolve-protobuf-schema": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", - "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", - "license": "MIT", - "dependencies": { - "protocol-buffers-schema": "^3.3.1" - } - }, "node_modules/resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", @@ -26035,12 +24495,6 @@ "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", "integrity": "sha512-zgn5OjNQXLUTdq8m17KdaicF6w89TZs8ZU8y0AYENIU6wG8GG6LLm0yLSiPY8DmaYmHdgRW8rnApjoT0fQRfMg==" }, - "node_modules/right-now": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/right-now/-/right-now-1.0.0.tgz", - "integrity": "sha512-DA8+YS+sMIVpbsuKgy+Z67L9Lxb1p05mNxRpDPNksPDEFir4vmBlUtuN9jkTGn9YMMdlBuK7XQgFiz6ws+yhSg==", - "license": "MIT" - }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -26073,7 +24527,6 @@ "version": "1.32.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.32.1.tgz", "integrity": "sha512-/2HA0Ec70TvQnXdzynFffkjA6XN+1e2pEv/uKS5Ulca40g2L7KuOE3riasHoNVHOsFD5KKZgDsMk1CP3Tw9s+A==", - "peer": true, "dependencies": { "@types/estree": "*", "@types/node": "*", @@ -26871,12 +25324,6 @@ "sha.js": "bin.js" } }, - "node_modules/shallow-copy": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/shallow-copy/-/shallow-copy-0.0.1.tgz", - "integrity": "sha512-b6i4ZpVuUxB9h5gfCxPiusKYkqTMOjEbBs4wMaFbkfia4yFv92UKZ6Df8WXcKbn08JNL/abvg3FnMAOfakDvUw==", - "license": "MIT" - }, "node_modules/shallowequal": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", @@ -26985,12 +25432,6 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, - "node_modules/signum": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/signum/-/signum-1.0.0.tgz", - "integrity": "sha512-yodFGwcyt59XRh7w5W3jPcIQb3Bwi21suEfT7MAWnBX3iCdklJpgDgvGT9o04UonglZN5SNMfJFkHIR/jO8GHw==", - "license": "MIT" - }, "node_modules/simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -27231,7 +25672,6 @@ "version": "1.6.1", "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.6.1.tgz", "integrity": "sha512-2g0tjOR+fRs0amxENLi/q5TiJTqY+WXFOzb5UwXndlK6TO3U/mirZznpx6w34HVMoc3g7cY24yC/ZMIYnDlfkw==", - "peer": true, "dependencies": { "debug": "^3.2.7", "eventsource": "^2.0.2", @@ -27442,14 +25882,6 @@ "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", "dev": true }, - "node_modules/stack-trace": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz", - "integrity": "sha512-vjUc6sfgtgY0dxCdnc40mK6Oftjo9+2K8H/NG81TMhgL392FtiPA9tn9RLyTxXmTLPJPjF3VyzFp6bsWFLisMQ==", - "engines": { - "node": "*" - } - }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -27474,15 +25906,6 @@ "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" }, - "node_modules/static-eval": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.1.1.tgz", - "integrity": "sha512-MgWpQ/ZjGieSVB3eOJVs4OA2LT/q1vx98KPCTTQPzq/aLr0YUXTsgryTXr4SLfR0ZfUUCiedM9n/ABeDIyy4mA==", - "license": "MIT", - "dependencies": { - "escodegen": "^2.1.0" - } - }, "node_modules/static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -27544,30 +25967,6 @@ "xtend": "^4.0.0" } }, - "node_modules/stream-parser": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/stream-parser/-/stream-parser-0.3.1.tgz", - "integrity": "sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ==", - "license": "MIT", - "dependencies": { - "debug": "2" - } - }, - "node_modules/stream-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/stream-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, "node_modules/stream-shift": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", @@ -27606,15 +26005,6 @@ "resolved": "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz", "integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==" }, - "node_modules/string-split-by": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string-split-by/-/string-split-by-1.0.0.tgz", - "integrity": "sha512-KaJKY+hfpzNyet/emP81PJA9hTVSfxNLS9SFTWxdCnnW1/zOOwiV248+EfoX7IQFcBaOp4G5YE6xTJMF+pLg6A==", - "license": "MIT", - "dependencies": { - "parenthesis": "^3.1.5" - } - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -27814,12 +26204,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strongly-connected-components": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strongly-connected-components/-/strongly-connected-components-1.0.1.tgz", - "integrity": "sha512-i0TFx4wPcO0FwX+4RkLJi1MxmcTv90jNZgxMu9XRnMXMeFUY1VJlIoXpZunPUvUUqbCT1pg5PEkFqqpcaElNaA==", - "license": "MIT" - }, "node_modules/style-loader": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", @@ -27862,27 +26246,6 @@ "node": ">=8" } }, - "node_modules/supercluster": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.5.tgz", - "integrity": "sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==", - "license": "ISC", - "dependencies": { - "kdbush": "^3.0.0" - } - }, - "node_modules/supercluster/node_modules/kdbush": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz", - "integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==", - "license": "ISC" - }, - "node_modules/superscript-text": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/superscript-text/-/superscript-text-1.0.0.tgz", - "integrity": "sha512-gwu8l5MtRZ6koO0icVTlmN5pm7Dhh1+Xpe9O4x6ObMAsW+3jPbW14d1DsBq1F4wiI+WOFjXF35pslgec/G8yCQ==", - "license": "MIT" - }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -27936,51 +26299,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/svg-arc-to-cubic-bezier": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz", - "integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==", - "license": "ISC" - }, "node_modules/svg-parser": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==" }, - "node_modules/svg-path-bounds": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/svg-path-bounds/-/svg-path-bounds-1.0.2.tgz", - "integrity": "sha512-H4/uAgLWrppIC0kHsb2/dWUYSmb4GE5UqH06uqWBcg6LBjX2fu0A8+JrO2/FJPZiSsNOKZAhyFFgsLTdYUvSqQ==", - "license": "MIT", - "dependencies": { - "abs-svg-path": "^0.1.1", - "is-svg-path": "^1.0.1", - "normalize-svg-path": "^1.0.0", - "parse-svg-path": "^0.1.2" - } - }, - "node_modules/svg-path-bounds/node_modules/normalize-svg-path": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz", - "integrity": "sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==", - "license": "MIT", - "dependencies": { - "svg-arc-to-cubic-bezier": "^3.0.0" - } - }, - "node_modules/svg-path-sdf": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/svg-path-sdf/-/svg-path-sdf-1.1.3.tgz", - "integrity": "sha512-vJJjVq/R5lSr2KLfVXVAStktfcfa1pNFjFOgyJnzZFXlO/fDZ5DmM8FpnSKKzLPfEYTVeXuVBTHF296TpxuJVg==", - "license": "MIT", - "dependencies": { - "bitmap-sdf": "^1.0.0", - "draw-svg-path": "^1.0.0", - "is-svg-path": "^1.0.1", - "parse-svg-path": "^0.1.2", - "svg-path-bounds": "^1.0.1" - } - }, "node_modules/svg.draggable.js": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz", @@ -28321,6 +26644,7 @@ "version": "5.3.14", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", @@ -28351,9 +26675,10 @@ } }, "node_modules/terser-webpack-plugin/node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -28381,6 +26706,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -28391,12 +26717,14 @@ "node_modules/terser-webpack-plugin/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "peer": true }, "node_modules/terser-webpack-plugin/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "peer": true, "engines": { "node": ">=8" } @@ -28405,6 +26733,7 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "peer": true, "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -28417,12 +26746,14 @@ "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "peer": true }, "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", - "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -28441,6 +26772,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "peer": true, "dependencies": { "randombytes": "^2.1.0" } @@ -28449,6 +26781,7 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -28460,12 +26793,13 @@ } }, "node_modules/terser-webpack-plugin/node_modules/terser": { - "version": "5.39.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", - "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", + "version": "5.44.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", + "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", + "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -28547,12 +26881,6 @@ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" }, - "node_modules/tinycolor2": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", - "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", - "license": "MIT" - }, "node_modules/tinyglobby": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz", @@ -28588,7 +26916,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, - "peer": true, "engines": { "node": ">=12" }, @@ -28605,12 +26932,6 @@ "node": "^18.0.0 || >=20.0.0" } }, - "node_modules/tinyqueue": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", - "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==", - "license": "ISC" - }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -28621,12 +26942,6 @@ "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", "integrity": "sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==" }, - "node_modules/to-float32": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/to-float32/-/to-float32-1.1.0.tgz", - "integrity": "sha512-keDnAusn/vc+R3iEiSDw8TOF7gPiTLdK1ArvWtYbJQiVfmRg6i/CAvbKq3uIS0vWroAC7ZecN3DjQKw3aSklUg==", - "license": "MIT" - }, "node_modules/to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", @@ -28649,15 +26964,6 @@ "node": ">=0.10.0" } }, - "node_modules/to-px": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/to-px/-/to-px-1.0.1.tgz", - "integrity": "sha512-2y3LjBeIZYL19e5gczp14/uRWFDtDUErJPVN3VU9a7SJO+RjGRtYR47aMN2bZgGlxvW4ZcEz2ddUPVHXcMfuXw==", - "license": "MIT", - "dependencies": { - "parse-unit": "^1.0.1" - } - }, "node_modules/to-regex": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", @@ -28691,26 +26997,6 @@ "node": ">=0.6" } }, - "node_modules/topojson-client": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz", - "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==", - "license": "ISC", - "dependencies": { - "commander": "2" - }, - "bin": { - "topo2geo": "bin/topo2geo", - "topomerge": "bin/topomerge", - "topoquantize": "bin/topoquantize" - } - }, - "node_modules/topojson-client/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT" - }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -29023,6 +27309,17 @@ "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" }, + "node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -29118,16 +27415,6 @@ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, - "node_modules/typedarray-pool": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/typedarray-pool/-/typedarray-pool-1.2.0.tgz", - "integrity": "sha512-YTSQbzX43yvtpfRtIDAYygoYtgT+Rpjuxy9iOpczrjpXLgGoyG7aS5USJXV2d3nn8uHTeb9rXDvzS27zUg5KYQ==", - "license": "MIT", - "dependencies": { - "bit-twiddle": "^1.0.0", - "dup": "^1.0.0" - } - }, "node_modules/typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -29145,7 +27432,6 @@ "version": "5.8.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -29395,9 +27681,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", "funding": [ { "type": "opencollective", @@ -29428,12 +27714,6 @@ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, - "node_modules/update-diff": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/update-diff/-/update-diff-1.1.0.tgz", - "integrity": "sha512-rCiBPiHxZwT4+sBhEbChzpO5hYHjm91kScWgdHf4Qeafs6Ba7MBl+d9GlGv72bcTZQO0sLmtQS1pHSWoCLtN/A==", - "license": "MIT" - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -29739,17 +28019,6 @@ "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" }, - "node_modules/vt-pbf": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", - "integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==", - "license": "MIT", - "dependencies": { - "@mapbox/point-geometry": "0.1.0", - "@mapbox/vector-tile": "^1.3.1", - "pbf": "^3.2.1" - } - }, "node_modules/w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", @@ -30057,21 +28326,6 @@ "minimalistic-assert": "^1.0.0" } }, - "node_modules/weak-map": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/weak-map/-/weak-map-1.0.8.tgz", - "integrity": "sha512-lNR9aAefbGPpHO7AEnY0hCFjz1eTkWCXYvkTRrTHs9qv8zJp+SkVYpzfLIFXQQiG3tVvbNFQgVg2bQS8YGgxyw==", - "license": "Apache-2.0" - }, - "node_modules/webgl-context": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/webgl-context/-/webgl-context-2.2.0.tgz", - "integrity": "sha512-q/fGIivtqTT7PEoF07axFIlHNk/XCPaYpq64btnepopSWvKNFkoORlQYgqDigBIuGA1ExnFd/GnSUnBNEPQY7Q==", - "license": "MIT", - "dependencies": { - "get-canvas-context": "^1.0.1" - } - }, "node_modules/webidl-conversions": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", @@ -30081,22 +28335,22 @@ } }, "node_modules/webpack": { - "version": "5.99.8", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.8.tgz", - "integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==", - "license": "MIT", + "version": "5.102.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz", + "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.6", + "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.14.0", - "browserslist": "^4.24.0", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.26.3", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", + "enhanced-resolve": "^5.17.3", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -30106,11 +28360,11 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^4.3.2", - "tapable": "^2.1.1", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.11", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" + "watchpack": "^2.4.4", + "webpack-sources": "^3.3.3" }, "bin": { "webpack": "bin/webpack.js" @@ -30278,7 +28532,6 @@ "version": "3.11.1", "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.1.tgz", "integrity": "sha512-u4R3mRzZkbxQVa+MBWi2uVpB5W59H3ekZAJsQlKUTdl7Elcah2EhygTPLmeFXybQkf9i2+L0kn7ik9SnXa6ihQ==", - "peer": true, "dependencies": { "ansi-html": "0.0.7", "bonjour": "^3.5.0", @@ -30973,6 +29226,7 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "peer": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" @@ -30981,22 +29235,26 @@ "node_modules/webpack/node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==" + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "peer": true }, "node_modules/webpack/node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==" + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "peer": true }, "node_modules/webpack/node_modules/@webassemblyjs/helper-wasm-bytecode": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==" + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "peer": true }, "node_modules/webpack/node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -31008,6 +29266,7 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "peer": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } @@ -31016,6 +29275,7 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "peer": true, "dependencies": { "@xtuc/long": "4.2.2" } @@ -31023,12 +29283,14 @@ "node_modules/webpack/node_modules/@webassemblyjs/utf8": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==" + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "peer": true }, "node_modules/webpack/node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -31044,6 +29306,7 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", @@ -31056,6 +29319,7 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -31067,6 +29331,7 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", @@ -31080,15 +29345,17 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, "node_modules/webpack/node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -31096,11 +29363,22 @@ "node": ">=0.4.0" } }, + "node_modules/webpack/node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "peer": true, + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, "node_modules/webpack/node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "license": "MIT", "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", @@ -31117,7 +29395,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -31126,9 +29404,10 @@ } }, "node_modules/webpack/node_modules/enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "peer": true, "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -31141,21 +29420,26 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" + "peer": true }, "node_modules/webpack/node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "peer": true, "engines": { "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/webpack/node_modules/schema-utils": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", - "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", - "license": "MIT", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -31171,17 +29455,23 @@ } }, "node_modules/webpack/node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "peer": true, "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/webpack/node_modules/watchpack": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", - "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "peer": true, "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -31191,9 +29481,10 @@ } }, "node_modules/webpack/node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "peer": true, "engines": { "node": ">=10.13.0" } @@ -31572,15 +29863,6 @@ "microevent.ts": "~0.1.1" } }, - "node_modules/world-calendars": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/world-calendars/-/world-calendars-1.0.4.tgz", - "integrity": "sha512-VGRnLJS+xJmGDPodgJRnGIDwGu0s+Cr9V2HB3EzlDZ5n0qb8h5SJtGUEkjrphZYAglEiXZ6kiXdmk0H/h/uu/w==", - "license": "MIT", - "dependencies": { - "object-assign": "^4.1.0" - } - }, "node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -31824,7 +30106,6 @@ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/frontend/static/frontend/package.json b/src/frontend/static/frontend/package.json index fd73478c..63d13940 100644 --- a/src/frontend/static/frontend/package.json +++ b/src/frontend/static/frontend/package.json @@ -34,6 +34,7 @@ "@types/validator": "^13.12.3", "babel-loader": "^10.0.0", "babel-plugin-react-compiler": "^19.1.0-rc.2", + "baseline-browser-mapping": "^2.10.0", "css-loader": "^7.1.2", "eslint": "^9.27.0", "eslint-plugin-jsdoc": "^50.6.9", @@ -66,13 +67,12 @@ "ky": "^1.7.5", "lodash": "^4.17.21", "neo-react-semantic-ui-range": "^0.3.6", - "plotly.js": "^3.3.0", "react": "^18.3.1", "react-apexcharts": "^1.4.1", "react-avatar": "^5.0.3", "react-colorful": "^5.6.1", "react-dom": "^18.3.1", - "react-plotly.js": "^2.6.0", + "react-intl": "^7.1.14", "react-virtualized": "^9.22.6", "recharts": "^2.15.1", "semantic-ui-react": "^2.1.5", diff --git a/src/frontend/static/frontend/rspack.common.js b/src/frontend/static/frontend/rspack.common.js index e7f41f91..e8132aa9 100644 --- a/src/frontend/static/frontend/rspack.common.js +++ b/src/frontend/static/frontend/rspack.common.js @@ -14,7 +14,6 @@ const PATHS = { export const common = { entry: { - differentialExpression: `${PATHS.src}/differential-expression.tsx`, base: `${PATHS.src}/base.tsx`, gem: `${PATHS.src}/gem.tsx`, main: `${PATHS.src}/index.tsx`, @@ -26,7 +25,8 @@ export const common = { survival: `${PATHS.src}/survival.tsx`, aboutUs: `${PATHS.src}/about-us.tsx`, openSource: `${PATHS.src}/open-source.tsx`, - sitePolicy: `${PATHS.src}/site-policy.tsx` + sitePolicy: `${PATHS.src}/site-policy.tsx`, + faq: `${PATHS.src}/faq.tsx` }, output: { path: PATHS.output, diff --git a/src/frontend/static/frontend/src/components/Base.tsx b/src/frontend/static/frontend/src/components/Base.tsx index b70f80e2..84425088 100644 --- a/src/frontend/static/frontend/src/components/Base.tsx +++ b/src/frontend/static/frontend/src/components/Base.tsx @@ -4,6 +4,26 @@ import ky from 'ky' import { DjangoUser } from '../utils/django_interfaces' import { Nullable } from '../utils/interfaces' import { Footer } from './Footer' +import { IntlProvider } from 'react-intl' + +// Locales +import es from '../locales/es' +import en from '../locales/en' + +// Common dependencies for all the pages +import 'fomantic-ui-css/semantic.css' +import '../css/base.css' + +const messages = { en, es } + +interface LocaleContextType { + locale: 'en' | 'es', + setLocale: React.Dispatch> +} +const LocaleContext = React.createContext({ + locale: 'es', + setLocale: () => {} +}) declare const urlCurrentUser: string @@ -29,6 +49,8 @@ const Base = (props: BaseProps) => { const abortController = useRef(new AbortController()) const [currentUser, setUser] = useState>(null) const [isLoadingCurrentUser, setIsLoadingCurrentUser] = useState(true) + // State that defines the current language ('es' or 'en') for , used to display the interface in the selected locale + const [locale, setLocale] = useState<'en' | 'es'>('es') /** * Method which is executed when the component has mounted @@ -75,17 +97,21 @@ const Base = (props: BaseProps) => { return ( - {/* Navbar */} - + + + {/* Navbar */} + - {/* Composition part */} -
- {props.children} -
+ {/* Composition part */} +
+ {props.children} +
- {/* Footer */} - {/* TODO: add license */} -