From 6926f502c50e1b1d860ef1e4d1eff3b95ea18b57 Mon Sep 17 00:00:00 2001 From: Trey T Date: Sun, 12 Apr 2026 09:54:27 -0500 Subject: [PATCH] =?UTF-8?q?Initial=20commit=20=E2=80=94=20PlantGuideScrape?= =?UTF-8?q?r=20project?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 20 + .gitignore | 39 + README.md | 209 + accum_images.md | 231 + backend/Dockerfile | 24 + backend/add_indexes.py | 19 + backend/alembic.ini | 42 + backend/alembic/env.py | 54 + backend/alembic/script.py.mako | 26 + backend/alembic/versions/001_initial.py | 112 + .../002_add_cached_stats_and_indexes.py | 53 + .../versions/003_add_job_max_images.py | 31 + backend/app/__init__.py | 1 + backend/app/api/__init__.py | 1 + backend/app/api/exports.py | 175 + backend/app/api/images.py | 441 + backend/app/api/jobs.py | 173 + backend/app/api/sources.py | 198 + backend/app/api/species.py | 366 + backend/app/api/stats.py | 190 + backend/app/config.py | 38 + backend/app/database.py | 44 + backend/app/main.py | 95 + backend/app/models/__init__.py | 8 + backend/app/models/api_key.py | 18 + backend/app/models/cached_stats.py | 14 + backend/app/models/export.py | 24 + backend/app/models/image.py | 36 + backend/app/models/job.py | 27 + backend/app/models/species.py | 21 + backend/app/schemas/__init__.py | 15 + backend/app/schemas/api_key.py | 36 + backend/app/schemas/export.py | 45 + backend/app/schemas/image.py | 47 + backend/app/schemas/job.py | 35 + backend/app/schemas/species.py | 44 + backend/app/schemas/stats.py | 43 + backend/app/scrapers/__init__.py | 41 + backend/app/scrapers/base.py | 57 + backend/app/scrapers/bhl.py | 228 + backend/app/scrapers/bing.py | 135 + backend/app/scrapers/duckduckgo.py | 101 + backend/app/scrapers/eol.py | 226 + backend/app/scrapers/flickr.py | 146 + backend/app/scrapers/gbif.py | 159 + backend/app/scrapers/inaturalist.py | 144 + backend/app/scrapers/trefle.py | 154 + backend/app/scrapers/wikimedia.py | 146 + backend/app/utils/__init__.py | 1 + backend/app/utils/dedup.py | 80 + backend/app/utils/image_quality.py | 109 + backend/app/utils/logging.py | 92 + backend/app/workers/__init__.py | 1 + backend/app/workers/celery_app.py | 36 + backend/app/workers/export_tasks.py | 170 + backend/app/workers/quality_tasks.py | 224 + backend/app/workers/scrape_tasks.py | 164 + backend/app/workers/stats_tasks.py | 193 + backend/requirements.txt | 34 + backend/tests/__init__.py | 1 + docker-compose.unraid.yml | 114 + docker-compose.yml | 73 + docs/master_plan.md | 564 + frontend/Dockerfile | 14 + frontend/dist/assets/index-BXIq8BNP.js | 283 + frontend/dist/assets/index-uHzGA3u6.css | 1 + frontend/dist/index.html | 14 + frontend/index.html | 13 + frontend/package.json | 31 + frontend/postcss.config.js | 6 + frontend/src/App.tsx | 81 + frontend/src/api/client.ts | 275 + frontend/src/index.css | 7 + frontend/src/main.tsx | 22 + frontend/src/pages/Dashboard.tsx | 413 + frontend/src/pages/Export.tsx | 346 + frontend/src/pages/Images.tsx | 331 + frontend/src/pages/Jobs.tsx | 354 + frontend/src/pages/Settings.tsx | 543 + frontend/src/pages/Species.tsx | 997 + frontend/src/vite-env.d.ts | 9 + frontend/tailwind.config.js | 11 + frontend/tsconfig.json | 21 + frontend/tsconfig.node.json | 10 + frontend/vite.config.ts | 18 + houseplants_list.json | 18874 ++++++++++++++++ nginx/nginx.conf | 58 + 87 files changed, 29120 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 accum_images.md create mode 100644 backend/Dockerfile create mode 100644 backend/add_indexes.py create mode 100644 backend/alembic.ini create mode 100644 backend/alembic/env.py create mode 100644 backend/alembic/script.py.mako create mode 100644 backend/alembic/versions/001_initial.py create mode 100644 backend/alembic/versions/002_add_cached_stats_and_indexes.py create mode 100644 backend/alembic/versions/003_add_job_max_images.py create mode 100644 backend/app/__init__.py create mode 100644 backend/app/api/__init__.py create mode 100644 backend/app/api/exports.py create mode 100644 backend/app/api/images.py create mode 100644 backend/app/api/jobs.py create mode 100644 backend/app/api/sources.py create mode 100644 backend/app/api/species.py create mode 100644 backend/app/api/stats.py create mode 100644 backend/app/config.py create mode 100644 backend/app/database.py create mode 100644 backend/app/main.py create mode 100644 backend/app/models/__init__.py create mode 100644 backend/app/models/api_key.py create mode 100644 backend/app/models/cached_stats.py create mode 100644 backend/app/models/export.py create mode 100644 backend/app/models/image.py create mode 100644 backend/app/models/job.py create mode 100644 backend/app/models/species.py create mode 100644 backend/app/schemas/__init__.py create mode 100644 backend/app/schemas/api_key.py create mode 100644 backend/app/schemas/export.py create mode 100644 backend/app/schemas/image.py create mode 100644 backend/app/schemas/job.py create mode 100644 backend/app/schemas/species.py create mode 100644 backend/app/schemas/stats.py create mode 100644 backend/app/scrapers/__init__.py create mode 100644 backend/app/scrapers/base.py create mode 100644 backend/app/scrapers/bhl.py create mode 100644 backend/app/scrapers/bing.py create mode 100644 backend/app/scrapers/duckduckgo.py create mode 100644 backend/app/scrapers/eol.py create mode 100644 backend/app/scrapers/flickr.py create mode 100644 backend/app/scrapers/gbif.py create mode 100644 backend/app/scrapers/inaturalist.py create mode 100644 backend/app/scrapers/trefle.py create mode 100644 backend/app/scrapers/wikimedia.py create mode 100644 backend/app/utils/__init__.py create mode 100644 backend/app/utils/dedup.py create mode 100644 backend/app/utils/image_quality.py create mode 100644 backend/app/utils/logging.py create mode 100644 backend/app/workers/__init__.py create mode 100644 backend/app/workers/celery_app.py create mode 100644 backend/app/workers/export_tasks.py create mode 100644 backend/app/workers/quality_tasks.py create mode 100644 backend/app/workers/scrape_tasks.py create mode 100644 backend/app/workers/stats_tasks.py create mode 100644 backend/requirements.txt create mode 100644 backend/tests/__init__.py create mode 100644 docker-compose.unraid.yml create mode 100644 docker-compose.yml create mode 100644 docs/master_plan.md create mode 100644 frontend/Dockerfile create mode 100644 frontend/dist/assets/index-BXIq8BNP.js create mode 100644 frontend/dist/assets/index-uHzGA3u6.css create mode 100644 frontend/dist/index.html create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.js create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/api/client.ts create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/pages/Dashboard.tsx create mode 100644 frontend/src/pages/Export.tsx create mode 100644 frontend/src/pages/Images.tsx create mode 100644 frontend/src/pages/Jobs.tsx create mode 100644 frontend/src/pages/Settings.tsx create mode 100644 frontend/src/pages/Species.tsx create mode 100644 frontend/src/vite-env.d.ts create mode 100644 frontend/tailwind.config.js create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts create mode 100755 houseplants_list.json create mode 100644 nginx/nginx.conf diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a84b178 --- /dev/null +++ b/.env.example @@ -0,0 +1,20 @@ +# Database +DATABASE_URL=sqlite:////data/db/plants.sqlite + +# Redis +REDIS_URL=redis://redis:6379/0 + +# Storage paths +IMAGES_PATH=/data/images +EXPORTS_PATH=/data/exports + +# API Keys (user-provided) +FLICKR_API_KEY= +FLICKR_API_SECRET= +INATURALIST_APP_ID= +INATURALIST_APP_SECRET= +TREFLE_API_KEY= + +# Optional settings +LOG_LEVEL=INFO +CELERY_CONCURRENCY=4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9685575 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +venv/ +.venv/ +ENV/ +env/ +.eggs/ +*.egg-info/ +*.egg + +# Node +node_modules/ +npm-debug.log +yarn-error.log + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Project specific +data/ +*.sqlite +*.db +.env +*.zip + +# Docker +docker-compose.override.yml diff --git a/README.md b/README.md new file mode 100644 index 0000000..33453b4 --- /dev/null +++ b/README.md @@ -0,0 +1,209 @@ +# PlantGuideScraper + +Web-based interface for managing a multi-source houseplant image scraping pipeline. Collects images from iNaturalist, Flickr, Wikimedia Commons, and Trefle.io to build datasets for CoreML training. + +## Features + +- **Species Management**: Import species lists via CSV or JSON, search and filter by genus or image status +- **Multi-Source Scraping**: iNaturalist/GBIF, Flickr, Wikimedia Commons, Trefle.io +- **Image Quality Pipeline**: Automatic deduplication, blur detection, resizing +- **License Filtering**: Only collect commercially-safe CC0/CC-BY licensed images +- **Export for CoreML**: Train/test split, Create ML-compatible folder structure +- **Real-time Dashboard**: Progress tracking, statistics, job monitoring + +## Quick Start + +```bash +# Clone and start +cd PlantGuideScraper +docker-compose up --build + +# Access the UI +open http://localhost +``` + +## Unraid Deployment + +### Setup + +1. Copy the project to your Unraid server: + ```bash + scp -r PlantGuideScraper root@YOUR_UNRAID_IP:/mnt/user/appdata/PlantGuideScraper + ``` + +2. SSH into Unraid and create data directories: + ```bash + ssh root@YOUR_UNRAID_IP + mkdir -p /mnt/user/appdata/PlantGuideScraper/{database,images,exports,redis} + ``` + +3. Install **Docker Compose Manager** from Community Applications + +4. In Unraid: **Docker → Compose → Add New Stack** + - Path: `/mnt/user/appdata/PlantGuideScraper/docker-compose.unraid.yml` + - Click **Compose Up** + +5. Access at `http://YOUR_UNRAID_IP:8580` + +### Configurable Paths + +Edit `docker-compose.unraid.yml` to customize where data is stored. Look for these lines in both `backend` and `celery` services: + +```yaml +# === CONFIGURABLE DATA PATHS === +- /mnt/user/appdata/PlantGuideScraper/database:/data/db # DATABASE_PATH +- /mnt/user/appdata/PlantGuideScraper/images:/data/images # IMAGES_PATH +- /mnt/user/appdata/PlantGuideScraper/exports:/data/exports # EXPORTS_PATH +``` + +| Path | Description | Default | +|------|-------------|---------| +| DATABASE_PATH | SQLite database file | `/mnt/user/appdata/PlantGuideScraper/database` | +| IMAGES_PATH | Downloaded images (can be 100GB+) | `/mnt/user/appdata/PlantGuideScraper/images` | +| EXPORTS_PATH | Generated export zip files | `/mnt/user/appdata/PlantGuideScraper/exports` | + +**Example: Store images on a separate share:** +```yaml +- /mnt/user/data/PlantImages:/data/images # IMAGES_PATH +``` + +**Important:** Keep paths identical in both `backend` and `celery` services. + +## Configuration + +1. Configure API keys in Settings: + - **Flickr**: Get key at https://www.flickr.com/services/api/ + - **Trefle**: Get key at https://trefle.io/ + - iNaturalist and Wikimedia don't require keys + +2. Import species list (see Import Documentation below) + +3. Select species and start scraping + +## Import Documentation + +### CSV Import + +Import species from a CSV file with the following columns: + +| Column | Required | Description | +|--------|----------|-------------| +| `scientific_name` | Yes | Binomial name (e.g., "Monstera deliciosa") | +| `common_name` | No | Common name (e.g., "Swiss Cheese Plant") | +| `genus` | No | Auto-extracted from scientific_name if not provided | +| `family` | No | Plant family (e.g., "Araceae") | + +**Example CSV:** +```csv +scientific_name,common_name,genus,family +Monstera deliciosa,Swiss Cheese Plant,Monstera,Araceae +Philodendron hederaceum,Heartleaf Philodendron,Philodendron,Araceae +Epipremnum aureum,Golden Pothos,Epipremnum,Araceae +``` + +### JSON Import + +Import species from a JSON file with the following structure: + +```json +{ + "plants": [ + { + "scientific_name": "Monstera deliciosa", + "common_names": ["Swiss Cheese Plant", "Split-leaf Philodendron"], + "family": "Araceae" + }, + { + "scientific_name": "Philodendron hederaceum", + "common_names": ["Heartleaf Philodendron"], + "family": "Araceae" + } + ] +} +``` + +| Field | Required | Description | +|-------|----------|-------------| +| `scientific_name` | Yes | Binomial name | +| `common_names` | No | Array of common names (first one is used) | +| `family` | No | Plant family | + +**Notes:** +- Genus is automatically extracted from the first word of `scientific_name` +- Duplicate species (by scientific_name) are skipped +- The included `houseplants_list.json` contains 2,278 houseplant species + +### API Endpoints + +```bash +# Import CSV +curl -X POST http://localhost/api/species/import \ + -F "file=@species.csv" + +# Import JSON +curl -X POST http://localhost/api/species/import-json \ + -F "file=@plants.json" +``` + +**Response:** +```json +{ + "imported": 150, + "skipped": 5, + "errors": [] +} +``` + +## Architecture + +``` +┌─────────────┐ ┌─────────────────┐ ┌─────────────┐ +│ React │────▶│ FastAPI │────▶│ Celery │ +│ Frontend │ │ Backend │ │ Workers │ +└─────────────┘ └─────────────────┘ └─────────────┘ + │ │ + ▼ ▼ + ┌─────────────┐ ┌─────────────┐ + │ SQLite │ │ Redis │ + │ Database │ │ Queue │ + └─────────────┘ └─────────────┘ +``` + +## Export Format + +Exports are Create ML-compatible: + +``` +export.zip/ +├── Training/ +│ ├── Monstera_deliciosa/ +│ │ ├── img_00001.jpg +│ │ └── ... +│ └── ... +└── Testing/ + ├── Monstera_deliciosa/ + └── ... +``` + +## Data Storage + +All data is stored in the `./data` directory: + +``` +data/ +├── db/ +│ └── plants.sqlite # SQLite database +├── images/ # Downloaded images +│ └── {species_id}/ +│ └── {image_id}.jpg +└── exports/ # Generated export archives + └── {export_id}.zip +``` + +## API Documentation + +Full API docs available at http://localhost/api/docs + +## License + +MIT diff --git a/accum_images.md b/accum_images.md new file mode 100644 index 0000000..b899d9c --- /dev/null +++ b/accum_images.md @@ -0,0 +1,231 @@ +# Houseplant Image Dataset Accumulation Plan + +## Overview + +Build a custom CoreML model for houseplant identification by accumulating a large dataset of houseplant images with proper licensing for commercial use. + +--- + +## Requirements Summary + +| Parameter | Value | +|-----------|-------| +| Target species | 5,000-10,000 (realistic houseplant ceiling) | +| Images per species | 200-500 (recommended) | +| Total images | ~1-5 million | +| Budget | Free preferred, paid as reference | +| Compute | M1 Max Mac (training) + Unraid server (data pipeline) | +| Curation | Automated pipeline | +| Timeline | Weeks-months | +| Licensing | Must allow training + commercial model distribution | + +--- + +## Hardware Assessment + +| Machine | Role | Capability | +|---------|------|------------| +| M1 Max Mac | **Training** | Create ML can train 5-10K class models; 32+ GB unified memory is ideal | +| Unraid Server | **Data pipeline** | Scraping, downloading, preprocessing, storage | + +M1 Max is legitimately viable for this task via Create ML or PyTorch+MPS. No cloud GPU required. + +--- + +## Data Sources Analysis + +### Tier 1: Primary Sources (Recommended) + +| Source | License | Commercial-Safe | Volume | Houseplant Coverage | Access Method | +|--------|---------|-----------------|--------|---------------------|---------------| +| **iNaturalist via GBIF** | CC-BY, CC0 (filter) | Yes (filtered) | 100M+ observations | Good (has "captive/cultivated" flag) | Bulk export + API | +| **Flickr** | CC-BY, CC0 (filter) | Yes (filtered) | Millions | Moderate | API | +| **Wikimedia Commons** | CC-BY, CC-BY-SA, Public Domain | Mostly | Thousands | Moderate | API | + +### Tier 2: Supplemental Sources + +| Source | License | Commercial-Safe | Notes | +|--------|---------|-----------------|-------| +| **USDA PLANTS** | Public Domain | Yes | US-focused, limited images | +| **Encyclopedia of Life** | Mixed | Check each | Aggregator, good metadata | +| **Pl@ntNet-300K Dataset** | CC-BY-SA | Share-alike | Good for research/prototyping only | + +### Tier 3: Paid Options (Reference) + +| Source | Estimated Cost | Notes | +|--------|----------------|-------| +| iNaturalist AWS Open Data | Free | Bulk image export, requires S3 costs for transfer | +| Custom scraping infrastructure | $50-200/mo | Proxies, storage, bandwidth | +| Commercial botanical databases | $1000s+ | Getty, Alamy — not recommended | + +--- + +## Licensing Decision Matrix + +``` +Want commercial model distribution? +├─ YES → Use ONLY: CC0, CC-BY, Public Domain +│ Filter OUT: CC-BY-NC, CC-BY-SA, All Rights Reserved +│ +└─ NO (research only) → Can use CC-BY-NC, CC-BY-SA + Pl@ntNet-300K dataset becomes viable +``` + +**Recommendation**: Filter for commercial-safe licenses from day 1. Avoids re-scraping later. + +--- + +## Houseplant Species Taxonomy + +**Problem**: No canonical "houseplant" species list exists. Must construct one. + +**Approach**: +1. Start with Wikipedia "List of houseplants" (~500 species) +2. Expand via genus crawl (all Philodendron, all Hoya, etc.) +3. Cross-reference with RHS, ASPCA, nursery catalogs +4. Target: **1,000-3,000 species** is realistic for quality dataset + +**Key Genera** (prioritize these — cover 80% of common houseplants): +``` +Philodendron, Monstera, Pothos/Epipremnum, Ficus, Dracaena, +Sansevieria, Calathea, Maranta, Alocasia, Anthurium, +Peperomia, Hoya, Begonia, Tradescantia, Pilea, +Aglaonema, Dieffenbachia, Spathiphyllum, Zamioculcas, Crassula +``` + +--- + +## Data Quality Requirements + +| Parameter | Minimum | Target | Rationale | +|-----------|---------|--------|-----------| +| Images per species | 100 | 300-500 | Below 100 = unreliable classification | +| Resolution | 256x256 | 512x512+ | Downsample to 224x224 for training | +| Variety | Single angle | Multi-angle, growth stages, lighting | Generalization | +| Label accuracy | 80% | 95%+ | iNaturalist "Research Grade" = community verified | + +--- + +## Training Approach Options + +### Option A: Create ML (Recommended) + +| Pros | Cons | +|------|------| +| Native Apple Silicon optimization | Limited hyperparameter control | +| Outputs CoreML directly | Max ~10K classes practical limit | +| No Python/ML expertise needed | Less flexible augmentation | +| Fast iteration | | + +**Best for**: This use case exactly. + +### Option B: PyTorch + MPS Transfer Learning + +| Pros | Cons | +|------|------| +| Full control over architecture | Steeper learning curve | +| State-of-art augmentation (albumentations) | Manual CoreML conversion | +| Can use EfficientNet, ConvNeXt, etc. | Slower iteration | + +**Best for**: If Create ML hits limits or you need custom architecture. + +### Option C: Cloud GPU (Google Colab / AWS Spot) + +| Pros | Cons | +|------|------| +| Faster training for large models | Cost | +| No local resource constraints | Network transfer overhead | + +**Best for**: If dataset exceeds M1 Max memory or you want transformer-based vision models. + +**Recommendation**: Start with Create ML. Pivot to Option B only if needed. + +--- + +## Pipeline Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ UNRAID SERVER │ +├─────────────────────────────────────────────────────────────────┤ +│ 1. Species List Generator │ +│ └─ Scrape Wikipedia, RHS, expand by genus │ +│ │ +│ 2. Image Downloader │ +│ ├─ iNaturalist/GBIF bulk export (primary) │ +│ ├─ Flickr API (supplemental) │ +│ └─ License filter (CC-BY, CC0 only) │ +│ │ +│ 3. Preprocessing Pipeline │ +│ ├─ Resize to 512x512 │ +│ ├─ Remove duplicates (perceptual hash) │ +│ ├─ Remove low-quality (blur detection, size filter) │ +│ └─ Organize: /species_name/image_001.jpg │ +│ │ +│ 4. Dataset Statistics │ +│ └─ Report per-species counts, flag under-represented │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ (rsync/SMB) +┌─────────────────────────────────────────────────────────────────┐ +│ M1 MAX MAC │ +├─────────────────────────────────────────────────────────────────┤ +│ 5. Create ML Training │ +│ ├─ Import dataset folder │ +│ ├─ Train image classifier │ +│ └─ Export .mlmodel │ +│ │ +│ 6. Validation │ +│ ├─ Test on held-out images │ +│ └─ Test on real-world photos (your phone) │ +│ │ +│ 7. Integration │ +│ └─ Replace PlantNet-300K in PlantGuide │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Timeline + +| Phase | Duration | Output | +|-------|----------|--------| +| 1. Species list curation | 1 week | 1,000-3,000 target species with scientific + common names | +| 2. Pipeline development | 1-2 weeks | Automated scraper on Unraid | +| 3. Data collection | 2-4 weeks | Running 24/7, rate-limited by APIs | +| 4. Preprocessing + QA | 1 week | Clean dataset, statistics report | +| 5. Initial training | 2-3 days | First model with subset (500 species) | +| 6. Full training | 1 week | Full model, iteration | +| 7. Validation + tuning | 1 week | Production-ready model | + +**Total: 6-10 weeks** + +--- + +## Risk Analysis + +| Risk | Likelihood | Mitigation | +|------|------------|------------| +| Insufficient images for rare species | High | Accept lower coverage OR merge to genus-level for rare species | +| API rate limits slow collection | High | Parallelize sources, use bulk exports, patience | +| Noisy labels degrade accuracy | Medium | Use only "Research Grade" iNaturalist, implement confidence thresholds | +| Create ML memory limits | Low | M1 Max should handle; fallback to PyTorch | +| License ambiguity | Low | Strict filter on download, keep metadata | + +--- + +## Next Steps + +1. **Build species master list** — Python script to scrape/merge sources +2. **Set up GBIF bulk download** — Filter: Plantae, captive/cultivated, CC-BY/CC0, has images +3. **Build Flickr supplemental scraper** — Target under-represented species +4. **Docker container on Unraid** — Orchestrate pipeline +5. **Create ML project setup** — Folder structure, initial test with 50 species + +--- + +## Open Questions + +- Prioritize **speed** (start with 500 species, fast iteration) or **completeness** (build full 3K species list first)? +- Any specific houseplant species that must be included? +- Docker running on Unraid already? diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..1aba55a --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,24 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + gcc \ + g++ \ + libffi-dev \ + && rm -rf /var/lib/apt/lists/* + +# Install Python dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Create data directories +RUN mkdir -p /data/db /data/images /data/exports + +EXPOSE 8000 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/add_indexes.py b/backend/add_indexes.py new file mode 100644 index 0000000..fd1f891 --- /dev/null +++ b/backend/add_indexes.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python +"""Add missing database indexes.""" +from sqlalchemy import text +from app.database import engine + +with engine.connect() as conn: + # Single column indexes + conn.execute(text('CREATE INDEX IF NOT EXISTS ix_images_license ON images(license)')) + conn.execute(text('CREATE INDEX IF NOT EXISTS ix_images_status ON images(status)')) + conn.execute(text('CREATE INDEX IF NOT EXISTS ix_images_source ON images(source)')) + conn.execute(text('CREATE INDEX IF NOT EXISTS ix_images_species_id ON images(species_id)')) + conn.execute(text('CREATE INDEX IF NOT EXISTS ix_images_phash ON images(phash)')) + + # Composite indexes for common query patterns + conn.execute(text('CREATE INDEX IF NOT EXISTS ix_images_species_status ON images(species_id, status)')) + conn.execute(text('CREATE INDEX IF NOT EXISTS ix_images_status_created ON images(status, created_at)')) + + conn.commit() + print('All indexes created successfully') diff --git a/backend/alembic.ini b/backend/alembic.ini new file mode 100644 index 0000000..7e85cc4 --- /dev/null +++ b/backend/alembic.ini @@ -0,0 +1,42 @@ +[alembic] +script_location = alembic +prepend_sys_path = . +version_path_separator = os + +sqlalchemy.url = sqlite:////data/db/plants.sqlite + +[post_write_hooks] + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/backend/alembic/env.py b/backend/alembic/env.py new file mode 100644 index 0000000..388bf03 --- /dev/null +++ b/backend/alembic/env.py @@ -0,0 +1,54 @@ +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +# Import models for autogenerate +from app.database import Base +from app.models import Species, Image, Job, ApiKey, Export + +config = context.config + +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +target_metadata = Base.metadata + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode.""" + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode.""" + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/backend/alembic/script.py.mako b/backend/alembic/script.py.mako new file mode 100644 index 0000000..fbc4b07 --- /dev/null +++ b/backend/alembic/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/backend/alembic/versions/001_initial.py b/backend/alembic/versions/001_initial.py new file mode 100644 index 0000000..03659ad --- /dev/null +++ b/backend/alembic/versions/001_initial.py @@ -0,0 +1,112 @@ +"""Initial migration + +Revision ID: 001 +Revises: +Create Date: 2024-01-01 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +revision: str = '001' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Species table + op.create_table( + 'species', + sa.Column('id', sa.Integer(), primary_key=True), + sa.Column('scientific_name', sa.String(), nullable=False, unique=True), + sa.Column('common_name', sa.String(), nullable=True), + sa.Column('genus', sa.String(), nullable=True), + sa.Column('family', sa.String(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()), + ) + op.create_index('ix_species_scientific_name', 'species', ['scientific_name']) + op.create_index('ix_species_genus', 'species', ['genus']) + + # API Keys table + op.create_table( + 'api_keys', + sa.Column('id', sa.Integer(), primary_key=True), + sa.Column('source', sa.String(), nullable=False, unique=True), + sa.Column('api_key', sa.String(), nullable=False), + sa.Column('api_secret', sa.String(), nullable=True), + sa.Column('rate_limit_per_sec', sa.Float(), default=1.0), + sa.Column('enabled', sa.Boolean(), default=True), + ) + + # Images table + op.create_table( + 'images', + sa.Column('id', sa.Integer(), primary_key=True), + sa.Column('species_id', sa.Integer(), sa.ForeignKey('species.id'), nullable=False), + sa.Column('source', sa.String(), nullable=False), + sa.Column('source_id', sa.String(), nullable=True), + sa.Column('url', sa.String(), nullable=False), + sa.Column('local_path', sa.String(), nullable=True), + sa.Column('license', sa.String(), nullable=False), + sa.Column('attribution', sa.String(), nullable=True), + sa.Column('width', sa.Integer(), nullable=True), + sa.Column('height', sa.Integer(), nullable=True), + sa.Column('phash', sa.String(), nullable=True), + sa.Column('quality_score', sa.Float(), nullable=True), + sa.Column('status', sa.String(), default='pending'), + sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()), + ) + op.create_index('ix_images_species_id', 'images', ['species_id']) + op.create_index('ix_images_source', 'images', ['source']) + op.create_index('ix_images_status', 'images', ['status']) + op.create_index('ix_images_phash', 'images', ['phash']) + op.create_unique_constraint('uq_source_source_id', 'images', ['source', 'source_id']) + + # Jobs table + op.create_table( + 'jobs', + sa.Column('id', sa.Integer(), primary_key=True), + sa.Column('name', sa.String(), nullable=False), + sa.Column('source', sa.String(), nullable=False), + sa.Column('species_filter', sa.Text(), nullable=True), + sa.Column('status', sa.String(), default='pending'), + sa.Column('progress_current', sa.Integer(), default=0), + sa.Column('progress_total', sa.Integer(), default=0), + sa.Column('images_downloaded', sa.Integer(), default=0), + sa.Column('images_rejected', sa.Integer(), default=0), + sa.Column('celery_task_id', sa.String(), nullable=True), + sa.Column('started_at', sa.DateTime(), nullable=True), + sa.Column('completed_at', sa.DateTime(), nullable=True), + sa.Column('error_message', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()), + ) + op.create_index('ix_jobs_status', 'jobs', ['status']) + + # Exports table + op.create_table( + 'exports', + sa.Column('id', sa.Integer(), primary_key=True), + sa.Column('name', sa.String(), nullable=False), + sa.Column('filter_criteria', sa.Text(), nullable=True), + sa.Column('train_split', sa.Float(), default=0.8), + sa.Column('status', sa.String(), default='pending'), + sa.Column('file_path', sa.String(), nullable=True), + sa.Column('file_size', sa.Integer(), nullable=True), + sa.Column('species_count', sa.Integer(), nullable=True), + sa.Column('image_count', sa.Integer(), nullable=True), + sa.Column('celery_task_id', sa.String(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()), + sa.Column('completed_at', sa.DateTime(), nullable=True), + sa.Column('error_message', sa.Text(), nullable=True), + ) + + +def downgrade() -> None: + op.drop_table('exports') + op.drop_table('jobs') + op.drop_table('images') + op.drop_table('api_keys') + op.drop_table('species') diff --git a/backend/alembic/versions/002_add_cached_stats_and_indexes.py b/backend/alembic/versions/002_add_cached_stats_and_indexes.py new file mode 100644 index 0000000..0983475 --- /dev/null +++ b/backend/alembic/versions/002_add_cached_stats_and_indexes.py @@ -0,0 +1,53 @@ +"""Add cached_stats table and license index + +Revision ID: 002 +Revises: 001 +Create Date: 2025-01-25 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +revision: str = '002' +down_revision: Union[str, None] = '001' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Cached stats table for pre-calculated dashboard statistics + op.create_table( + 'cached_stats', + sa.Column('id', sa.Integer(), primary_key=True), + sa.Column('key', sa.String(50), nullable=False, unique=True), + sa.Column('value', sa.Text(), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.func.now()), + ) + op.create_index('ix_cached_stats_key', 'cached_stats', ['key']) + + # Add license index to images table (if not exists) + # Using batch mode for SQLite compatibility + try: + op.create_index('ix_images_license', 'images', ['license']) + except Exception: + pass # Index may already exist + + # Add only_without_images column to jobs if it doesn't exist + try: + op.add_column('jobs', sa.Column('only_without_images', sa.Boolean(), default=False)) + except Exception: + pass # Column may already exist + + +def downgrade() -> None: + try: + op.drop_index('ix_images_license', 'images') + except Exception: + pass + try: + op.drop_column('jobs', 'only_without_images') + except Exception: + pass + op.drop_table('cached_stats') diff --git a/backend/alembic/versions/003_add_job_max_images.py b/backend/alembic/versions/003_add_job_max_images.py new file mode 100644 index 0000000..b5f0959 --- /dev/null +++ b/backend/alembic/versions/003_add_job_max_images.py @@ -0,0 +1,31 @@ +"""Add max_images column to jobs table + +Revision ID: 003 +Revises: 002 +Create Date: 2025-01-25 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +revision: str = '003' +down_revision: Union[str, None] = '002' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Add max_images column to jobs table + try: + op.add_column('jobs', sa.Column('max_images', sa.Integer(), nullable=True)) + except Exception: + pass # Column may already exist + + +def downgrade() -> None: + try: + op.drop_column('jobs', 'max_images') + except Exception: + pass diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..592c7c4 --- /dev/null +++ b/backend/app/__init__.py @@ -0,0 +1 @@ +# PlantGuideScraper Backend diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py new file mode 100644 index 0000000..2a30ed8 --- /dev/null +++ b/backend/app/api/__init__.py @@ -0,0 +1 @@ +# API routes diff --git a/backend/app/api/exports.py b/backend/app/api/exports.py new file mode 100644 index 0000000..fc18942 --- /dev/null +++ b/backend/app/api/exports.py @@ -0,0 +1,175 @@ +import json +import os +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException, Query +from fastapi.responses import FileResponse +from sqlalchemy.orm import Session +from sqlalchemy import func + +from app.database import get_db +from app.models import Export, Image, Species +from app.schemas.export import ( + ExportCreate, + ExportResponse, + ExportListResponse, + ExportPreview, +) +from app.workers.export_tasks import generate_export + +router = APIRouter() + + +@router.get("", response_model=ExportListResponse) +def list_exports( + limit: int = Query(50, ge=1, le=200), + db: Session = Depends(get_db), +): + """List all exports.""" + total = db.query(Export).count() + exports = db.query(Export).order_by(Export.created_at.desc()).limit(limit).all() + + return ExportListResponse( + items=[ExportResponse.model_validate(e) for e in exports], + total=total, + ) + + +@router.post("/preview", response_model=ExportPreview) +def preview_export(export: ExportCreate, db: Session = Depends(get_db)): + """Preview export without creating it.""" + criteria = export.filter_criteria + min_images = criteria.min_images_per_species + + # Build query + query = db.query(Image).filter(Image.status == "downloaded") + + if criteria.licenses: + query = query.filter(Image.license.in_(criteria.licenses)) + + if criteria.min_quality: + query = query.filter(Image.quality_score >= criteria.min_quality) + + if criteria.species_ids: + query = query.filter(Image.species_id.in_(criteria.species_ids)) + + # Count images per species + species_counts = db.query( + Image.species_id, + func.count(Image.id).label("count") + ).filter(Image.status == "downloaded") + + if criteria.licenses: + species_counts = species_counts.filter(Image.license.in_(criteria.licenses)) + if criteria.min_quality: + species_counts = species_counts.filter(Image.quality_score >= criteria.min_quality) + if criteria.species_ids: + species_counts = species_counts.filter(Image.species_id.in_(criteria.species_ids)) + + species_counts = species_counts.group_by(Image.species_id).all() + + valid_species = [s for s in species_counts if s.count >= min_images] + total_images = sum(s.count for s in valid_species) + + # Estimate file size (rough: 50KB per image) + estimated_size_mb = (total_images * 50) / 1024 + + return ExportPreview( + species_count=len(valid_species), + image_count=total_images, + estimated_size_mb=estimated_size_mb, + ) + + +@router.post("", response_model=ExportResponse) +def create_export(export: ExportCreate, db: Session = Depends(get_db)): + """Create and start a new export job.""" + db_export = Export( + name=export.name, + filter_criteria=export.filter_criteria.model_dump_json(), + train_split=export.train_split, + status="pending", + ) + db.add(db_export) + db.commit() + db.refresh(db_export) + + # Start Celery task + task = generate_export.delay(db_export.id) + db_export.celery_task_id = task.id + db.commit() + + return ExportResponse.model_validate(db_export) + + +@router.get("/{export_id}", response_model=ExportResponse) +def get_export(export_id: int, db: Session = Depends(get_db)): + """Get export status.""" + export = db.query(Export).filter(Export.id == export_id).first() + if not export: + raise HTTPException(status_code=404, detail="Export not found") + + return ExportResponse.model_validate(export) + + +@router.get("/{export_id}/progress") +def get_export_progress(export_id: int, db: Session = Depends(get_db)): + """Get real-time export progress.""" + from app.workers.celery_app import celery_app + + export = db.query(Export).filter(Export.id == export_id).first() + if not export: + raise HTTPException(status_code=404, detail="Export not found") + + if not export.celery_task_id: + return {"status": export.status} + + result = celery_app.AsyncResult(export.celery_task_id) + + if result.state == "PROGRESS": + meta = result.info + return { + "status": "generating", + "current": meta.get("current", 0), + "total": meta.get("total", 0), + "current_species": meta.get("species", ""), + } + + return {"status": export.status} + + +@router.get("/{export_id}/download") +def download_export(export_id: int, db: Session = Depends(get_db)): + """Download export zip file.""" + export = db.query(Export).filter(Export.id == export_id).first() + if not export: + raise HTTPException(status_code=404, detail="Export not found") + + if export.status != "completed": + raise HTTPException(status_code=400, detail="Export not ready") + + if not export.file_path or not os.path.exists(export.file_path): + raise HTTPException(status_code=404, detail="Export file not found") + + return FileResponse( + export.file_path, + media_type="application/zip", + filename=f"{export.name}.zip", + ) + + +@router.delete("/{export_id}") +def delete_export(export_id: int, db: Session = Depends(get_db)): + """Delete an export and its file.""" + export = db.query(Export).filter(Export.id == export_id).first() + if not export: + raise HTTPException(status_code=404, detail="Export not found") + + # Delete file if exists + if export.file_path and os.path.exists(export.file_path): + os.remove(export.file_path) + + db.delete(export) + db.commit() + + return {"status": "deleted"} diff --git a/backend/app/api/images.py b/backend/app/api/images.py new file mode 100644 index 0000000..7c0a217 --- /dev/null +++ b/backend/app/api/images.py @@ -0,0 +1,441 @@ +import os +import shutil +import uuid +from pathlib import Path +from typing import Optional, List + +from fastapi import APIRouter, Depends, HTTPException, Query +from fastapi.responses import FileResponse +from sqlalchemy.orm import Session +from sqlalchemy import func +from PIL import Image as PILImage + +from app.database import get_db +from app.models import Image, Species +from app.schemas.image import ImageResponse, ImageListResponse +from app.config import get_settings + +router = APIRouter() +settings = get_settings() + + +@router.get("", response_model=ImageListResponse) +def list_images( + page: int = Query(1, ge=1), + page_size: int = Query(50, ge=1, le=200), + species_id: Optional[int] = None, + source: Optional[str] = None, + license: Optional[str] = None, + status: Optional[str] = None, + min_quality: Optional[float] = None, + search: Optional[str] = None, + db: Session = Depends(get_db), +): + """List images with pagination and filters.""" + # Use joinedload to fetch species in single query + from sqlalchemy.orm import joinedload + query = db.query(Image).options(joinedload(Image.species)) + + if species_id: + query = query.filter(Image.species_id == species_id) + + if source: + query = query.filter(Image.source == source) + + if license: + query = query.filter(Image.license == license) + + if status: + query = query.filter(Image.status == status) + + if min_quality: + query = query.filter(Image.quality_score >= min_quality) + + if search: + search_term = f"%{search}%" + query = query.join(Species).filter( + (Species.scientific_name.ilike(search_term)) | + (Species.common_name.ilike(search_term)) + ) + + # Use faster count for simple queries + if not search: + # Build count query without join for better performance + count_query = db.query(func.count(Image.id)) + if species_id: + count_query = count_query.filter(Image.species_id == species_id) + if source: + count_query = count_query.filter(Image.source == source) + if license: + count_query = count_query.filter(Image.license == license) + if status: + count_query = count_query.filter(Image.status == status) + if min_quality: + count_query = count_query.filter(Image.quality_score >= min_quality) + total = count_query.scalar() + else: + total = query.count() + + pages = (total + page_size - 1) // page_size + + images = query.order_by(Image.created_at.desc()).offset( + (page - 1) * page_size + ).limit(page_size).all() + + items = [ + ImageResponse( + id=img.id, + species_id=img.species_id, + species_name=img.species.scientific_name if img.species else None, + source=img.source, + source_id=img.source_id, + url=img.url, + local_path=img.local_path, + license=img.license, + attribution=img.attribution, + width=img.width, + height=img.height, + quality_score=img.quality_score, + status=img.status, + created_at=img.created_at, + ) + for img in images + ] + + return ImageListResponse( + items=items, + total=total, + page=page, + page_size=page_size, + pages=pages, + ) + + +@router.get("/sources") +def list_sources(db: Session = Depends(get_db)): + """List all unique image sources.""" + sources = db.query(Image.source).distinct().all() + return [s[0] for s in sources] + + +@router.get("/licenses") +def list_licenses(db: Session = Depends(get_db)): + """List all unique licenses.""" + licenses = db.query(Image.license).distinct().all() + return [l[0] for l in licenses] + + +@router.post("/process-pending") +def process_pending_images( + source: Optional[str] = None, + db: Session = Depends(get_db), +): + """Queue all pending images for download and processing.""" + from app.workers.quality_tasks import batch_process_pending_images + + query = db.query(func.count(Image.id)).filter(Image.status == "pending") + if source: + query = query.filter(Image.source == source) + pending_count = query.scalar() + + task = batch_process_pending_images.delay(source=source) + + return { + "pending_count": pending_count, + "task_id": task.id, + } + + +@router.get("/process-pending/status/{task_id}") +def process_pending_status(task_id: str): + """Check status of a batch processing task.""" + from app.workers.celery_app import celery_app + + result = celery_app.AsyncResult(task_id) + state = result.state # PENDING, STARTED, PROGRESS, SUCCESS, FAILURE + + response = {"task_id": task_id, "state": state} + + if state == "PROGRESS" and isinstance(result.info, dict): + response["queued"] = result.info.get("queued", 0) + response["total"] = result.info.get("total", 0) + elif state == "SUCCESS" and isinstance(result.result, dict): + response["queued"] = result.result.get("queued", 0) + response["total"] = result.result.get("total", 0) + + return response + + +@router.get("/{image_id}", response_model=ImageResponse) +def get_image(image_id: int, db: Session = Depends(get_db)): + """Get an image by ID.""" + image = db.query(Image).filter(Image.id == image_id).first() + if not image: + raise HTTPException(status_code=404, detail="Image not found") + + return ImageResponse( + id=image.id, + species_id=image.species_id, + species_name=image.species.scientific_name if image.species else None, + source=image.source, + source_id=image.source_id, + url=image.url, + local_path=image.local_path, + license=image.license, + attribution=image.attribution, + width=image.width, + height=image.height, + quality_score=image.quality_score, + status=image.status, + created_at=image.created_at, + ) + + +@router.get("/{image_id}/file") +def get_image_file(image_id: int, db: Session = Depends(get_db)): + """Get the actual image file.""" + image = db.query(Image).filter(Image.id == image_id).first() + if not image: + raise HTTPException(status_code=404, detail="Image not found") + + if not image.local_path: + raise HTTPException(status_code=404, detail="Image file not available") + + return FileResponse(image.local_path, media_type="image/jpeg") + + +@router.delete("/{image_id}") +def delete_image(image_id: int, db: Session = Depends(get_db)): + """Delete an image.""" + image = db.query(Image).filter(Image.id == image_id).first() + if not image: + raise HTTPException(status_code=404, detail="Image not found") + + # Delete file if exists + if image.local_path: + import os + if os.path.exists(image.local_path): + os.remove(image.local_path) + + db.delete(image) + db.commit() + + return {"status": "deleted"} + + +@router.post("/bulk-delete") +def bulk_delete_images( + image_ids: List[int], + db: Session = Depends(get_db), +): + """Delete multiple images.""" + import os + + images = db.query(Image).filter(Image.id.in_(image_ids)).all() + + deleted = 0 + for image in images: + if image.local_path and os.path.exists(image.local_path): + os.remove(image.local_path) + db.delete(image) + deleted += 1 + + db.commit() + + return {"deleted": deleted} + + +@router.get("/import/scan") +def scan_imports(db: Session = Depends(get_db)): + """Scan the imports folder and return what can be imported. + + Expected structure: imports/{source}/{species_name}/*.jpg + """ + imports_path = Path(settings.imports_path) + + if not imports_path.exists(): + return { + "available": False, + "message": f"Imports folder not found: {imports_path}", + "sources": [], + "total_images": 0, + "matched_species": 0, + "unmatched_species": [], + } + + results = { + "available": True, + "sources": [], + "total_images": 0, + "matched_species": 0, + "unmatched_species": [], + } + + # Get all species for matching + species_map = {} + for species in db.query(Species).all(): + # Map by scientific name with underscores and spaces + species_map[species.scientific_name.lower()] = species + species_map[species.scientific_name.replace(" ", "_").lower()] = species + + seen_unmatched = set() + + # Scan source folders + for source_dir in imports_path.iterdir(): + if not source_dir.is_dir(): + continue + + source_name = source_dir.name + source_info = { + "name": source_name, + "species_count": 0, + "image_count": 0, + } + + # Scan species folders within source + for species_dir in source_dir.iterdir(): + if not species_dir.is_dir(): + continue + + species_name = species_dir.name.replace("_", " ") + species_key = species_name.lower() + + # Count images + image_files = list(species_dir.glob("*.jpg")) + \ + list(species_dir.glob("*.jpeg")) + \ + list(species_dir.glob("*.png")) + + if not image_files: + continue + + source_info["image_count"] += len(image_files) + results["total_images"] += len(image_files) + + if species_key in species_map or species_dir.name.lower() in species_map: + source_info["species_count"] += 1 + results["matched_species"] += 1 + else: + if species_name not in seen_unmatched: + seen_unmatched.add(species_name) + results["unmatched_species"].append(species_name) + + if source_info["image_count"] > 0: + results["sources"].append(source_info) + + return results + + +@router.post("/import/run") +def run_import( + move_files: bool = Query(False, description="Move files instead of copy"), + db: Session = Depends(get_db), +): + """Import images from the imports folder. + + Expected structure: imports/{source}/{species_name}/*.jpg + Images are copied/moved to: images/{species_name}/{source}_{filename} + """ + imports_path = Path(settings.imports_path) + images_path = Path(settings.images_path) + + if not imports_path.exists(): + raise HTTPException(status_code=400, detail="Imports folder not found") + + # Get all species for matching + species_map = {} + for species in db.query(Species).all(): + species_map[species.scientific_name.lower()] = species + species_map[species.scientific_name.replace(" ", "_").lower()] = species + + imported = 0 + skipped = 0 + errors = [] + + # Scan source folders + for source_dir in imports_path.iterdir(): + if not source_dir.is_dir(): + continue + + source_name = source_dir.name + + # Scan species folders within source + for species_dir in source_dir.iterdir(): + if not species_dir.is_dir(): + continue + + species_name = species_dir.name.replace("_", " ") + species_key = species_name.lower() + + # Find matching species + species = species_map.get(species_key) or species_map.get(species_dir.name.lower()) + if not species: + continue + + # Create target directory + target_dir = images_path / species.scientific_name.replace(" ", "_") + target_dir.mkdir(parents=True, exist_ok=True) + + # Process images + image_files = list(species_dir.glob("*.jpg")) + \ + list(species_dir.glob("*.jpeg")) + \ + list(species_dir.glob("*.png")) + + for img_file in image_files: + try: + # Generate unique filename + ext = img_file.suffix.lower() + if ext == ".jpeg": + ext = ".jpg" + new_filename = f"{source_name}_{img_file.stem}_{uuid.uuid4().hex[:8]}{ext}" + target_path = target_dir / new_filename + + # Check if already imported (by original filename pattern) + existing = db.query(Image).filter( + Image.species_id == species.id, + Image.source == source_name, + Image.source_id == img_file.stem, + ).first() + + if existing: + skipped += 1 + continue + + # Get image dimensions + try: + with PILImage.open(img_file) as pil_img: + width, height = pil_img.size + except Exception: + width, height = None, None + + # Copy or move file + if move_files: + shutil.move(str(img_file), str(target_path)) + else: + shutil.copy2(str(img_file), str(target_path)) + + # Create database record + image = Image( + species_id=species.id, + source=source_name, + source_id=img_file.stem, + url=f"file://{img_file}", + local_path=str(target_path), + license="unknown", + width=width, + height=height, + status="downloaded", + ) + db.add(image) + imported += 1 + + except Exception as e: + errors.append(f"{img_file}: {str(e)}") + + # Commit after each species to avoid large transactions + db.commit() + + return { + "imported": imported, + "skipped": skipped, + "errors": errors[:20], + } diff --git a/backend/app/api/jobs.py b/backend/app/api/jobs.py new file mode 100644 index 0000000..fe8c5f1 --- /dev/null +++ b/backend/app/api/jobs.py @@ -0,0 +1,173 @@ +import json +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session + +from app.database import get_db +from app.models import Job +from app.schemas.job import JobCreate, JobResponse, JobListResponse +from app.workers.scrape_tasks import run_scrape_job + +router = APIRouter() + + +@router.get("", response_model=JobListResponse) +def list_jobs( + status: Optional[str] = None, + source: Optional[str] = None, + limit: int = Query(50, ge=1, le=200), + db: Session = Depends(get_db), +): + """List all jobs.""" + query = db.query(Job) + + if status: + query = query.filter(Job.status == status) + + if source: + query = query.filter(Job.source == source) + + total = query.count() + jobs = query.order_by(Job.created_at.desc()).limit(limit).all() + + return JobListResponse( + items=[JobResponse.model_validate(j) for j in jobs], + total=total, + ) + + +@router.post("", response_model=JobResponse) +def create_job(job: JobCreate, db: Session = Depends(get_db)): + """Create and start a new scrape job.""" + species_filter = None + if job.species_ids: + species_filter = json.dumps(job.species_ids) + + db_job = Job( + name=job.name, + source=job.source, + species_filter=species_filter, + only_without_images=job.only_without_images, + max_images=job.max_images, + status="pending", + ) + db.add(db_job) + db.commit() + db.refresh(db_job) + + # Start the Celery task + task = run_scrape_job.delay(db_job.id) + db_job.celery_task_id = task.id + db.commit() + + return JobResponse.model_validate(db_job) + + +@router.get("/{job_id}", response_model=JobResponse) +def get_job(job_id: int, db: Session = Depends(get_db)): + """Get job status.""" + job = db.query(Job).filter(Job.id == job_id).first() + if not job: + raise HTTPException(status_code=404, detail="Job not found") + + return JobResponse.model_validate(job) + + +@router.get("/{job_id}/progress") +def get_job_progress(job_id: int, db: Session = Depends(get_db)): + """Get real-time job progress from Celery.""" + from app.workers.celery_app import celery_app + + job = db.query(Job).filter(Job.id == job_id).first() + if not job: + raise HTTPException(status_code=404, detail="Job not found") + + if not job.celery_task_id: + return { + "status": job.status, + "progress_current": job.progress_current, + "progress_total": job.progress_total, + } + + # Get Celery task state + result = celery_app.AsyncResult(job.celery_task_id) + + if result.state == "PROGRESS": + meta = result.info + return { + "status": "running", + "progress_current": meta.get("current", 0), + "progress_total": meta.get("total", 0), + "current_species": meta.get("species", ""), + } + + return { + "status": job.status, + "progress_current": job.progress_current, + "progress_total": job.progress_total, + } + + +@router.post("/{job_id}/pause") +def pause_job(job_id: int, db: Session = Depends(get_db)): + """Pause a running job.""" + from app.workers.celery_app import celery_app + + job = db.query(Job).filter(Job.id == job_id).first() + if not job: + raise HTTPException(status_code=404, detail="Job not found") + + if job.status != "running": + raise HTTPException(status_code=400, detail="Job is not running") + + # Revoke Celery task + if job.celery_task_id: + celery_app.control.revoke(job.celery_task_id, terminate=True) + + job.status = "paused" + db.commit() + + return {"status": "paused"} + + +@router.post("/{job_id}/resume") +def resume_job(job_id: int, db: Session = Depends(get_db)): + """Resume a paused job.""" + job = db.query(Job).filter(Job.id == job_id).first() + if not job: + raise HTTPException(status_code=404, detail="Job not found") + + if job.status != "paused": + raise HTTPException(status_code=400, detail="Job is not paused") + + # Start new Celery task + task = run_scrape_job.delay(job.id) + job.celery_task_id = task.id + job.status = "pending" + db.commit() + + return {"status": "resumed"} + + +@router.post("/{job_id}/cancel") +def cancel_job(job_id: int, db: Session = Depends(get_db)): + """Cancel a job.""" + from app.workers.celery_app import celery_app + + job = db.query(Job).filter(Job.id == job_id).first() + if not job: + raise HTTPException(status_code=404, detail="Job not found") + + if job.status in ["completed", "failed"]: + raise HTTPException(status_code=400, detail="Job already finished") + + # Revoke Celery task + if job.celery_task_id: + celery_app.control.revoke(job.celery_task_id, terminate=True) + + job.status = "failed" + job.error_message = "Cancelled by user" + db.commit() + + return {"status": "cancelled"} diff --git a/backend/app/api/sources.py b/backend/app/api/sources.py new file mode 100644 index 0000000..d68831b --- /dev/null +++ b/backend/app/api/sources.py @@ -0,0 +1,198 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session + +from app.database import get_db +from app.models import ApiKey +from app.schemas.api_key import ApiKeyCreate, ApiKeyUpdate, ApiKeyResponse + +router = APIRouter() + +# Available sources +# auth_type: "none" (no auth), "api_key" (single key), "api_key_secret" (key + secret), "oauth" (client_id + client_secret + access_token) +# default_rate: safe default requests per second for each API +AVAILABLE_SOURCES = [ + {"name": "gbif", "label": "GBIF", "requires_secret": False, "auth_type": "none", "default_rate": 1.0}, # Free, no auth required + {"name": "inaturalist", "label": "iNaturalist", "requires_secret": True, "auth_type": "api_key_secret", "default_rate": 1.0}, # 60/min limit + {"name": "flickr", "label": "Flickr", "requires_secret": True, "auth_type": "api_key_secret", "default_rate": 0.5}, # 3600/hr shared limit + {"name": "wikimedia", "label": "Wikimedia Commons", "requires_secret": True, "auth_type": "oauth", "default_rate": 1.0}, # generous limits + {"name": "trefle", "label": "Trefle.io", "requires_secret": False, "auth_type": "api_key", "default_rate": 1.0}, # 120/min limit + {"name": "duckduckgo", "label": "DuckDuckGo", "requires_secret": False, "auth_type": "none", "default_rate": 0.5}, # Web search, no API key + {"name": "bing", "label": "Bing Image Search", "requires_secret": False, "auth_type": "api_key", "default_rate": 3.0}, # Azure Cognitive Services +] + + +def mask_api_key(key: str) -> str: + """Mask API key, showing only last 4 characters.""" + if not key or len(key) <= 4: + return "****" + return "*" * (len(key) - 4) + key[-4:] + + +@router.get("") +def list_sources(db: Session = Depends(get_db)): + """List all available sources with their configuration status.""" + api_keys = {k.source: k for k in db.query(ApiKey).all()} + + result = [] + for source in AVAILABLE_SOURCES: + api_key = api_keys.get(source["name"]) + default_rate = source.get("default_rate", 1.0) + result.append({ + "name": source["name"], + "label": source["label"], + "requires_secret": source["requires_secret"], + "auth_type": source.get("auth_type", "api_key"), + "configured": api_key is not None, + "enabled": api_key.enabled if api_key else False, + "api_key_masked": mask_api_key(api_key.api_key) if api_key else None, + "has_secret": bool(api_key.api_secret) if api_key else False, + "has_access_token": bool(getattr(api_key, 'access_token', None)) if api_key else False, + "rate_limit_per_sec": api_key.rate_limit_per_sec if api_key else default_rate, + "default_rate": default_rate, + }) + + return result + + +@router.get("/{source}") +def get_source(source: str, db: Session = Depends(get_db)): + """Get source configuration.""" + source_info = next((s for s in AVAILABLE_SOURCES if s["name"] == source), None) + if not source_info: + raise HTTPException(status_code=404, detail="Unknown source") + + api_key = db.query(ApiKey).filter(ApiKey.source == source).first() + default_rate = source_info.get("default_rate", 1.0) + + return { + "name": source_info["name"], + "label": source_info["label"], + "requires_secret": source_info["requires_secret"], + "auth_type": source_info.get("auth_type", "api_key"), + "configured": api_key is not None, + "enabled": api_key.enabled if api_key else False, + "api_key_masked": mask_api_key(api_key.api_key) if api_key else None, + "has_secret": bool(api_key.api_secret) if api_key else False, + "has_access_token": bool(getattr(api_key, 'access_token', None)) if api_key else False, + "rate_limit_per_sec": api_key.rate_limit_per_sec if api_key else default_rate, + "default_rate": default_rate, + } + + +@router.put("/{source}") +def update_source( + source: str, + config: ApiKeyCreate, + db: Session = Depends(get_db), +): + """Create or update source configuration.""" + source_info = next((s for s in AVAILABLE_SOURCES if s["name"] == source), None) + if not source_info: + raise HTTPException(status_code=404, detail="Unknown source") + + # For sources that require auth, validate api_key is provided + auth_type = source_info.get("auth_type", "api_key") + if auth_type != "none" and not config.api_key: + raise HTTPException(status_code=400, detail="API key is required for this source") + + api_key = db.query(ApiKey).filter(ApiKey.source == source).first() + + # Use placeholder for no-auth sources + api_key_value = config.api_key or "no-auth" + + if api_key: + # Update existing + api_key.api_key = api_key_value + if config.api_secret: + api_key.api_secret = config.api_secret + if config.access_token: + api_key.access_token = config.access_token + api_key.rate_limit_per_sec = config.rate_limit_per_sec + api_key.enabled = config.enabled + else: + # Create new + api_key = ApiKey( + source=source, + api_key=api_key_value, + api_secret=config.api_secret, + access_token=config.access_token, + rate_limit_per_sec=config.rate_limit_per_sec, + enabled=config.enabled, + ) + db.add(api_key) + + db.commit() + db.refresh(api_key) + + return { + "name": source, + "configured": True, + "enabled": api_key.enabled, + "api_key_masked": mask_api_key(api_key.api_key) if auth_type != "none" else None, + "has_secret": bool(api_key.api_secret), + "has_access_token": bool(api_key.access_token), + "rate_limit_per_sec": api_key.rate_limit_per_sec, + } + + +@router.patch("/{source}") +def patch_source( + source: str, + config: ApiKeyUpdate, + db: Session = Depends(get_db), +): + """Partially update source configuration.""" + api_key = db.query(ApiKey).filter(ApiKey.source == source).first() + if not api_key: + raise HTTPException(status_code=404, detail="Source not configured") + + update_data = config.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(api_key, field, value) + + db.commit() + db.refresh(api_key) + + return { + "name": source, + "configured": True, + "enabled": api_key.enabled, + "api_key_masked": mask_api_key(api_key.api_key), + "has_secret": bool(api_key.api_secret), + "has_access_token": bool(api_key.access_token), + "rate_limit_per_sec": api_key.rate_limit_per_sec, + } + + +@router.delete("/{source}") +def delete_source(source: str, db: Session = Depends(get_db)): + """Delete source configuration.""" + api_key = db.query(ApiKey).filter(ApiKey.source == source).first() + if not api_key: + raise HTTPException(status_code=404, detail="Source not configured") + + db.delete(api_key) + db.commit() + + return {"status": "deleted"} + + +@router.post("/{source}/test") +def test_source(source: str, db: Session = Depends(get_db)): + """Test source API connection.""" + api_key = db.query(ApiKey).filter(ApiKey.source == source).first() + if not api_key: + raise HTTPException(status_code=404, detail="Source not configured") + + # Import and test the scraper + from app.scrapers import get_scraper + + scraper = get_scraper(source) + if not scraper: + raise HTTPException(status_code=400, detail="No scraper for this source") + + try: + result = scraper.test_connection(api_key) + return {"status": "success", "message": result} + except Exception as e: + return {"status": "error", "message": str(e)} diff --git a/backend/app/api/species.py b/backend/app/api/species.py new file mode 100644 index 0000000..deab481 --- /dev/null +++ b/backend/app/api/species.py @@ -0,0 +1,366 @@ +import csv +import io +import json +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File +from sqlalchemy.orm import Session +from sqlalchemy import func, text + +from app.database import get_db +from app.models import Species, Image +from app.schemas.species import ( + SpeciesCreate, + SpeciesUpdate, + SpeciesResponse, + SpeciesListResponse, + SpeciesImportResponse, +) + +router = APIRouter() + + +def get_species_with_count(db: Session, species: Species) -> SpeciesResponse: + """Get species response with image count.""" + image_count = db.query(func.count(Image.id)).filter( + Image.species_id == species.id, + Image.status == "downloaded" + ).scalar() + + return SpeciesResponse( + id=species.id, + scientific_name=species.scientific_name, + common_name=species.common_name, + genus=species.genus, + family=species.family, + created_at=species.created_at, + image_count=image_count or 0, + ) + + +@router.get("", response_model=SpeciesListResponse) +def list_species( + page: int = Query(1, ge=1), + page_size: int = Query(50, ge=1, le=500), + search: Optional[str] = None, + genus: Optional[str] = None, + has_images: Optional[bool] = None, + max_images: Optional[int] = Query(None, description="Filter species with less than N images"), + min_images: Optional[int] = Query(None, description="Filter species with at least N images"), + db: Session = Depends(get_db), +): + """List species with pagination and filters. + + Filters: + - search: Search by scientific or common name + - genus: Filter by genus + - has_images: True for species with images, False for species without + - max_images: Filter species with fewer than N downloaded images + - min_images: Filter species with at least N downloaded images + """ + # If filtering by image count, we need to use a subquery approach + if max_images is not None or min_images is not None: + # Build a subquery with image counts per species + image_counts = ( + db.query( + Species.id.label("species_id"), + func.count(Image.id).label("img_count") + ) + .outerjoin(Image, (Image.species_id == Species.id) & (Image.status == "downloaded")) + .group_by(Species.id) + .subquery() + ) + + # Join species with their counts + query = db.query(Species).join( + image_counts, Species.id == image_counts.c.species_id + ) + + if max_images is not None: + query = query.filter(image_counts.c.img_count < max_images) + + if min_images is not None: + query = query.filter(image_counts.c.img_count >= min_images) + else: + query = db.query(Species) + + if search: + search_term = f"%{search}%" + query = query.filter( + (Species.scientific_name.ilike(search_term)) | + (Species.common_name.ilike(search_term)) + ) + + if genus: + query = query.filter(Species.genus == genus) + + # Filter by whether species has downloaded images (only if not using min/max filters) + if has_images is not None and max_images is None and min_images is None: + # Get IDs of species that have at least one downloaded image + species_with_images = ( + db.query(Image.species_id) + .filter(Image.status == "downloaded") + .distinct() + .subquery() + ) + if has_images: + query = query.filter(Species.id.in_(db.query(species_with_images.c.species_id))) + else: + query = query.filter(~Species.id.in_(db.query(species_with_images.c.species_id))) + + total = query.count() + pages = (total + page_size - 1) // page_size + + species_list = query.order_by(Species.scientific_name).offset( + (page - 1) * page_size + ).limit(page_size).all() + + # Fetch image counts in bulk for all species on this page + species_ids = [s.id for s in species_list] + if species_ids: + count_query = db.query( + Image.species_id, + func.count(Image.id) + ).filter( + Image.species_id.in_(species_ids), + Image.status == "downloaded" + ).group_by(Image.species_id).all() + count_map = {species_id: count for species_id, count in count_query} + else: + count_map = {} + + items = [ + SpeciesResponse( + id=s.id, + scientific_name=s.scientific_name, + common_name=s.common_name, + genus=s.genus, + family=s.family, + created_at=s.created_at, + image_count=count_map.get(s.id, 0), + ) + for s in species_list + ] + + return SpeciesListResponse( + items=items, + total=total, + page=page, + page_size=page_size, + pages=pages, + ) + + +@router.post("", response_model=SpeciesResponse) +def create_species(species: SpeciesCreate, db: Session = Depends(get_db)): + """Create a new species.""" + existing = db.query(Species).filter( + Species.scientific_name == species.scientific_name + ).first() + + if existing: + raise HTTPException(status_code=400, detail="Species already exists") + + # Auto-extract genus from scientific name if not provided + genus = species.genus + if not genus and " " in species.scientific_name: + genus = species.scientific_name.split()[0] + + db_species = Species( + scientific_name=species.scientific_name, + common_name=species.common_name, + genus=genus, + family=species.family, + ) + db.add(db_species) + db.commit() + db.refresh(db_species) + + return get_species_with_count(db, db_species) + + +@router.post("/import", response_model=SpeciesImportResponse) +async def import_species( + file: UploadFile = File(...), + db: Session = Depends(get_db), +): + """Import species from CSV file. + + Expected columns: scientific_name, common_name (optional), genus (optional), family (optional) + """ + if not file.filename.endswith(".csv"): + raise HTTPException(status_code=400, detail="File must be a CSV") + + content = await file.read() + text = content.decode("utf-8") + + reader = csv.DictReader(io.StringIO(text)) + + imported = 0 + skipped = 0 + errors = [] + + for row_num, row in enumerate(reader, start=2): + scientific_name = row.get("scientific_name", "").strip() + if not scientific_name: + errors.append(f"Row {row_num}: Missing scientific_name") + continue + + # Check if already exists + existing = db.query(Species).filter( + Species.scientific_name == scientific_name + ).first() + + if existing: + skipped += 1 + continue + + # Auto-extract genus if not provided + genus = row.get("genus", "").strip() + if not genus and " " in scientific_name: + genus = scientific_name.split()[0] + + try: + species = Species( + scientific_name=scientific_name, + common_name=row.get("common_name", "").strip() or None, + genus=genus or None, + family=row.get("family", "").strip() or None, + ) + db.add(species) + imported += 1 + except Exception as e: + errors.append(f"Row {row_num}: {str(e)}") + + db.commit() + + return SpeciesImportResponse( + imported=imported, + skipped=skipped, + errors=errors[:10], # Limit error messages + ) + + +@router.post("/import-json", response_model=SpeciesImportResponse) +async def import_species_json( + file: UploadFile = File(...), + db: Session = Depends(get_db), +): + """Import species from JSON file. + + Expected format: {"plants": [{"scientific_name": "...", "common_names": [...], "family": "..."}]} + """ + if not file.filename.endswith(".json"): + raise HTTPException(status_code=400, detail="File must be a JSON") + + content = await file.read() + try: + data = json.loads(content.decode("utf-8")) + except json.JSONDecodeError as e: + raise HTTPException(status_code=400, detail=f"Invalid JSON: {e}") + + plants = data.get("plants", []) + if not plants: + raise HTTPException(status_code=400, detail="No plants found in JSON") + + imported = 0 + skipped = 0 + errors = [] + + for idx, plant in enumerate(plants): + scientific_name = plant.get("scientific_name", "").strip() + if not scientific_name: + errors.append(f"Plant {idx}: Missing scientific_name") + continue + + # Check if already exists + existing = db.query(Species).filter( + Species.scientific_name == scientific_name + ).first() + + if existing: + skipped += 1 + continue + + # Auto-extract genus from scientific name + genus = None + if " " in scientific_name: + genus = scientific_name.split()[0] + + # Get first common name if array provided + common_names = plant.get("common_names", []) + common_name = common_names[0] if common_names else None + + try: + species = Species( + scientific_name=scientific_name, + common_name=common_name, + genus=genus, + family=plant.get("family"), + ) + db.add(species) + imported += 1 + except Exception as e: + errors.append(f"Plant {idx}: {str(e)}") + + db.commit() + + return SpeciesImportResponse( + imported=imported, + skipped=skipped, + errors=errors[:10], + ) + + +@router.get("/{species_id}", response_model=SpeciesResponse) +def get_species(species_id: int, db: Session = Depends(get_db)): + """Get a species by ID.""" + species = db.query(Species).filter(Species.id == species_id).first() + if not species: + raise HTTPException(status_code=404, detail="Species not found") + + return get_species_with_count(db, species) + + +@router.put("/{species_id}", response_model=SpeciesResponse) +def update_species( + species_id: int, + species_update: SpeciesUpdate, + db: Session = Depends(get_db), +): + """Update a species.""" + species = db.query(Species).filter(Species.id == species_id).first() + if not species: + raise HTTPException(status_code=404, detail="Species not found") + + update_data = species_update.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(species, field, value) + + db.commit() + db.refresh(species) + + return get_species_with_count(db, species) + + +@router.delete("/{species_id}") +def delete_species(species_id: int, db: Session = Depends(get_db)): + """Delete a species and all its images.""" + species = db.query(Species).filter(Species.id == species_id).first() + if not species: + raise HTTPException(status_code=404, detail="Species not found") + + db.delete(species) + db.commit() + + return {"status": "deleted"} + + +@router.get("/genera/list") +def list_genera(db: Session = Depends(get_db)): + """List all unique genera.""" + genera = db.query(Species.genus).filter( + Species.genus.isnot(None) + ).distinct().order_by(Species.genus).all() + + return [g[0] for g in genera] diff --git a/backend/app/api/stats.py b/backend/app/api/stats.py new file mode 100644 index 0000000..cc44b4d --- /dev/null +++ b/backend/app/api/stats.py @@ -0,0 +1,190 @@ +import json + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from sqlalchemy import func, case + +from app.database import get_db +from app.models import Species, Image, Job +from app.models.cached_stats import CachedStats +from app.schemas.stats import StatsResponse, SourceStats, LicenseStats, SpeciesStats, JobStats + +router = APIRouter() + + +@router.get("", response_model=StatsResponse) +def get_stats(db: Session = Depends(get_db)): + """Get dashboard statistics from cache (updated every 60s by Celery).""" + # Try to get cached stats + cached = db.query(CachedStats).filter(CachedStats.key == "dashboard_stats").first() + + if cached: + data = json.loads(cached.value) + return StatsResponse( + total_species=data["total_species"], + total_images=data["total_images"], + images_downloaded=data["images_downloaded"], + images_pending=data["images_pending"], + images_rejected=data["images_rejected"], + disk_usage_mb=data["disk_usage_mb"], + sources=[SourceStats(**s) for s in data["sources"]], + licenses=[LicenseStats(**l) for l in data["licenses"]], + jobs=JobStats(**data["jobs"]), + top_species=[SpeciesStats(**s) for s in data["top_species"]], + under_represented=[SpeciesStats(**s) for s in data["under_represented"]], + ) + + # No cache yet - return empty stats (Celery will populate soon) + # This only happens on first startup before Celery runs + return StatsResponse( + total_species=0, + total_images=0, + images_downloaded=0, + images_pending=0, + images_rejected=0, + disk_usage_mb=0.0, + sources=[], + licenses=[], + jobs=JobStats(running=0, pending=0, completed=0, failed=0), + top_species=[], + under_represented=[], + ) + + +@router.post("/refresh") +def refresh_stats_now(db: Session = Depends(get_db)): + """Manually trigger a stats refresh.""" + from app.workers.stats_tasks import refresh_stats + refresh_stats.delay() + return {"status": "refresh_queued"} + + +@router.get("/sources") +def get_source_stats(db: Session = Depends(get_db)): + """Get per-source breakdown.""" + stats = db.query( + Image.source, + func.count(Image.id).label("total"), + func.sum(case((Image.status == "downloaded", 1), else_=0)).label("downloaded"), + func.sum(case((Image.status == "pending", 1), else_=0)).label("pending"), + func.sum(case((Image.status == "rejected", 1), else_=0)).label("rejected"), + ).group_by(Image.source).all() + + return [ + { + "source": s.source, + "total": s.total, + "downloaded": s.downloaded or 0, + "pending": s.pending or 0, + "rejected": s.rejected or 0, + } + for s in stats + ] + + +@router.get("/species") +def get_species_stats( + min_count: int = 0, + max_count: int = None, + db: Session = Depends(get_db), +): + """Get per-species image counts.""" + query = db.query( + Species.id, + Species.scientific_name, + Species.common_name, + Species.genus, + func.count(Image.id).label("image_count") + ).outerjoin(Image, (Image.species_id == Species.id) & (Image.status == "downloaded") + ).group_by(Species.id) + + if min_count > 0: + query = query.having(func.count(Image.id) >= min_count) + + if max_count is not None: + query = query.having(func.count(Image.id) <= max_count) + + stats = query.order_by(func.count(Image.id).desc()).all() + + return [ + { + "id": s.id, + "scientific_name": s.scientific_name, + "common_name": s.common_name, + "genus": s.genus, + "image_count": s.image_count, + } + for s in stats + ] + + +@router.get("/distribution") +def get_image_distribution(db: Session = Depends(get_db)): + """Get distribution of images per species for ML training assessment. + + Returns counts of species at various image thresholds to help + determine dataset quality for training image classifiers. + """ + from sqlalchemy import text + + # Get image counts per species using optimized raw SQL + distribution_sql = text(""" + WITH species_counts AS ( + SELECT + s.id, + COUNT(i.id) as cnt + FROM species s + LEFT JOIN images i ON i.species_id = s.id AND i.status = 'downloaded' + GROUP BY s.id + ) + SELECT + COUNT(*) as total_species, + SUM(CASE WHEN cnt = 0 THEN 1 ELSE 0 END) as with_0, + SUM(CASE WHEN cnt >= 1 AND cnt < 10 THEN 1 ELSE 0 END) as with_1_9, + SUM(CASE WHEN cnt >= 10 AND cnt < 25 THEN 1 ELSE 0 END) as with_10_24, + SUM(CASE WHEN cnt >= 25 AND cnt < 50 THEN 1 ELSE 0 END) as with_25_49, + SUM(CASE WHEN cnt >= 50 AND cnt < 100 THEN 1 ELSE 0 END) as with_50_99, + SUM(CASE WHEN cnt >= 100 AND cnt < 200 THEN 1 ELSE 0 END) as with_100_199, + SUM(CASE WHEN cnt >= 200 THEN 1 ELSE 0 END) as with_200_plus, + SUM(CASE WHEN cnt >= 10 THEN 1 ELSE 0 END) as trainable_10, + SUM(CASE WHEN cnt >= 25 THEN 1 ELSE 0 END) as trainable_25, + SUM(CASE WHEN cnt >= 50 THEN 1 ELSE 0 END) as trainable_50, + SUM(CASE WHEN cnt >= 100 THEN 1 ELSE 0 END) as trainable_100, + AVG(cnt) as avg_images, + MAX(cnt) as max_images, + MIN(cnt) as min_images, + SUM(cnt) as total_images + FROM species_counts + """) + + result = db.execute(distribution_sql).fetchone() + + return { + "total_species": result[0] or 0, + "distribution": { + "0_images": result[1] or 0, + "1_to_9": result[2] or 0, + "10_to_24": result[3] or 0, + "25_to_49": result[4] or 0, + "50_to_99": result[5] or 0, + "100_to_199": result[6] or 0, + "200_plus": result[7] or 0, + }, + "trainable_species": { + "min_10_images": result[8] or 0, + "min_25_images": result[9] or 0, + "min_50_images": result[10] or 0, + "min_100_images": result[11] or 0, + }, + "summary": { + "avg_images_per_species": round(result[12] or 0, 1), + "max_images": result[13] or 0, + "min_images": result[14] or 0, + "total_downloaded_images": result[15] or 0, + }, + "recommendations": { + "for_basic_model": f"{result[8] or 0} species with 10+ images", + "for_good_model": f"{result[10] or 0} species with 50+ images", + "for_excellent_model": f"{result[11] or 0} species with 100+ images", + } + } diff --git a/backend/app/config.py b/backend/app/config.py new file mode 100644 index 0000000..b8f82eb --- /dev/null +++ b/backend/app/config.py @@ -0,0 +1,38 @@ +from pydantic_settings import BaseSettings +from functools import lru_cache + + +class Settings(BaseSettings): + # Database + database_url: str = "sqlite:////data/db/plants.sqlite" + + # Redis + redis_url: str = "redis://redis:6379/0" + + # Storage paths + images_path: str = "/data/images" + exports_path: str = "/data/exports" + imports_path: str = "/data/imports" + logs_path: str = "/data/logs" + + # API Keys + flickr_api_key: str = "" + flickr_api_secret: str = "" + inaturalist_app_id: str = "" + inaturalist_app_secret: str = "" + trefle_api_key: str = "" + + # Logging + log_level: str = "INFO" + + # Celery + celery_concurrency: int = 4 + + class Config: + env_file = ".env" + extra = "ignore" + + +@lru_cache() +def get_settings() -> Settings: + return Settings() diff --git a/backend/app/database.py b/backend/app/database.py new file mode 100644 index 0000000..194be63 --- /dev/null +++ b/backend/app/database.py @@ -0,0 +1,44 @@ +from sqlalchemy import create_engine, event +from sqlalchemy.orm import sessionmaker, declarative_base +from sqlalchemy.pool import StaticPool + +from app.config import get_settings + +settings = get_settings() + +# SQLite-specific configuration +connect_args = {"check_same_thread": False} + +engine = create_engine( + settings.database_url, + connect_args=connect_args, + poolclass=StaticPool, + echo=False, +) + +# Enable WAL mode for better concurrent access +@event.listens_for(engine, "connect") +def set_sqlite_pragma(dbapi_connection, connection_record): + cursor = dbapi_connection.cursor() + cursor.execute("PRAGMA journal_mode=WAL") + cursor.execute("PRAGMA synchronous=NORMAL") + cursor.execute("PRAGMA foreign_keys=ON") + cursor.close() + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() + + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + + +def init_db(): + """Create all tables.""" + from app.models import species, image, job, api_key, export, cached_stats # noqa + Base.metadata.create_all(bind=engine) diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..f1d1a09 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,95 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from app.config import get_settings +from app.database import init_db +from app.api import species, images, jobs, exports, stats, sources + +settings = get_settings() + +app = FastAPI( + title="PlantGuideScraper API", + description="Web scraper interface for houseplant image collection", + version="1.0.0", +) + +# CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Include routers +app.include_router(species.router, prefix="/api/species", tags=["Species"]) +app.include_router(images.router, prefix="/api/images", tags=["Images"]) +app.include_router(jobs.router, prefix="/api/jobs", tags=["Jobs"]) +app.include_router(exports.router, prefix="/api/exports", tags=["Exports"]) +app.include_router(stats.router, prefix="/api/stats", tags=["Stats"]) +app.include_router(sources.router, prefix="/api/sources", tags=["Sources"]) + + +@app.on_event("startup") +async def startup_event(): + """Initialize database on startup.""" + init_db() + + +@app.get("/health") +async def health_check(): + """Health check endpoint.""" + return {"status": "healthy", "service": "plant-scraper"} + + +@app.get("/api/debug") +async def debug_check(): + """Debug endpoint - checks database connection.""" + import time + from app.database import SessionLocal + from app.models import Species, Image + + results = {"status": "checking", "checks": {}} + + # Check 1: Can we create a session? + try: + start = time.time() + db = SessionLocal() + results["checks"]["session_create"] = {"ok": True, "ms": int((time.time() - start) * 1000)} + except Exception as e: + results["checks"]["session_create"] = {"ok": False, "error": str(e)} + results["status"] = "error" + return results + + # Check 2: Simple query - count species + try: + start = time.time() + count = db.query(Species).count() + results["checks"]["species_count"] = {"ok": True, "count": count, "ms": int((time.time() - start) * 1000)} + except Exception as e: + results["checks"]["species_count"] = {"ok": False, "error": str(e)} + results["status"] = "error" + db.close() + return results + + # Check 3: Count images + try: + start = time.time() + count = db.query(Image).count() + results["checks"]["image_count"] = {"ok": True, "count": count, "ms": int((time.time() - start) * 1000)} + except Exception as e: + results["checks"]["image_count"] = {"ok": False, "error": str(e)} + results["status"] = "error" + db.close() + return results + + db.close() + results["status"] = "healthy" + return results + + +@app.get("/") +async def root(): + """Root endpoint.""" + return {"message": "PlantGuideScraper API", "docs": "/docs"} diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 0000000..4f233aa --- /dev/null +++ b/backend/app/models/__init__.py @@ -0,0 +1,8 @@ +from app.models.species import Species +from app.models.image import Image +from app.models.job import Job +from app.models.api_key import ApiKey +from app.models.export import Export +from app.models.cached_stats import CachedStats + +__all__ = ["Species", "Image", "Job", "ApiKey", "Export", "CachedStats"] diff --git a/backend/app/models/api_key.py b/backend/app/models/api_key.py new file mode 100644 index 0000000..cce2f1c --- /dev/null +++ b/backend/app/models/api_key.py @@ -0,0 +1,18 @@ +from sqlalchemy import Column, Integer, String, Float, Boolean + +from app.database import Base + + +class ApiKey(Base): + __tablename__ = "api_keys" + + id = Column(Integer, primary_key=True, index=True) + source = Column(String, unique=True, nullable=False) # 'flickr', 'inaturalist', 'wikimedia', 'trefle' + api_key = Column(String, nullable=False) # Also used as Client ID for OAuth sources + api_secret = Column(String, nullable=True) # Also used as Client Secret for OAuth sources + access_token = Column(String, nullable=True) # For OAuth sources like Wikimedia + rate_limit_per_sec = Column(Float, default=1.0) + enabled = Column(Boolean, default=True) + + def __repr__(self): + return f"" diff --git a/backend/app/models/cached_stats.py b/backend/app/models/cached_stats.py new file mode 100644 index 0000000..423f3d5 --- /dev/null +++ b/backend/app/models/cached_stats.py @@ -0,0 +1,14 @@ +from datetime import datetime +from sqlalchemy import Column, Integer, String, Text, DateTime + +from app.database import Base + + +class CachedStats(Base): + """Stores pre-calculated statistics updated by Celery beat.""" + __tablename__ = "cached_stats" + + id = Column(Integer, primary_key=True, index=True) + key = Column(String(50), unique=True, nullable=False, index=True) + value = Column(Text, nullable=False) # JSON-encoded stats + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) diff --git a/backend/app/models/export.py b/backend/app/models/export.py new file mode 100644 index 0000000..3312dae --- /dev/null +++ b/backend/app/models/export.py @@ -0,0 +1,24 @@ +from sqlalchemy import Column, Integer, String, Float, DateTime, Text, func + +from app.database import Base + + +class Export(Base): + __tablename__ = "exports" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String, nullable=False) + filter_criteria = Column(Text, nullable=True) # JSON: min_images, licenses, min_quality, species_ids + train_split = Column(Float, default=0.8) + status = Column(String, default="pending") # pending, generating, completed, failed + file_path = Column(String, nullable=True) + file_size = Column(Integer, nullable=True) + species_count = Column(Integer, nullable=True) + image_count = Column(Integer, nullable=True) + celery_task_id = Column(String, nullable=True) + created_at = Column(DateTime, server_default=func.now()) + completed_at = Column(DateTime, nullable=True) + error_message = Column(Text, nullable=True) + + def __repr__(self): + return f"" diff --git a/backend/app/models/image.py b/backend/app/models/image.py new file mode 100644 index 0000000..5e0dfbb --- /dev/null +++ b/backend/app/models/image.py @@ -0,0 +1,36 @@ +from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey, func, UniqueConstraint, Index +from sqlalchemy.orm import relationship + +from app.database import Base + + +class Image(Base): + __tablename__ = "images" + + id = Column(Integer, primary_key=True, index=True) + species_id = Column(Integer, ForeignKey("species.id"), nullable=False, index=True) + source = Column(String, nullable=False, index=True) + source_id = Column(String, nullable=True) + url = Column(String, nullable=False) + local_path = Column(String, nullable=True) + license = Column(String, nullable=False, index=True) + attribution = Column(String, nullable=True) + width = Column(Integer, nullable=True) + height = Column(Integer, nullable=True) + phash = Column(String, nullable=True, index=True) + quality_score = Column(Float, nullable=True) + status = Column(String, default="pending", index=True) # pending, downloaded, rejected, deleted + created_at = Column(DateTime, server_default=func.now()) + + # Composite indexes for common query patterns + __table_args__ = ( + UniqueConstraint("source", "source_id", name="uq_source_source_id"), + Index("ix_images_species_status", "species_id", "status"), # For counting images per species by status + Index("ix_images_status_created", "status", "created_at"), # For listing images by status + ) + + # Relationships + species = relationship("Species", back_populates="images") + + def __repr__(self): + return f"" diff --git a/backend/app/models/job.py b/backend/app/models/job.py new file mode 100644 index 0000000..92ee8a1 --- /dev/null +++ b/backend/app/models/job.py @@ -0,0 +1,27 @@ +from sqlalchemy import Column, Integer, String, DateTime, Text, Boolean, func + +from app.database import Base + + +class Job(Base): + __tablename__ = "jobs" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String, nullable=False) + source = Column(String, nullable=False) + species_filter = Column(Text, nullable=True) # JSON array of species IDs or NULL for all + only_without_images = Column(Boolean, default=False) # If True, only scrape species with 0 images + max_images = Column(Integer, nullable=True) # If set, only scrape species with fewer than N images + status = Column(String, default="pending", index=True) # pending, running, paused, completed, failed + progress_current = Column(Integer, default=0) + progress_total = Column(Integer, default=0) + images_downloaded = Column(Integer, default=0) + images_rejected = Column(Integer, default=0) + celery_task_id = Column(String, nullable=True) + started_at = Column(DateTime, nullable=True) + completed_at = Column(DateTime, nullable=True) + error_message = Column(Text, nullable=True) + created_at = Column(DateTime, server_default=func.now()) + + def __repr__(self): + return f"" diff --git a/backend/app/models/species.py b/backend/app/models/species.py new file mode 100644 index 0000000..aeef2a0 --- /dev/null +++ b/backend/app/models/species.py @@ -0,0 +1,21 @@ +from sqlalchemy import Column, Integer, String, DateTime, func +from sqlalchemy.orm import relationship + +from app.database import Base + + +class Species(Base): + __tablename__ = "species" + + id = Column(Integer, primary_key=True, index=True) + scientific_name = Column(String, unique=True, nullable=False, index=True) + common_name = Column(String, nullable=True) + genus = Column(String, nullable=True, index=True) + family = Column(String, nullable=True) + created_at = Column(DateTime, server_default=func.now()) + + # Relationships + images = relationship("Image", back_populates="species", cascade="all, delete-orphan") + + def __repr__(self): + return f"" diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py new file mode 100644 index 0000000..67785a4 --- /dev/null +++ b/backend/app/schemas/__init__.py @@ -0,0 +1,15 @@ +from app.schemas.species import SpeciesCreate, SpeciesUpdate, SpeciesResponse, SpeciesListResponse +from app.schemas.image import ImageResponse, ImageListResponse, ImageFilter +from app.schemas.job import JobCreate, JobResponse, JobListResponse +from app.schemas.api_key import ApiKeyCreate, ApiKeyUpdate, ApiKeyResponse +from app.schemas.export import ExportCreate, ExportResponse, ExportListResponse +from app.schemas.stats import StatsResponse, SourceStats, SpeciesStats + +__all__ = [ + "SpeciesCreate", "SpeciesUpdate", "SpeciesResponse", "SpeciesListResponse", + "ImageResponse", "ImageListResponse", "ImageFilter", + "JobCreate", "JobResponse", "JobListResponse", + "ApiKeyCreate", "ApiKeyUpdate", "ApiKeyResponse", + "ExportCreate", "ExportResponse", "ExportListResponse", + "StatsResponse", "SourceStats", "SpeciesStats", +] diff --git a/backend/app/schemas/api_key.py b/backend/app/schemas/api_key.py new file mode 100644 index 0000000..92de9aa --- /dev/null +++ b/backend/app/schemas/api_key.py @@ -0,0 +1,36 @@ +from pydantic import BaseModel +from typing import Optional + + +class ApiKeyBase(BaseModel): + source: str + api_key: Optional[str] = None # Optional for no-auth sources, used as Client ID for OAuth + api_secret: Optional[str] = None # Also used as Client Secret for OAuth sources + access_token: Optional[str] = None # For OAuth sources like Wikimedia + rate_limit_per_sec: float = 1.0 + enabled: bool = True + + +class ApiKeyCreate(ApiKeyBase): + pass + + +class ApiKeyUpdate(BaseModel): + api_key: Optional[str] = None + api_secret: Optional[str] = None + access_token: Optional[str] = None + rate_limit_per_sec: Optional[float] = None + enabled: Optional[bool] = None + + +class ApiKeyResponse(BaseModel): + id: int + source: str + api_key_masked: str # Show only last 4 chars + has_secret: bool + has_access_token: bool + rate_limit_per_sec: float + enabled: bool + + class Config: + from_attributes = True diff --git a/backend/app/schemas/export.py b/backend/app/schemas/export.py new file mode 100644 index 0000000..c5af8e6 --- /dev/null +++ b/backend/app/schemas/export.py @@ -0,0 +1,45 @@ +from pydantic import BaseModel +from datetime import datetime +from typing import Optional, List + + +class ExportFilter(BaseModel): + min_images_per_species: int = 100 + licenses: Optional[List[str]] = None # None means all + min_quality: Optional[float] = None + species_ids: Optional[List[int]] = None # None means all + + +class ExportCreate(BaseModel): + name: str + filter_criteria: ExportFilter + train_split: float = 0.8 + + +class ExportResponse(BaseModel): + id: int + name: str + filter_criteria: Optional[str] = None + train_split: float + status: str + file_path: Optional[str] = None + file_size: Optional[int] = None + species_count: Optional[int] = None + image_count: Optional[int] = None + created_at: datetime + completed_at: Optional[datetime] = None + error_message: Optional[str] = None + + class Config: + from_attributes = True + + +class ExportListResponse(BaseModel): + items: List[ExportResponse] + total: int + + +class ExportPreview(BaseModel): + species_count: int + image_count: int + estimated_size_mb: float diff --git a/backend/app/schemas/image.py b/backend/app/schemas/image.py new file mode 100644 index 0000000..87f24a5 --- /dev/null +++ b/backend/app/schemas/image.py @@ -0,0 +1,47 @@ +from pydantic import BaseModel +from datetime import datetime +from typing import Optional, List + + +class ImageBase(BaseModel): + species_id: int + source: str + url: str + license: str + + +class ImageResponse(BaseModel): + id: int + species_id: int + species_name: Optional[str] = None + source: str + source_id: Optional[str] = None + url: str + local_path: Optional[str] = None + license: str + attribution: Optional[str] = None + width: Optional[int] = None + height: Optional[int] = None + quality_score: Optional[float] = None + status: str + created_at: datetime + + class Config: + from_attributes = True + + +class ImageListResponse(BaseModel): + items: List[ImageResponse] + total: int + page: int + page_size: int + pages: int + + +class ImageFilter(BaseModel): + species_id: Optional[int] = None + source: Optional[str] = None + license: Optional[str] = None + status: Optional[str] = None + min_quality: Optional[float] = None + search: Optional[str] = None diff --git a/backend/app/schemas/job.py b/backend/app/schemas/job.py new file mode 100644 index 0000000..2444275 --- /dev/null +++ b/backend/app/schemas/job.py @@ -0,0 +1,35 @@ +from pydantic import BaseModel +from datetime import datetime +from typing import Optional, List + + +class JobCreate(BaseModel): + name: str + source: str + species_ids: Optional[List[int]] = None # None means all species + only_without_images: bool = False # If True, only scrape species with 0 images + max_images: Optional[int] = None # If set, only scrape species with fewer than N images + + +class JobResponse(BaseModel): + id: int + name: str + source: str + species_filter: Optional[str] = None + status: str + progress_current: int + progress_total: int + images_downloaded: int + images_rejected: int + started_at: Optional[datetime] = None + completed_at: Optional[datetime] = None + error_message: Optional[str] = None + created_at: datetime + + class Config: + from_attributes = True + + +class JobListResponse(BaseModel): + items: List[JobResponse] + total: int diff --git a/backend/app/schemas/species.py b/backend/app/schemas/species.py new file mode 100644 index 0000000..578f805 --- /dev/null +++ b/backend/app/schemas/species.py @@ -0,0 +1,44 @@ +from pydantic import BaseModel +from datetime import datetime +from typing import Optional, List + + +class SpeciesBase(BaseModel): + scientific_name: str + common_name: Optional[str] = None + genus: Optional[str] = None + family: Optional[str] = None + + +class SpeciesCreate(SpeciesBase): + pass + + +class SpeciesUpdate(BaseModel): + scientific_name: Optional[str] = None + common_name: Optional[str] = None + genus: Optional[str] = None + family: Optional[str] = None + + +class SpeciesResponse(SpeciesBase): + id: int + created_at: datetime + image_count: int = 0 + + class Config: + from_attributes = True + + +class SpeciesListResponse(BaseModel): + items: List[SpeciesResponse] + total: int + page: int + page_size: int + pages: int + + +class SpeciesImportResponse(BaseModel): + imported: int + skipped: int + errors: List[str] diff --git a/backend/app/schemas/stats.py b/backend/app/schemas/stats.py new file mode 100644 index 0000000..9834870 --- /dev/null +++ b/backend/app/schemas/stats.py @@ -0,0 +1,43 @@ +from pydantic import BaseModel +from typing import List, Dict + + +class SourceStats(BaseModel): + source: str + image_count: int + downloaded: int + pending: int + rejected: int + + +class LicenseStats(BaseModel): + license: str + count: int + + +class SpeciesStats(BaseModel): + id: int + scientific_name: str + common_name: str | None + image_count: int + + +class JobStats(BaseModel): + running: int + pending: int + completed: int + failed: int + + +class StatsResponse(BaseModel): + total_species: int + total_images: int + images_downloaded: int + images_pending: int + images_rejected: int + disk_usage_mb: float + sources: List[SourceStats] + licenses: List[LicenseStats] + jobs: JobStats + top_species: List[SpeciesStats] + under_represented: List[SpeciesStats] # Species with < 100 images diff --git a/backend/app/scrapers/__init__.py b/backend/app/scrapers/__init__.py new file mode 100644 index 0000000..be94b97 --- /dev/null +++ b/backend/app/scrapers/__init__.py @@ -0,0 +1,41 @@ +from typing import Optional + +from app.scrapers.base import BaseScraper +from app.scrapers.inaturalist import INaturalistScraper +from app.scrapers.flickr import FlickrScraper +from app.scrapers.wikimedia import WikimediaScraper +from app.scrapers.trefle import TrefleScraper +from app.scrapers.gbif import GBIFScraper +from app.scrapers.duckduckgo import DuckDuckGoScraper +from app.scrapers.bing import BingScraper + + +def get_scraper(source: str) -> Optional[BaseScraper]: + """Get scraper instance for a source.""" + scrapers = { + "inaturalist": INaturalistScraper, + "flickr": FlickrScraper, + "wikimedia": WikimediaScraper, + "trefle": TrefleScraper, + "gbif": GBIFScraper, + "duckduckgo": DuckDuckGoScraper, + "bing": BingScraper, + } + + scraper_class = scrapers.get(source) + if scraper_class: + return scraper_class() + return None + + +__all__ = [ + "get_scraper", + "BaseScraper", + "INaturalistScraper", + "FlickrScraper", + "WikimediaScraper", + "TrefleScraper", + "GBIFScraper", + "DuckDuckGoScraper", + "BingScraper", +] diff --git a/backend/app/scrapers/base.py b/backend/app/scrapers/base.py new file mode 100644 index 0000000..856b2aa --- /dev/null +++ b/backend/app/scrapers/base.py @@ -0,0 +1,57 @@ +from abc import ABC, abstractmethod +from typing import Dict, Any, Optional +import logging + +from sqlalchemy.orm import Session + +from app.models import Species, ApiKey + + +class BaseScraper(ABC): + """Base class for all image scrapers.""" + + name: str = "base" + requires_api_key: bool = True + + @abstractmethod + def scrape_species( + self, + species: Species, + db: Session, + logger: Optional[logging.Logger] = None + ) -> Dict[str, int]: + """ + Scrape images for a species. + + Args: + species: The species to scrape images for + db: Database session + logger: Optional logger for debugging + + Returns: + Dict with 'downloaded' and 'rejected' counts + """ + pass + + @abstractmethod + def test_connection(self, api_key: ApiKey) -> str: + """ + Test API connection. + + Args: + api_key: The API key configuration + + Returns: + Success message + + Raises: + Exception if connection fails + """ + pass + + def get_api_key(self, db: Session) -> ApiKey: + """Get API key for this scraper.""" + return db.query(ApiKey).filter( + ApiKey.source == self.name, + ApiKey.enabled == True + ).first() diff --git a/backend/app/scrapers/bhl.py b/backend/app/scrapers/bhl.py new file mode 100644 index 0000000..c9de4e7 --- /dev/null +++ b/backend/app/scrapers/bhl.py @@ -0,0 +1,228 @@ +import time +import logging +from typing import Dict, Optional + +import httpx +from sqlalchemy.orm import Session + +from app.scrapers.base import BaseScraper +from app.models import Species, Image, ApiKey +from app.workers.quality_tasks import download_and_process_image + + +class BHLScraper(BaseScraper): + """Scraper for Biodiversity Heritage Library (BHL) images. + + BHL provides access to digitized biodiversity literature and illustrations. + Most content is public domain (pre-1927) or CC-licensed. + + Note: BHL images are primarily historical botanical illustrations, + which may differ from photographs but are valuable for training. + """ + + name = "bhl" + requires_api_key = True # BHL requires free API key + + BASE_URL = "https://www.biodiversitylibrary.org/api3" + + HEADERS = { + "User-Agent": "PlantGuideScraper/1.0 (Plant image collection for ML training)", + "Accept": "application/json", + } + + # BHL content is mostly public domain + ALLOWED_LICENSES = {"CC0", "CC-BY", "CC-BY-SA", "PD"} + + def scrape_species( + self, + species: Species, + db: Session, + logger: Optional[logging.Logger] = None + ) -> Dict[str, int]: + """Scrape images from BHL for a species.""" + api_key = self.get_api_key(db) + if not api_key: + return {"downloaded": 0, "rejected": 0, "error": "No API key configured"} + + rate_limit = api_key.rate_limit_per_sec if api_key else 0.5 + + downloaded = 0 + rejected = 0 + + def log(level: str, msg: str): + if logger: + getattr(logger, level)(msg) + + try: + # Disable SSL verification - some Docker environments lack proper CA certificates + with httpx.Client(timeout=30, headers=self.HEADERS, verify=False) as client: + # Search for name in BHL + search_response = client.get( + f"{self.BASE_URL}", + params={ + "op": "NameSearch", + "name": species.scientific_name, + "format": "json", + "apikey": api_key.api_key, + }, + ) + search_response.raise_for_status() + search_data = search_response.json() + + results = search_data.get("Result", []) + if not results: + log("info", f" Species not found in BHL: {species.scientific_name}") + return {"downloaded": 0, "rejected": 0} + + time.sleep(1.0 / rate_limit) + + # Get pages with illustrations for each name result + for name_result in results[:5]: # Limit to top 5 matches + name_bank_id = name_result.get("NameBankID") + if not name_bank_id: + continue + + # Get publications with this name + pub_response = client.get( + f"{self.BASE_URL}", + params={ + "op": "NameGetDetail", + "namebankid": name_bank_id, + "format": "json", + "apikey": api_key.api_key, + }, + ) + pub_response.raise_for_status() + pub_data = pub_response.json() + + time.sleep(1.0 / rate_limit) + + # Extract titles and get page images + for title in pub_data.get("Result", []): + title_id = title.get("TitleID") + if not title_id: + continue + + # Get pages for this title + pages_response = client.get( + f"{self.BASE_URL}", + params={ + "op": "GetPageMetadata", + "titleid": title_id, + "format": "json", + "apikey": api_key.api_key, + "ocr": "false", + "names": "false", + }, + ) + + if pages_response.status_code != 200: + continue + + pages_data = pages_response.json() + pages = pages_data.get("Result", []) + + time.sleep(1.0 / rate_limit) + + # Look for pages that are likely illustrations + for page in pages[:100]: # Limit pages per title + page_types = page.get("PageTypes", []) + + # Only get illustration/plate pages + is_illustration = any( + pt.get("PageTypeName", "").lower() in ["illustration", "plate", "figure", "map"] + for pt in page_types + ) if page_types else False + + if not is_illustration and page_types: + continue + + page_id = page.get("PageID") + if not page_id: + continue + + # Construct image URL + # BHL provides multiple image sizes + image_url = f"https://www.biodiversitylibrary.org/pageimage/{page_id}" + + # Check if already exists + source_id = str(page_id) + existing = db.query(Image).filter( + Image.source == self.name, + Image.source_id == source_id, + ).first() + + if existing: + continue + + # Determine license - BHL content is usually public domain + item_url = page.get("ItemUrl", "") + year = None + try: + # Try to extract year from ItemUrl or other fields + if "Year" in page: + year = int(page.get("Year", 0)) + except (ValueError, TypeError): + pass + + # Content before 1927 is public domain in US + if year and year < 1927: + license_code = "PD" + else: + license_code = "CC0" # BHL default for older works + + # Build attribution + title_name = title.get("ShortTitle", title.get("FullTitle", "Unknown")) + attribution = f"From '{title_name}' via Biodiversity Heritage Library ({license_code})" + + # Create image record + image = Image( + species_id=species.id, + source=self.name, + source_id=source_id, + url=image_url, + license=license_code, + attribution=attribution, + status="pending", + ) + db.add(image) + db.commit() + + # Queue for download + download_and_process_image.delay(image.id) + downloaded += 1 + + # Limit total per species + if downloaded >= 50: + break + + if downloaded >= 50: + break + + if downloaded >= 50: + break + + except httpx.HTTPStatusError as e: + log("error", f" HTTP error for {species.scientific_name}: {e.response.status_code}") + except Exception as e: + log("error", f" Error scraping BHL for {species.scientific_name}: {e}") + + return {"downloaded": downloaded, "rejected": rejected} + + def test_connection(self, api_key: ApiKey) -> str: + """Test BHL API connection.""" + with httpx.Client(timeout=10, headers=self.HEADERS, verify=False) as client: + response = client.get( + f"{self.BASE_URL}", + params={ + "op": "NameSearch", + "name": "Rosa", + "format": "json", + "apikey": api_key.api_key, + }, + ) + response.raise_for_status() + data = response.json() + + results = data.get("Result", []) + return f"BHL API connection successful ({len(results)} results for 'Rosa')" diff --git a/backend/app/scrapers/bing.py b/backend/app/scrapers/bing.py new file mode 100644 index 0000000..bff09ea --- /dev/null +++ b/backend/app/scrapers/bing.py @@ -0,0 +1,135 @@ +import hashlib +import time +import logging +from typing import Dict, Optional + +import httpx +from sqlalchemy.orm import Session + +from app.scrapers.base import BaseScraper +from app.models import Species, Image, ApiKey +from app.workers.quality_tasks import download_and_process_image + + +class BingScraper(BaseScraper): + """Scraper for Bing Image Search v7 API (Azure Cognitive Services).""" + + name = "bing" + requires_api_key = True + + BASE_URL = "https://api.bing.microsoft.com/v7.0/images/search" + + NEGATIVE_TERMS = "-herbarium -specimen -illustration -drawing -diagram -dried -pressed" + + LICENSE_MAP = { + "Public": "CC0", + "Share": "CC-BY-SA", + "ShareCommercially": "CC-BY", + "Modify": "CC-BY-SA", + "ModifyCommercially": "CC-BY", + } + + def _build_queries(self, species: Species) -> list[str]: + queries = [f'"{species.scientific_name}" plant photo {self.NEGATIVE_TERMS}'] + if species.common_name: + queries.append(f'"{species.common_name}" houseplant photo {self.NEGATIVE_TERMS}') + return queries + + def scrape_species( + self, + species: Species, + db: Session, + logger: Optional[logging.Logger] = None, + ) -> Dict[str, int]: + api_key = self.get_api_key(db) + if not api_key: + return {"downloaded": 0, "rejected": 0} + + rate_limit = api_key.rate_limit_per_sec or 3.0 + downloaded = 0 + rejected = 0 + seen_urls = set() + + headers = { + "Ocp-Apim-Subscription-Key": api_key.api_key, + } + + try: + queries = self._build_queries(species) + + with httpx.Client(timeout=30, headers=headers) as client: + for query in queries: + params = { + "q": query, + "imageType": "Photo", + "license": "ShareCommercially", + "count": 50, + } + + response = client.get(self.BASE_URL, params=params) + response.raise_for_status() + data = response.json() + + for result in data.get("value", []): + url = result.get("contentUrl") + if not url or url in seen_urls: + continue + seen_urls.add(url) + + # Use Bing's imageId, fall back to md5 hash + source_id = result.get("imageId") or hashlib.md5(url.encode()).hexdigest()[:16] + + existing = db.query(Image).filter( + Image.source == self.name, + Image.source_id == source_id, + ).first() + + if existing: + continue + + # Map license + bing_license = result.get("license", "") + license_code = self.LICENSE_MAP.get(bing_license, "UNKNOWN") + + host = result.get("hostPageDisplayUrl", "") + attribution = f"via Bing ({host})" if host else "via Bing Image Search" + + image = Image( + species_id=species.id, + source=self.name, + source_id=source_id, + url=url, + width=result.get("width"), + height=result.get("height"), + license=license_code, + attribution=attribution, + status="pending", + ) + db.add(image) + db.commit() + + download_and_process_image.delay(image.id) + downloaded += 1 + + time.sleep(1.0 / rate_limit) + + except Exception as e: + if logger: + logger.error(f"Error scraping Bing for {species.scientific_name}: {e}") + else: + print(f"Error scraping Bing for {species.scientific_name}: {e}") + + return {"downloaded": downloaded, "rejected": rejected} + + def test_connection(self, api_key: ApiKey) -> str: + headers = {"Ocp-Apim-Subscription-Key": api_key.api_key} + with httpx.Client(timeout=10, headers=headers) as client: + response = client.get( + self.BASE_URL, + params={"q": "Monstera deliciosa plant", "count": 1}, + ) + response.raise_for_status() + data = response.json() + + count = data.get("totalEstimatedMatches", 0) + return f"Bing Image Search working ({count:,} estimated matches)" diff --git a/backend/app/scrapers/duckduckgo.py b/backend/app/scrapers/duckduckgo.py new file mode 100644 index 0000000..15eece7 --- /dev/null +++ b/backend/app/scrapers/duckduckgo.py @@ -0,0 +1,101 @@ +import hashlib +import time +import logging +from typing import Dict, Optional + +from duckduckgo_search import DDGS +from sqlalchemy.orm import Session + +from app.scrapers.base import BaseScraper +from app.models import Species, Image, ApiKey +from app.workers.quality_tasks import download_and_process_image + + +class DuckDuckGoScraper(BaseScraper): + """Scraper for DuckDuckGo image search. No API key required.""" + + name = "duckduckgo" + requires_api_key = False + + NEGATIVE_TERMS = "-herbarium -specimen -illustration -drawing -diagram -dried -pressed" + + def _build_queries(self, species: Species) -> list[str]: + queries = [f'"{species.scientific_name}" plant photo {self.NEGATIVE_TERMS}'] + if species.common_name: + queries.append(f'"{species.common_name}" houseplant photo {self.NEGATIVE_TERMS}') + return queries + + def scrape_species( + self, + species: Species, + db: Session, + logger: Optional[logging.Logger] = None, + ) -> Dict[str, int]: + api_key = self.get_api_key(db) + rate_limit = api_key.rate_limit_per_sec if api_key else 0.5 + + downloaded = 0 + rejected = 0 + seen_urls = set() + + try: + queries = self._build_queries(species) + + with DDGS() as ddgs: + for query in queries: + results = ddgs.images( + keywords=query, + type_image="photo", + max_results=50, + ) + + for result in results: + url = result.get("image") + if not url or url in seen_urls: + continue + seen_urls.add(url) + + source_id = hashlib.md5(url.encode()).hexdigest()[:16] + + # Check if already exists + existing = db.query(Image).filter( + Image.source == self.name, + Image.source_id == source_id, + ).first() + + if existing: + continue + + title = result.get("title", "") + attribution = f"{title} via DuckDuckGo" if title else "via DuckDuckGo" + + image = Image( + species_id=species.id, + source=self.name, + source_id=source_id, + url=url, + license="UNKNOWN", + attribution=attribution, + status="pending", + ) + db.add(image) + db.commit() + + download_and_process_image.delay(image.id) + downloaded += 1 + + time.sleep(1.0 / rate_limit) + + except Exception as e: + if logger: + logger.error(f"Error scraping DuckDuckGo for {species.scientific_name}: {e}") + else: + print(f"Error scraping DuckDuckGo for {species.scientific_name}: {e}") + + return {"downloaded": downloaded, "rejected": rejected} + + def test_connection(self, api_key: ApiKey) -> str: + with DDGS() as ddgs: + results = ddgs.images(keywords="Monstera deliciosa plant", max_results=1) + count = len(list(results)) + return f"DuckDuckGo search working ({count} test result)" diff --git a/backend/app/scrapers/eol.py b/backend/app/scrapers/eol.py new file mode 100644 index 0000000..b3f9be3 --- /dev/null +++ b/backend/app/scrapers/eol.py @@ -0,0 +1,226 @@ +import time +import logging +from typing import Dict, Optional + +import httpx +from sqlalchemy.orm import Session + +from app.scrapers.base import BaseScraper +from app.models import Species, Image, ApiKey +from app.workers.quality_tasks import download_and_process_image + + +class EOLScraper(BaseScraper): + """Scraper for Encyclopedia of Life (EOL) images. + + EOL aggregates biodiversity data from many sources and provides + a free API with no authentication required. + """ + + name = "eol" + requires_api_key = False + + BASE_URL = "https://eol.org/api" + + HEADERS = { + "User-Agent": "PlantGuideScraper/1.0 (Plant image collection for ML training)", + "Accept": "application/json", + } + + # Map EOL license URLs to short codes + LICENSE_MAP = { + "http://creativecommons.org/publicdomain/zero/1.0/": "CC0", + "http://creativecommons.org/publicdomain/mark/1.0/": "CC0", + "http://creativecommons.org/licenses/by/2.0/": "CC-BY", + "http://creativecommons.org/licenses/by/3.0/": "CC-BY", + "http://creativecommons.org/licenses/by/4.0/": "CC-BY", + "http://creativecommons.org/licenses/by-sa/2.0/": "CC-BY-SA", + "http://creativecommons.org/licenses/by-sa/3.0/": "CC-BY-SA", + "http://creativecommons.org/licenses/by-sa/4.0/": "CC-BY-SA", + "https://creativecommons.org/publicdomain/zero/1.0/": "CC0", + "https://creativecommons.org/publicdomain/mark/1.0/": "CC0", + "https://creativecommons.org/licenses/by/2.0/": "CC-BY", + "https://creativecommons.org/licenses/by/3.0/": "CC-BY", + "https://creativecommons.org/licenses/by/4.0/": "CC-BY", + "https://creativecommons.org/licenses/by-sa/2.0/": "CC-BY-SA", + "https://creativecommons.org/licenses/by-sa/3.0/": "CC-BY-SA", + "https://creativecommons.org/licenses/by-sa/4.0/": "CC-BY-SA", + "pd": "CC0", # Public domain + "public domain": "CC0", + } + + # Commercial-safe licenses + ALLOWED_LICENSES = {"CC0", "CC-BY", "CC-BY-SA"} + + def scrape_species( + self, + species: Species, + db: Session, + logger: Optional[logging.Logger] = None + ) -> Dict[str, int]: + """Scrape images from EOL for a species.""" + api_key = self.get_api_key(db) + rate_limit = api_key.rate_limit_per_sec if api_key else 0.5 + + downloaded = 0 + rejected = 0 + + def log(level: str, msg: str): + if logger: + getattr(logger, level)(msg) + + try: + # Disable SSL verification - EOL is a trusted source and some Docker + # environments lack proper CA certificates + with httpx.Client(timeout=30, headers=self.HEADERS, verify=False) as client: + # Step 1: Search for the species + search_response = client.get( + f"{self.BASE_URL}/search/1.0.json", + params={ + "q": species.scientific_name, + "page": 1, + "exact": "true", + }, + ) + search_response.raise_for_status() + search_data = search_response.json() + + results = search_data.get("results", []) + if not results: + log("info", f" Species not found in EOL: {species.scientific_name}") + return {"downloaded": 0, "rejected": 0} + + # Get the EOL page ID + eol_page_id = results[0].get("id") + if not eol_page_id: + return {"downloaded": 0, "rejected": 0} + + time.sleep(1.0 / rate_limit) + + # Step 2: Get page details with images + page_response = client.get( + f"{self.BASE_URL}/pages/1.0/{eol_page_id}.json", + params={ + "images_per_page": 75, + "images_page": 1, + "videos_per_page": 0, + "sounds_per_page": 0, + "maps_per_page": 0, + "texts_per_page": 0, + "details": "true", + "licenses": "cc-by|cc-by-sa|pd|cc-by-nc", + }, + ) + page_response.raise_for_status() + page_data = page_response.json() + + data_objects = page_data.get("dataObjects", []) + log("debug", f" Found {len(data_objects)} media objects") + + for obj in data_objects: + # Only process images + media_type = obj.get("dataType", "") + if "image" not in media_type.lower() and "stillimage" not in media_type.lower(): + continue + + # Get image URL + image_url = obj.get("eolMediaURL") or obj.get("mediaURL") + if not image_url: + rejected += 1 + continue + + # Check license + license_url = obj.get("license", "").lower() + license_code = None + + # Try to match license URL + for pattern, code in self.LICENSE_MAP.items(): + if pattern in license_url: + license_code = code + break + + if not license_code: + # Check for NC licenses which we reject + if "-nc" in license_url: + rejected += 1 + continue + # Unknown license, skip + log("debug", f" Rejected: unknown license {license_url}") + rejected += 1 + continue + + if license_code not in self.ALLOWED_LICENSES: + rejected += 1 + continue + + # Create unique source ID + source_id = str(obj.get("dataObjectVersionID") or obj.get("identifier") or hash(image_url)) + + # Check if already exists + existing = db.query(Image).filter( + Image.source == self.name, + Image.source_id == source_id, + ).first() + + if existing: + continue + + # Build attribution + agents = obj.get("agents", []) + photographer = None + rights_holder = None + + for agent in agents: + role = agent.get("role", "").lower() + name = agent.get("full_name", "") + if role == "photographer": + photographer = name + elif role == "owner" or role == "rights holder": + rights_holder = name + + attribution_parts = [] + if photographer: + attribution_parts.append(f"Photo by {photographer}") + if rights_holder and rights_holder != photographer: + attribution_parts.append(f"Rights: {rights_holder}") + attribution_parts.append(f"via EOL ({license_code})") + attribution = " | ".join(attribution_parts) + + # Create image record + image = Image( + species_id=species.id, + source=self.name, + source_id=source_id, + url=image_url, + license=license_code, + attribution=attribution, + status="pending", + ) + db.add(image) + db.commit() + + # Queue for download + download_and_process_image.delay(image.id) + downloaded += 1 + + time.sleep(1.0 / rate_limit) + + except httpx.HTTPStatusError as e: + log("error", f" HTTP error for {species.scientific_name}: {e.response.status_code}") + except Exception as e: + log("error", f" Error scraping EOL for {species.scientific_name}: {e}") + + return {"downloaded": downloaded, "rejected": rejected} + + def test_connection(self, api_key: ApiKey) -> str: + """Test EOL API connection.""" + with httpx.Client(timeout=10, headers=self.HEADERS, verify=False) as client: + response = client.get( + f"{self.BASE_URL}/search/1.0.json", + params={"q": "Rosa", "page": 1}, + ) + response.raise_for_status() + data = response.json() + + total = data.get("totalResults", 0) + return f"EOL API connection successful ({total} results for 'Rosa')" diff --git a/backend/app/scrapers/flickr.py b/backend/app/scrapers/flickr.py new file mode 100644 index 0000000..2ead9b4 --- /dev/null +++ b/backend/app/scrapers/flickr.py @@ -0,0 +1,146 @@ +import time +import logging +from typing import Dict, Optional + +import httpx +from sqlalchemy.orm import Session + +from app.scrapers.base import BaseScraper +from app.models import Species, Image, ApiKey +from app.workers.quality_tasks import download_and_process_image + + +class FlickrScraper(BaseScraper): + """Scraper for Flickr images via their API.""" + + name = "flickr" + requires_api_key = True + + BASE_URL = "https://api.flickr.com/services/rest/" + + HEADERS = { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15" + } + + # Commercial-safe license IDs + # 4 = CC BY 2.0, 7 = No known copyright, 8 = US Gov, 9 = CC0 + ALLOWED_LICENSES = "4,7,8,9" + + LICENSE_MAP = { + "4": "CC-BY", + "7": "NO-KNOWN-COPYRIGHT", + "8": "US-GOV", + "9": "CC0", + } + + def scrape_species( + self, + species: Species, + db: Session, + logger: Optional[logging.Logger] = None + ) -> Dict[str, int]: + """Scrape images from Flickr for a species.""" + api_key = self.get_api_key(db) + if not api_key: + return {"downloaded": 0, "rejected": 0, "error": "No API key configured"} + + rate_limit = api_key.rate_limit_per_sec + + downloaded = 0 + rejected = 0 + + try: + params = { + "method": "flickr.photos.search", + "api_key": api_key.api_key, + "text": species.scientific_name, + "license": self.ALLOWED_LICENSES, + "content_type": 1, # Photos only + "media": "photos", + "extras": "license,url_l,url_o,owner_name", + "per_page": 100, + "format": "json", + "nojsoncallback": 1, + } + + with httpx.Client(timeout=30, headers=self.HEADERS) as client: + response = client.get(self.BASE_URL, params=params) + response.raise_for_status() + data = response.json() + + if data.get("stat") != "ok": + return {"downloaded": 0, "rejected": 0, "error": data.get("message")} + + photos = data.get("photos", {}).get("photo", []) + + for photo in photos: + # Get best URL (original or large) + url = photo.get("url_o") or photo.get("url_l") + if not url: + rejected += 1 + continue + + # Get license + license_id = str(photo.get("license", "")) + license_code = self.LICENSE_MAP.get(license_id, "UNKNOWN") + if license_code == "UNKNOWN": + rejected += 1 + continue + + # Check if already exists + source_id = str(photo.get("id")) + existing = db.query(Image).filter( + Image.source == self.name, + Image.source_id == source_id, + ).first() + + if existing: + continue + + # Build attribution + owner = photo.get("ownername", "Unknown") + attribution = f"Photo by {owner} on Flickr ({license_code})" + + # Create image record + image = Image( + species_id=species.id, + source=self.name, + source_id=source_id, + url=url, + license=license_code, + attribution=attribution, + status="pending", + ) + db.add(image) + db.commit() + + # Queue for download + download_and_process_image.delay(image.id) + downloaded += 1 + + # Rate limiting + time.sleep(1.0 / rate_limit) + + except Exception as e: + print(f"Error scraping Flickr for {species.scientific_name}: {e}") + + return {"downloaded": downloaded, "rejected": rejected} + + def test_connection(self, api_key: ApiKey) -> str: + """Test Flickr API connection.""" + params = { + "method": "flickr.test.echo", + "api_key": api_key.api_key, + "format": "json", + "nojsoncallback": 1, + } + + with httpx.Client(timeout=10, headers=self.HEADERS) as client: + response = client.get(self.BASE_URL, params=params) + response.raise_for_status() + data = response.json() + + if data.get("stat") != "ok": + raise Exception(data.get("message", "API test failed")) + + return "Flickr API connection successful" diff --git a/backend/app/scrapers/gbif.py b/backend/app/scrapers/gbif.py new file mode 100644 index 0000000..acd10d2 --- /dev/null +++ b/backend/app/scrapers/gbif.py @@ -0,0 +1,159 @@ +import time +import logging +from typing import Dict, Optional + +import httpx +from sqlalchemy.orm import Session + +from app.scrapers.base import BaseScraper +from app.models import Species, Image, ApiKey +from app.workers.quality_tasks import download_and_process_image + + +class GBIFScraper(BaseScraper): + """Scraper for GBIF (Global Biodiversity Information Facility) images.""" + + name = "gbif" + requires_api_key = False # GBIF is free to use + + BASE_URL = "https://api.gbif.org/v1" + + HEADERS = { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15" + } + + # Map GBIF license URLs to short codes + LICENSE_MAP = { + "http://creativecommons.org/publicdomain/zero/1.0/legalcode": "CC0", + "http://creativecommons.org/licenses/by/4.0/legalcode": "CC-BY", + "http://creativecommons.org/licenses/by-nc/4.0/legalcode": "CC-BY-NC", + "http://creativecommons.org/publicdomain/zero/1.0/": "CC0", + "http://creativecommons.org/licenses/by/4.0/": "CC-BY", + "http://creativecommons.org/licenses/by-nc/4.0/": "CC-BY-NC", + "https://creativecommons.org/publicdomain/zero/1.0/legalcode": "CC0", + "https://creativecommons.org/licenses/by/4.0/legalcode": "CC-BY", + "https://creativecommons.org/licenses/by-nc/4.0/legalcode": "CC-BY-NC", + "https://creativecommons.org/publicdomain/zero/1.0/": "CC0", + "https://creativecommons.org/licenses/by/4.0/": "CC-BY", + "https://creativecommons.org/licenses/by-nc/4.0/": "CC-BY-NC", + } + + # Only allow commercial-safe licenses + ALLOWED_LICENSES = {"CC0", "CC-BY"} + + def scrape_species( + self, + species: Species, + db: Session, + logger: Optional[logging.Logger] = None + ) -> Dict[str, int]: + """Scrape images from GBIF for a species.""" + # GBIF doesn't require API key, but we still respect rate limits + api_key = self.get_api_key(db) + rate_limit = api_key.rate_limit_per_sec if api_key else 1.0 + + downloaded = 0 + rejected = 0 + + try: + params = { + "scientificName": species.scientific_name, + "mediaType": "StillImage", + "limit": 100, + } + + with httpx.Client(timeout=30, headers=self.HEADERS) as client: + response = client.get( + f"{self.BASE_URL}/occurrence/search", + params=params, + ) + response.raise_for_status() + data = response.json() + + results = data.get("results", []) + + for occurrence in results: + media_list = occurrence.get("media", []) + + for media in media_list: + # Only process still images + if media.get("type") != "StillImage": + continue + + url = media.get("identifier") + if not url: + rejected += 1 + continue + + # Check license + license_url = media.get("license", "") + license_code = self.LICENSE_MAP.get(license_url) + + if not license_code or license_code not in self.ALLOWED_LICENSES: + rejected += 1 + continue + + # Create unique source ID from occurrence key and media URL + occurrence_key = occurrence.get("key", "") + # Use hash of URL for uniqueness within occurrence + url_hash = str(hash(url))[-8:] + source_id = f"{occurrence_key}_{url_hash}" + + # Check if already exists + existing = db.query(Image).filter( + Image.source == self.name, + Image.source_id == source_id, + ).first() + + if existing: + continue + + # Build attribution + creator = media.get("creator", "") + rights_holder = media.get("rightsHolder", "") + attribution_parts = [] + if creator: + attribution_parts.append(f"Photo by {creator}") + if rights_holder and rights_holder != creator: + attribution_parts.append(f"Rights: {rights_holder}") + attribution_parts.append(f"via GBIF ({license_code})") + attribution = " | ".join(attribution_parts) if attribution_parts else f"GBIF ({license_code})" + + # Create image record + image = Image( + species_id=species.id, + source=self.name, + source_id=source_id, + url=url, + license=license_code, + attribution=attribution, + status="pending", + ) + db.add(image) + db.commit() + + # Queue for download + download_and_process_image.delay(image.id) + downloaded += 1 + + # Rate limiting + time.sleep(1.0 / rate_limit) + + except Exception as e: + print(f"Error scraping GBIF for {species.scientific_name}: {e}") + + return {"downloaded": downloaded, "rejected": rejected} + + def test_connection(self, api_key: ApiKey) -> str: + """Test GBIF API connection.""" + # GBIF doesn't require authentication, just test the endpoint + with httpx.Client(timeout=10, headers=self.HEADERS) as client: + response = client.get( + f"{self.BASE_URL}/occurrence/search", + params={"limit": 1}, + ) + response.raise_for_status() + data = response.json() + + count = data.get("count", 0) + return f"GBIF API connection successful ({count:,} total occurrences available)" diff --git a/backend/app/scrapers/inaturalist.py b/backend/app/scrapers/inaturalist.py new file mode 100644 index 0000000..0f73986 --- /dev/null +++ b/backend/app/scrapers/inaturalist.py @@ -0,0 +1,144 @@ +import time +import logging +from typing import Dict, Optional + +import httpx +from sqlalchemy.orm import Session + +from app.scrapers.base import BaseScraper +from app.models import Species, Image, ApiKey +from app.workers.quality_tasks import download_and_process_image + + +class INaturalistScraper(BaseScraper): + """Scraper for iNaturalist observations via their API.""" + + name = "inaturalist" + requires_api_key = False # Public API, but rate limited + + BASE_URL = "https://api.inaturalist.org/v1" + + HEADERS = { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15" + } + + # Commercial-safe licenses (CC0, CC-BY) + ALLOWED_LICENSES = ["cc0", "cc-by"] + + def scrape_species( + self, + species: Species, + db: Session, + logger: Optional[logging.Logger] = None + ) -> Dict[str, int]: + """Scrape images from iNaturalist for a species.""" + api_key = self.get_api_key(db) + rate_limit = api_key.rate_limit_per_sec if api_key else 1.0 + + downloaded = 0 + rejected = 0 + + def log(level: str, msg: str): + if logger: + getattr(logger, level)(msg) + + try: + # Search for observations of this species + params = { + "taxon_name": species.scientific_name, + "quality_grade": "research", # Only research-grade + "photos": True, + "per_page": 200, + "order_by": "votes", + "license": ",".join(self.ALLOWED_LICENSES), + } + + log("debug", f" API request params: {params}") + + with httpx.Client(timeout=30, headers=self.HEADERS) as client: + response = client.get( + f"{self.BASE_URL}/observations", + params=params, + ) + log("debug", f" API response status: {response.status_code}") + response.raise_for_status() + data = response.json() + + observations = data.get("results", []) + total_results = data.get("total_results", 0) + log("debug", f" Found {len(observations)} observations (total: {total_results})") + + if not observations: + log("info", f" No observations found for {species.scientific_name}") + return {"downloaded": 0, "rejected": 0} + + for obs in observations: + photos = obs.get("photos", []) + for photo in photos: + # Check license + license_code = photo.get("license_code", "").lower() if photo.get("license_code") else "" + if license_code not in self.ALLOWED_LICENSES: + log("debug", f" Rejected photo {photo.get('id')}: license={license_code}") + rejected += 1 + continue + + # Get image URL (medium size for initial download) + url = photo.get("url", "") + if not url: + log("debug", f" Skipped photo {photo.get('id')}: no URL") + continue + + # Convert to larger size + url = url.replace("square", "large") + + # Check if already exists + source_id = str(photo.get("id")) + existing = db.query(Image).filter( + Image.source == self.name, + Image.source_id == source_id, + ).first() + + if existing: + log("debug", f" Skipped photo {source_id}: already exists") + continue + + # Create image record + image = Image( + species_id=species.id, + source=self.name, + source_id=source_id, + url=url, + license=license_code.upper(), + attribution=photo.get("attribution", ""), + status="pending", + ) + db.add(image) + db.commit() + + # Queue for download + download_and_process_image.delay(image.id) + downloaded += 1 + log("debug", f" Queued photo {source_id} for download") + + # Rate limiting + time.sleep(1.0 / rate_limit) + + except httpx.HTTPStatusError as e: + log("error", f" HTTP error for {species.scientific_name}: {e.response.status_code} - {e.response.text}") + except httpx.RequestError as e: + log("error", f" Request error for {species.scientific_name}: {e}") + except Exception as e: + log("error", f" Error scraping iNaturalist for {species.scientific_name}: {e}") + + return {"downloaded": downloaded, "rejected": rejected} + + def test_connection(self, api_key: ApiKey) -> str: + """Test iNaturalist API connection.""" + with httpx.Client(timeout=10, headers=self.HEADERS) as client: + response = client.get( + f"{self.BASE_URL}/observations", + params={"per_page": 1}, + ) + response.raise_for_status() + + return "iNaturalist API connection successful" diff --git a/backend/app/scrapers/trefle.py b/backend/app/scrapers/trefle.py new file mode 100644 index 0000000..604a56b --- /dev/null +++ b/backend/app/scrapers/trefle.py @@ -0,0 +1,154 @@ +import time +import logging +from typing import Dict, Optional + +import httpx +from sqlalchemy.orm import Session + +from app.scrapers.base import BaseScraper +from app.models import Species, Image, ApiKey +from app.workers.quality_tasks import download_and_process_image + + +class TrefleScraper(BaseScraper): + """Scraper for Trefle.io plant database.""" + + name = "trefle" + requires_api_key = True + + BASE_URL = "https://trefle.io/api/v1" + + HEADERS = { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15" + } + + def scrape_species( + self, + species: Species, + db: Session, + logger: Optional[logging.Logger] = None + ) -> Dict[str, int]: + """Scrape images from Trefle for a species.""" + api_key = self.get_api_key(db) + if not api_key: + return {"downloaded": 0, "rejected": 0, "error": "No API key configured"} + + rate_limit = api_key.rate_limit_per_sec + + downloaded = 0 + rejected = 0 + + try: + # Search for the species + params = { + "token": api_key.api_key, + "q": species.scientific_name, + } + + with httpx.Client(timeout=30, headers=self.HEADERS) as client: + response = client.get( + f"{self.BASE_URL}/plants/search", + params=params, + ) + response.raise_for_status() + data = response.json() + + plants = data.get("data", []) + + for plant in plants: + # Get plant details for more images + plant_id = plant.get("id") + if not plant_id: + continue + + detail_response = client.get( + f"{self.BASE_URL}/plants/{plant_id}", + params={"token": api_key.api_key}, + ) + + if detail_response.status_code != 200: + continue + + plant_detail = detail_response.json().get("data", {}) + + # Get main image + main_image = plant_detail.get("image_url") + if main_image: + source_id = f"main_{plant_id}" + existing = db.query(Image).filter( + Image.source == self.name, + Image.source_id == source_id, + ).first() + + if not existing: + image = Image( + species_id=species.id, + source=self.name, + source_id=source_id, + url=main_image, + license="TREFLE", # Trefle's own license + attribution="Trefle.io Plant Database", + status="pending", + ) + db.add(image) + db.commit() + download_and_process_image.delay(image.id) + downloaded += 1 + + # Get additional images from species detail + images = plant_detail.get("images", {}) + for image_type, image_list in images.items(): + if not isinstance(image_list, list): + continue + + for img in image_list: + url = img.get("image_url") + if not url: + continue + + img_id = img.get("id", url.split("/")[-1]) + source_id = f"{image_type}_{img_id}" + + existing = db.query(Image).filter( + Image.source == self.name, + Image.source_id == source_id, + ).first() + + if existing: + continue + + copyright_info = img.get("copyright", "") + image = Image( + species_id=species.id, + source=self.name, + source_id=source_id, + url=url, + license="TREFLE", + attribution=copyright_info or "Trefle.io", + status="pending", + ) + db.add(image) + db.commit() + download_and_process_image.delay(image.id) + downloaded += 1 + + # Rate limiting + time.sleep(1.0 / rate_limit) + + except Exception as e: + print(f"Error scraping Trefle for {species.scientific_name}: {e}") + + return {"downloaded": downloaded, "rejected": rejected} + + def test_connection(self, api_key: ApiKey) -> str: + """Test Trefle API connection.""" + params = {"token": api_key.api_key} + + with httpx.Client(timeout=10, headers=self.HEADERS) as client: + response = client.get( + f"{self.BASE_URL}/plants", + params=params, + ) + response.raise_for_status() + + return "Trefle API connection successful" diff --git a/backend/app/scrapers/wikimedia.py b/backend/app/scrapers/wikimedia.py new file mode 100644 index 0000000..b733f11 --- /dev/null +++ b/backend/app/scrapers/wikimedia.py @@ -0,0 +1,146 @@ +import time +import logging +from typing import Dict, Optional + +import httpx +from sqlalchemy.orm import Session + +from app.scrapers.base import BaseScraper +from app.models import Species, Image, ApiKey +from app.workers.quality_tasks import download_and_process_image + + +class WikimediaScraper(BaseScraper): + """Scraper for Wikimedia Commons images.""" + + name = "wikimedia" + requires_api_key = False + + BASE_URL = "https://commons.wikimedia.org/w/api.php" + + HEADERS = { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15" + } + + def scrape_species( + self, + species: Species, + db: Session, + logger: Optional[logging.Logger] = None + ) -> Dict[str, int]: + """Scrape images from Wikimedia Commons for a species.""" + api_key = self.get_api_key(db) + rate_limit = api_key.rate_limit_per_sec if api_key else 1.0 + + downloaded = 0 + rejected = 0 + + try: + # Search for images in the species category + search_term = species.scientific_name + + params = { + "action": "query", + "format": "json", + "generator": "search", + "gsrsearch": f"filetype:bitmap {search_term}", + "gsrnamespace": 6, # File namespace + "gsrlimit": 50, + "prop": "imageinfo", + "iiprop": "url|extmetadata|size", + } + + with httpx.Client(timeout=30, headers=self.HEADERS) as client: + response = client.get(self.BASE_URL, params=params) + response.raise_for_status() + data = response.json() + + pages = data.get("query", {}).get("pages", {}) + + for page_id, page in pages.items(): + if int(page_id) < 0: + continue + + imageinfo = page.get("imageinfo", [{}])[0] + url = imageinfo.get("url", "") + if not url: + continue + + # Check size + width = imageinfo.get("width", 0) + height = imageinfo.get("height", 0) + if width < 256 or height < 256: + rejected += 1 + continue + + # Get license from metadata + metadata = imageinfo.get("extmetadata", {}) + license_info = metadata.get("LicenseShortName", {}).get("value", "") + + # Filter for commercial-safe licenses + license_upper = license_info.upper() + if "CC BY" in license_upper or "CC0" in license_upper or "PUBLIC DOMAIN" in license_upper: + license_code = license_info + else: + rejected += 1 + continue + + # Check if already exists + source_id = str(page_id) + existing = db.query(Image).filter( + Image.source == self.name, + Image.source_id == source_id, + ).first() + + if existing: + continue + + # Get attribution + artist = metadata.get("Artist", {}).get("value", "Unknown") + # Clean HTML from artist + if "<" in artist: + import re + artist = re.sub(r"<[^>]+>", "", artist).strip() + + attribution = f"{artist} via Wikimedia Commons ({license_code})" + + # Create image record + image = Image( + species_id=species.id, + source=self.name, + source_id=source_id, + url=url, + license=license_code, + attribution=attribution, + width=width, + height=height, + status="pending", + ) + db.add(image) + db.commit() + + # Queue for download + download_and_process_image.delay(image.id) + downloaded += 1 + + # Rate limiting + time.sleep(1.0 / rate_limit) + + except Exception as e: + print(f"Error scraping Wikimedia for {species.scientific_name}: {e}") + + return {"downloaded": downloaded, "rejected": rejected} + + def test_connection(self, api_key: ApiKey) -> str: + """Test Wikimedia API connection.""" + params = { + "action": "query", + "format": "json", + "meta": "siteinfo", + } + + with httpx.Client(timeout=10, headers=self.HEADERS) as client: + response = client.get(self.BASE_URL, params=params) + response.raise_for_status() + + return "Wikimedia Commons API connection successful" diff --git a/backend/app/utils/__init__.py b/backend/app/utils/__init__.py new file mode 100644 index 0000000..80f423e --- /dev/null +++ b/backend/app/utils/__init__.py @@ -0,0 +1 @@ +# Utility functions diff --git a/backend/app/utils/dedup.py b/backend/app/utils/dedup.py new file mode 100644 index 0000000..00bc2f8 --- /dev/null +++ b/backend/app/utils/dedup.py @@ -0,0 +1,80 @@ +"""Image deduplication utilities using perceptual hashing.""" + +from typing import Optional + +import imagehash +from PIL import Image as PILImage + + +def calculate_phash(image_path: str) -> Optional[str]: + """ + Calculate perceptual hash for an image. + + Args: + image_path: Path to image file + + Returns: + Hex string of perceptual hash, or None if failed + """ + try: + with PILImage.open(image_path) as img: + return str(imagehash.phash(img)) + except Exception: + return None + + +def calculate_dhash(image_path: str) -> Optional[str]: + """ + Calculate difference hash for an image. + Faster but less accurate than phash. + + Args: + image_path: Path to image file + + Returns: + Hex string of difference hash, or None if failed + """ + try: + with PILImage.open(image_path) as img: + return str(imagehash.dhash(img)) + except Exception: + return None + + +def hashes_are_similar(hash1: str, hash2: str, threshold: int = 10) -> bool: + """ + Check if two hashes are similar (potential duplicates). + + Args: + hash1: First hash string + hash2: Second hash string + threshold: Maximum Hamming distance (default 10) + + Returns: + True if hashes are similar + """ + try: + h1 = imagehash.hex_to_hash(hash1) + h2 = imagehash.hex_to_hash(hash2) + return (h1 - h2) <= threshold + except Exception: + return False + + +def hamming_distance(hash1: str, hash2: str) -> int: + """ + Calculate Hamming distance between two hashes. + + Args: + hash1: First hash string + hash2: Second hash string + + Returns: + Hamming distance (0 = identical, higher = more different) + """ + try: + h1 = imagehash.hex_to_hash(hash1) + h2 = imagehash.hex_to_hash(hash2) + return int(h1 - h2) + except Exception: + return 64 # Maximum distance diff --git a/backend/app/utils/image_quality.py b/backend/app/utils/image_quality.py new file mode 100644 index 0000000..5734386 --- /dev/null +++ b/backend/app/utils/image_quality.py @@ -0,0 +1,109 @@ +"""Image quality assessment utilities.""" + +import numpy as np +from PIL import Image as PILImage +from scipy import ndimage + + +def calculate_blur_score(image_path: str) -> float: + """ + Calculate blur score using Laplacian variance. + Higher score = sharper image. + + Args: + image_path: Path to image file + + Returns: + Variance of Laplacian (higher = sharper) + """ + try: + img = PILImage.open(image_path).convert("L") + img_array = np.array(img) + laplacian = ndimage.laplace(img_array) + return float(np.var(laplacian)) + except Exception: + return 0.0 + + +def is_too_blurry(image_path: str, threshold: float = 100.0) -> bool: + """ + Check if image is too blurry for training. + + Args: + image_path: Path to image file + threshold: Minimum acceptable blur score (default 100) + + Returns: + True if image is too blurry + """ + score = calculate_blur_score(image_path) + return score < threshold + + +def get_image_dimensions(image_path: str) -> tuple[int, int]: + """ + Get image dimensions. + + Args: + image_path: Path to image file + + Returns: + Tuple of (width, height) + """ + try: + with PILImage.open(image_path) as img: + return img.size + except Exception: + return (0, 0) + + +def is_too_small(image_path: str, min_size: int = 256) -> bool: + """ + Check if image is too small for training. + + Args: + image_path: Path to image file + min_size: Minimum dimension size (default 256) + + Returns: + True if image is too small + """ + width, height = get_image_dimensions(image_path) + return width < min_size or height < min_size + + +def resize_image( + image_path: str, + output_path: str = None, + max_size: int = 512, + quality: int = 95, +) -> bool: + """ + Resize image to max dimension while preserving aspect ratio. + + Args: + image_path: Path to input image + output_path: Path for output (defaults to overwriting input) + max_size: Maximum dimension size (default 512) + quality: JPEG quality (default 95) + + Returns: + True if successful + """ + try: + output_path = output_path or image_path + + with PILImage.open(image_path) as img: + # Only resize if larger than max_size + if max(img.size) > max_size: + img.thumbnail((max_size, max_size), PILImage.Resampling.LANCZOS) + + # Convert to RGB if necessary (for JPEG) + if img.mode in ("RGBA", "P"): + img = img.convert("RGB") + + img.save(output_path, "JPEG", quality=quality) + + return True + except Exception: + return False diff --git a/backend/app/utils/logging.py b/backend/app/utils/logging.py new file mode 100644 index 0000000..5019313 --- /dev/null +++ b/backend/app/utils/logging.py @@ -0,0 +1,92 @@ +import logging +import os +from datetime import datetime +from pathlib import Path + +from app.config import get_settings + +settings = get_settings() + + +def setup_logging(): + """Configure file and console logging.""" + logs_path = Path(settings.logs_path) + logs_path.mkdir(parents=True, exist_ok=True) + + # Create a dated log file + log_file = logs_path / f"scraper_{datetime.now().strftime('%Y-%m-%d')}.log" + + # Configure root logger + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(log_file), + logging.StreamHandler() + ] + ) + + return logging.getLogger("plant_scraper") + + +def get_logger(name: str = "plant_scraper"): + """Get a logger instance.""" + logs_path = Path(settings.logs_path) + logs_path.mkdir(parents=True, exist_ok=True) + + logger = logging.getLogger(name) + + if not logger.handlers: + logger.setLevel(logging.INFO) + + # File handler with daily rotation + log_file = logs_path / f"scraper_{datetime.now().strftime('%Y-%m-%d')}.log" + file_handler = logging.FileHandler(log_file) + file_handler.setLevel(logging.INFO) + file_handler.setFormatter(logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + )) + + # Console handler + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.INFO) + console_handler.setFormatter(logging.Formatter( + '%(asctime)s - %(levelname)s - %(message)s' + )) + + logger.addHandler(file_handler) + logger.addHandler(console_handler) + + return logger + + +def get_job_logger(job_id: int): + """Get a logger specific to a job, writing to a job-specific file.""" + logs_path = Path(settings.logs_path) + logs_path.mkdir(parents=True, exist_ok=True) + + logger = logging.getLogger(f"job_{job_id}") + + if not logger.handlers: + logger.setLevel(logging.DEBUG) + + # Job-specific log file + job_log_file = logs_path / f"job_{job_id}.log" + file_handler = logging.FileHandler(job_log_file) + file_handler.setLevel(logging.DEBUG) + file_handler.setFormatter(logging.Formatter( + '%(asctime)s - %(levelname)s - %(message)s' + )) + + # Also log to daily file + daily_log_file = logs_path / f"scraper_{datetime.now().strftime('%Y-%m-%d')}.log" + daily_handler = logging.FileHandler(daily_log_file) + daily_handler.setLevel(logging.INFO) + daily_handler.setFormatter(logging.Formatter( + '%(asctime)s - job_%(name)s - %(levelname)s - %(message)s' + )) + + logger.addHandler(file_handler) + logger.addHandler(daily_handler) + + return logger diff --git a/backend/app/workers/__init__.py b/backend/app/workers/__init__.py new file mode 100644 index 0000000..5d74713 --- /dev/null +++ b/backend/app/workers/__init__.py @@ -0,0 +1 @@ +# Celery workers diff --git a/backend/app/workers/celery_app.py b/backend/app/workers/celery_app.py new file mode 100644 index 0000000..dcae9fd --- /dev/null +++ b/backend/app/workers/celery_app.py @@ -0,0 +1,36 @@ +from celery import Celery + +from app.config import get_settings + +settings = get_settings() + +celery_app = Celery( + "plant_scraper", + broker=settings.redis_url, + backend=settings.redis_url, + include=[ + "app.workers.scrape_tasks", + "app.workers.quality_tasks", + "app.workers.export_tasks", + "app.workers.stats_tasks", + ], +) + +celery_app.conf.update( + task_serializer="json", + accept_content=["json"], + result_serializer="json", + timezone="UTC", + enable_utc=True, + task_track_started=True, + task_time_limit=3600 * 24, # 24 hour max per task + worker_prefetch_multiplier=1, + task_acks_late=True, + beat_schedule={ + "refresh-stats-every-5min": { + "task": "app.workers.stats_tasks.refresh_stats", + "schedule": 300.0, # Every 5 minutes + }, + }, + beat_schedule_filename="/tmp/celerybeat-schedule", +) diff --git a/backend/app/workers/export_tasks.py b/backend/app/workers/export_tasks.py new file mode 100644 index 0000000..a40e274 --- /dev/null +++ b/backend/app/workers/export_tasks.py @@ -0,0 +1,170 @@ +import json +import os +import random +import shutil +import zipfile +from datetime import datetime +from pathlib import Path + +from app.workers.celery_app import celery_app +from app.database import SessionLocal +from app.models import Export, Image, Species +from app.config import get_settings + +settings = get_settings() + + +@celery_app.task(bind=True) +def generate_export(self, export_id: int): + """Generate a zip export for CoreML training.""" + db = SessionLocal() + try: + export = db.query(Export).filter(Export.id == export_id).first() + if not export: + return {"error": "Export not found"} + + # Update status + export.status = "generating" + export.celery_task_id = self.request.id + db.commit() + + # Parse filter criteria + criteria = json.loads(export.filter_criteria) if export.filter_criteria else {} + min_images = criteria.get("min_images_per_species", 100) + licenses = criteria.get("licenses") + min_quality = criteria.get("min_quality") + species_ids = criteria.get("species_ids") + + # Build query for images + query = db.query(Image).filter(Image.status == "downloaded") + + if licenses: + query = query.filter(Image.license.in_(licenses)) + + if min_quality: + query = query.filter(Image.quality_score >= min_quality) + + if species_ids: + query = query.filter(Image.species_id.in_(species_ids)) + + # Group by species and filter by min count + from sqlalchemy import func + species_counts = db.query( + Image.species_id, + func.count(Image.id).label("count") + ).filter(Image.status == "downloaded").group_by(Image.species_id).all() + + valid_species_ids = [s.species_id for s in species_counts if s.count >= min_images] + + if species_ids: + valid_species_ids = [s for s in valid_species_ids if s in species_ids] + + if not valid_species_ids: + export.status = "failed" + export.error_message = "No species meet the criteria" + export.completed_at = datetime.utcnow() + db.commit() + return {"error": "No species meet the criteria"} + + # Create export directory + export_dir = Path(settings.exports_path) / f"export_{export_id}" + train_dir = export_dir / "Training" + test_dir = export_dir / "Testing" + train_dir.mkdir(parents=True, exist_ok=True) + test_dir.mkdir(parents=True, exist_ok=True) + + total_images = 0 + species_count = 0 + + # Process each valid species + for i, species_id in enumerate(valid_species_ids): + species = db.query(Species).filter(Species.id == species_id).first() + if not species: + continue + + # Get images for this species + images_query = query.filter(Image.species_id == species_id) + if licenses: + images_query = images_query.filter(Image.license.in_(licenses)) + if min_quality: + images_query = images_query.filter(Image.quality_score >= min_quality) + + images = images_query.all() + if len(images) < min_images: + continue + + species_count += 1 + + # Create species folders + species_name = species.scientific_name.replace(" ", "_") + (train_dir / species_name).mkdir(exist_ok=True) + (test_dir / species_name).mkdir(exist_ok=True) + + # Shuffle and split + random.shuffle(images) + split_idx = int(len(images) * export.train_split) + train_images = images[:split_idx] + test_images = images[split_idx:] + + # Copy images + for j, img in enumerate(train_images): + if img.local_path and os.path.exists(img.local_path): + ext = Path(img.local_path).suffix or ".jpg" + dest = train_dir / species_name / f"img_{j:05d}{ext}" + shutil.copy2(img.local_path, dest) + total_images += 1 + + for j, img in enumerate(test_images): + if img.local_path and os.path.exists(img.local_path): + ext = Path(img.local_path).suffix or ".jpg" + dest = test_dir / species_name / f"img_{j:05d}{ext}" + shutil.copy2(img.local_path, dest) + total_images += 1 + + # Update progress + self.update_state( + state="PROGRESS", + meta={ + "current": i + 1, + "total": len(valid_species_ids), + "species": species.scientific_name, + } + ) + + # Create zip file + zip_path = Path(settings.exports_path) / f"export_{export_id}.zip" + with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf: + for root, dirs, files in os.walk(export_dir): + for file in files: + file_path = Path(root) / file + arcname = file_path.relative_to(export_dir) + zipf.write(file_path, arcname) + + # Clean up directory + shutil.rmtree(export_dir) + + # Update export record + export.status = "completed" + export.file_path = str(zip_path) + export.file_size = zip_path.stat().st_size + export.species_count = species_count + export.image_count = total_images + export.completed_at = datetime.utcnow() + db.commit() + + return { + "status": "completed", + "species_count": species_count, + "image_count": total_images, + "file_size": export.file_size, + } + + except Exception as e: + if export: + export.status = "failed" + export.error_message = str(e) + export.completed_at = datetime.utcnow() + db.commit() + raise + finally: + db.close() diff --git a/backend/app/workers/quality_tasks.py b/backend/app/workers/quality_tasks.py new file mode 100644 index 0000000..054628c --- /dev/null +++ b/backend/app/workers/quality_tasks.py @@ -0,0 +1,224 @@ +import os +from pathlib import Path + +import httpx +from PIL import Image as PILImage +import imagehash +import numpy as np +from scipy import ndimage + +from app.workers.celery_app import celery_app +from app.database import SessionLocal +from app.models import Image +from app.config import get_settings + +settings = get_settings() + + +def calculate_blur_score(image_path: str) -> float: + """Calculate blur score using Laplacian variance. Higher = sharper.""" + try: + img = PILImage.open(image_path).convert("L") + img_array = np.array(img) + laplacian = ndimage.laplace(img_array) + return float(np.var(laplacian)) + except Exception: + return 0.0 + + +def calculate_phash(image_path: str) -> str: + """Calculate perceptual hash for deduplication.""" + try: + img = PILImage.open(image_path) + return str(imagehash.phash(img)) + except Exception: + return "" + + +def check_color_distribution(image_path: str) -> tuple[bool, str]: + """Check if image has healthy color distribution for a plant photo. + + Returns (passed, reason) tuple. + Rejects: + - Low color variance (mean channel std < 25): herbarium specimens (brown on white) + - No green + low variance (green ratio < 5% AND mean std < 40): monochrome illustrations + """ + try: + img = PILImage.open(image_path).convert("RGB") + arr = np.array(img, dtype=np.float64) + + # Per-channel standard deviation + channel_stds = arr.std(axis=(0, 1)) # [R_std, G_std, B_std] + mean_std = float(channel_stds.mean()) + + if mean_std < 25: + return False, f"Low color variance ({mean_std:.1f})" + + # Check green ratio + channel_means = arr.mean(axis=(0, 1)) + total = channel_means.sum() + green_ratio = channel_means[1] / total if total > 0 else 0 + + if green_ratio < 0.05 and mean_std < 40: + return False, f"No green ({green_ratio:.2%}) + low variance ({mean_std:.1f})" + + return True, "" + except Exception: + return True, "" # Don't reject on error + + +def resize_image(image_path: str, target_size: int = 512) -> bool: + """Resize image to target size while maintaining aspect ratio.""" + try: + img = PILImage.open(image_path) + img.thumbnail((target_size, target_size), PILImage.Resampling.LANCZOS) + img.save(image_path, quality=95) + return True + except Exception: + return False + + +@celery_app.task +def download_and_process_image(image_id: int): + """Download image, check quality, dedupe, and resize.""" + db = SessionLocal() + try: + image = db.query(Image).filter(Image.id == image_id).first() + if not image: + return {"error": "Image not found"} + + # Create directory for species + species = image.species + species_dir = Path(settings.images_path) / species.scientific_name.replace(" ", "_") + species_dir.mkdir(parents=True, exist_ok=True) + + # Download image + filename = f"{image.source}_{image.source_id or image.id}.jpg" + local_path = species_dir / filename + + try: + headers = { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15" + } + with httpx.Client(timeout=30, headers=headers, follow_redirects=True) as client: + response = client.get(image.url) + response.raise_for_status() + + with open(local_path, "wb") as f: + f.write(response.content) + except Exception as e: + image.status = "rejected" + db.commit() + return {"error": f"Download failed: {e}"} + + # Check minimum size + try: + with PILImage.open(local_path) as img: + width, height = img.size + if width < 256 or height < 256: + os.remove(local_path) + image.status = "rejected" + db.commit() + return {"error": "Image too small"} + image.width = width + image.height = height + except Exception as e: + if local_path.exists(): + os.remove(local_path) + image.status = "rejected" + db.commit() + return {"error": f"Invalid image: {e}"} + + # Calculate perceptual hash for deduplication + phash = calculate_phash(str(local_path)) + if phash: + # Check for duplicates + existing = db.query(Image).filter( + Image.phash == phash, + Image.id != image.id, + Image.status == "downloaded" + ).first() + + if existing: + os.remove(local_path) + image.status = "rejected" + image.phash = phash + db.commit() + return {"error": "Duplicate image"} + + image.phash = phash + + # Calculate blur score + quality_score = calculate_blur_score(str(local_path)) + image.quality_score = quality_score + + # Reject very blurry images (threshold can be tuned) + if quality_score < 100: # Low variance = blurry + os.remove(local_path) + image.status = "rejected" + db.commit() + return {"error": "Image too blurry"} + + # Check color distribution (reject herbarium specimens, illustrations) + color_ok, color_reason = check_color_distribution(str(local_path)) + if not color_ok: + os.remove(local_path) + image.status = "rejected" + db.commit() + return {"error": f"Non-photo content: {color_reason}"} + + # Resize to 512x512 max + resize_image(str(local_path)) + + # Update image record + image.local_path = str(local_path) + image.status = "downloaded" + db.commit() + + return { + "status": "success", + "path": str(local_path), + "quality_score": quality_score, + } + + except Exception as e: + if image: + image.status = "rejected" + db.commit() + return {"error": str(e)} + finally: + db.close() + + +@celery_app.task(bind=True) +def batch_process_pending_images(self, source: str = None, chunk_size: int = 500): + """Process ALL pending images in chunks, with progress tracking.""" + db = SessionLocal() + try: + query = db.query(Image).filter(Image.status == "pending") + if source: + query = query.filter(Image.source == source) + + total = query.count() + queued = 0 + offset = 0 + + while offset < total: + chunk = query.order_by(Image.id).offset(offset).limit(chunk_size).all() + if not chunk: + break + + for image in chunk: + download_and_process_image.delay(image.id) + queued += 1 + + offset += len(chunk) + + self.update_state( + state="PROGRESS", + meta={"queued": queued, "total": total}, + ) + + return {"queued": queued, "total": total} + finally: + db.close() diff --git a/backend/app/workers/scrape_tasks.py b/backend/app/workers/scrape_tasks.py new file mode 100644 index 0000000..0132d6d --- /dev/null +++ b/backend/app/workers/scrape_tasks.py @@ -0,0 +1,164 @@ +import json +from datetime import datetime + +from app.workers.celery_app import celery_app +from app.database import SessionLocal +from app.models import Job, Species, Image +from app.utils.logging import get_job_logger + + +@celery_app.task(bind=True) +def run_scrape_job(self, job_id: int): + """Main scrape task that dispatches to source-specific scrapers.""" + logger = get_job_logger(job_id) + logger.info(f"Starting scrape job {job_id}") + + db = SessionLocal() + job = None + try: + job = db.query(Job).filter(Job.id == job_id).first() + if not job: + logger.error(f"Job {job_id} not found") + return {"error": "Job not found"} + + logger.info(f"Job: {job.name}, Source: {job.source}") + + # Update job status + job.status = "running" + job.started_at = datetime.utcnow() + job.celery_task_id = self.request.id + db.commit() + + # Get species to scrape + if job.species_filter: + species_ids = json.loads(job.species_filter) + query = db.query(Species).filter(Species.id.in_(species_ids)) + logger.info(f"Filtered to species IDs: {species_ids}") + else: + query = db.query(Species) + logger.info("Scraping all species") + + # Filter by image count if requested + if job.only_without_images or job.max_images: + from sqlalchemy import func + # Subquery to count downloaded images per species + image_count_subquery = ( + db.query(Image.species_id, func.count(Image.id).label("count")) + .filter(Image.status == "downloaded") + .group_by(Image.species_id) + .subquery() + ) + # Left join with the count subquery + query = query.outerjoin( + image_count_subquery, + Species.id == image_count_subquery.c.species_id + ) + + if job.only_without_images: + # Filter where count is NULL or 0 + query = query.filter( + (image_count_subquery.c.count == None) | (image_count_subquery.c.count == 0) + ) + logger.info("Filtering to species without images") + elif job.max_images: + # Filter where count is NULL or less than max_images + query = query.filter( + (image_count_subquery.c.count == None) | (image_count_subquery.c.count < job.max_images) + ) + logger.info(f"Filtering to species with fewer than {job.max_images} images") + + species_list = query.all() + logger.info(f"Total species to scrape: {len(species_list)}") + + job.progress_total = len(species_list) + db.commit() + + # Import scraper based on source + from app.scrapers import get_scraper + scraper = get_scraper(job.source) + + if not scraper: + error_msg = f"Unknown source: {job.source}" + logger.error(error_msg) + job.status = "failed" + job.error_message = error_msg + job.completed_at = datetime.utcnow() + db.commit() + return {"error": error_msg} + + logger.info(f"Using scraper: {scraper.name}") + + # Scrape each species + for i, species in enumerate(species_list): + try: + # Update progress + job.progress_current = i + 1 + db.commit() + + logger.info(f"[{i+1}/{len(species_list)}] Scraping: {species.scientific_name}") + + # Update task state for real-time monitoring + self.update_state( + state="PROGRESS", + meta={ + "current": i + 1, + "total": len(species_list), + "species": species.scientific_name, + } + ) + + # Run scraper for this species + results = scraper.scrape_species(species, db, logger) + downloaded = results.get("downloaded", 0) + rejected = results.get("rejected", 0) + job.images_downloaded += downloaded + job.images_rejected += rejected + db.commit() + + logger.info(f" -> Downloaded: {downloaded}, Rejected: {rejected}") + + except Exception as e: + # Log error but continue with other species + logger.error(f"Error scraping {species.scientific_name}: {e}", exc_info=True) + continue + + # Mark job complete + job.status = "completed" + job.completed_at = datetime.utcnow() + db.commit() + + logger.info(f"Job {job_id} completed. Total downloaded: {job.images_downloaded}, rejected: {job.images_rejected}") + + return { + "status": "completed", + "downloaded": job.images_downloaded, + "rejected": job.images_rejected, + } + + except Exception as e: + logger.error(f"Job {job_id} failed with error: {e}", exc_info=True) + if job: + job.status = "failed" + job.error_message = str(e) + job.completed_at = datetime.utcnow() + db.commit() + raise + finally: + db.close() + + +@celery_app.task +def pause_scrape_job(job_id: int): + """Pause a running scrape job.""" + db = SessionLocal() + try: + job = db.query(Job).filter(Job.id == job_id).first() + if job and job.status == "running": + job.status = "paused" + db.commit() + # Revoke the Celery task + if job.celery_task_id: + celery_app.control.revoke(job.celery_task_id, terminate=True) + return {"status": "paused"} + finally: + db.close() diff --git a/backend/app/workers/stats_tasks.py b/backend/app/workers/stats_tasks.py new file mode 100644 index 0000000..db8d417 --- /dev/null +++ b/backend/app/workers/stats_tasks.py @@ -0,0 +1,193 @@ +import json +import os +from datetime import datetime +from pathlib import Path + +from sqlalchemy import func, case, text + +from app.workers.celery_app import celery_app +from app.database import SessionLocal +from app.models import Species, Image, Job +from app.models.cached_stats import CachedStats +from app.config import get_settings + + +def get_directory_size_fast(path: str) -> int: + """Get directory size in bytes using fast os.scandir.""" + total = 0 + try: + with os.scandir(path) as it: + for entry in it: + try: + if entry.is_file(follow_symlinks=False): + total += entry.stat(follow_symlinks=False).st_size + elif entry.is_dir(follow_symlinks=False): + total += get_directory_size_fast(entry.path) + except (OSError, PermissionError): + pass + except (OSError, PermissionError): + pass + return total + + +@celery_app.task +def refresh_stats(): + """Calculate and cache dashboard statistics.""" + print("=== STATS TASK: Starting refresh ===", flush=True) + + db = SessionLocal() + try: + # Use raw SQL for maximum performance on SQLite + # All counts in a single query + counts_sql = text(""" + SELECT + (SELECT COUNT(*) FROM species) as total_species, + (SELECT COUNT(*) FROM images) as total_images, + (SELECT COUNT(*) FROM images WHERE status = 'downloaded') as images_downloaded, + (SELECT COUNT(*) FROM images WHERE status = 'pending') as images_pending, + (SELECT COUNT(*) FROM images WHERE status = 'rejected') as images_rejected + """) + counts = db.execute(counts_sql).fetchone() + total_species = counts[0] or 0 + total_images = counts[1] or 0 + images_downloaded = counts[2] or 0 + images_pending = counts[3] or 0 + images_rejected = counts[4] or 0 + + # Per-source stats - single query with GROUP BY + source_sql = text(""" + SELECT + source, + COUNT(*) as total, + SUM(CASE WHEN status = 'downloaded' THEN 1 ELSE 0 END) as downloaded, + SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending, + SUM(CASE WHEN status = 'rejected' THEN 1 ELSE 0 END) as rejected + FROM images + GROUP BY source + """) + source_stats_raw = db.execute(source_sql).fetchall() + sources = [ + { + "source": s[0], + "image_count": s[1], + "downloaded": s[2] or 0, + "pending": s[3] or 0, + "rejected": s[4] or 0, + } + for s in source_stats_raw + ] + + # Per-license stats - single indexed query + license_sql = text(""" + SELECT license, COUNT(*) as count + FROM images + WHERE status = 'downloaded' + GROUP BY license + """) + license_stats_raw = db.execute(license_sql).fetchall() + licenses = [ + {"license": l[0], "count": l[1]} + for l in license_stats_raw + ] + + # Job stats - single query + job_sql = text(""" + SELECT + SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) as running, + SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending, + SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed, + SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed + FROM jobs + """) + job_counts = db.execute(job_sql).fetchone() + jobs = { + "running": job_counts[0] or 0, + "pending": job_counts[1] or 0, + "completed": job_counts[2] or 0, + "failed": job_counts[3] or 0, + } + + # Top species by image count - optimized with index + top_sql = text(""" + SELECT s.id, s.scientific_name, s.common_name, COUNT(i.id) as image_count + FROM species s + INNER JOIN images i ON i.species_id = s.id AND i.status = 'downloaded' + GROUP BY s.id + ORDER BY image_count DESC + LIMIT 10 + """) + top_species_raw = db.execute(top_sql).fetchall() + top_species = [ + { + "id": s[0], + "scientific_name": s[1], + "common_name": s[2], + "image_count": s[3], + } + for s in top_species_raw + ] + + # Under-represented species - use pre-computed counts + under_sql = text(""" + SELECT s.id, s.scientific_name, s.common_name, COALESCE(img_counts.cnt, 0) as image_count + FROM species s + LEFT JOIN ( + SELECT species_id, COUNT(*) as cnt + FROM images + WHERE status = 'downloaded' + GROUP BY species_id + ) img_counts ON img_counts.species_id = s.id + WHERE COALESCE(img_counts.cnt, 0) < 100 + ORDER BY image_count ASC + LIMIT 10 + """) + under_rep_raw = db.execute(under_sql).fetchall() + under_represented = [ + { + "id": s[0], + "scientific_name": s[1], + "common_name": s[2], + "image_count": s[3], + } + for s in under_rep_raw + ] + + # Calculate disk usage (fast recursive scan) + settings = get_settings() + disk_usage_bytes = get_directory_size_fast(settings.images_path) + disk_usage_mb = round(disk_usage_bytes / (1024 * 1024), 2) + + # Build the stats object + stats = { + "total_species": total_species, + "total_images": total_images, + "images_downloaded": images_downloaded, + "images_pending": images_pending, + "images_rejected": images_rejected, + "disk_usage_mb": disk_usage_mb, + "sources": sources, + "licenses": licenses, + "jobs": jobs, + "top_species": top_species, + "under_represented": under_represented, + } + + # Store in database + cached = db.query(CachedStats).filter(CachedStats.key == "dashboard_stats").first() + if cached: + cached.value = json.dumps(stats) + cached.updated_at = datetime.utcnow() + else: + cached = CachedStats(key="dashboard_stats", value=json.dumps(stats)) + db.add(cached) + + db.commit() + print(f"=== STATS TASK: Refreshed (species={total_species}, images={total_images}) ===", flush=True) + + return {"status": "success", "total_species": total_species, "total_images": total_images} + + except Exception as e: + print(f"=== STATS TASK ERROR: {e} ===", flush=True) + raise + finally: + db.close() diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..50187bb --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,34 @@ +# Web framework +fastapi==0.109.0 +uvicorn[standard]==0.27.0 +python-multipart==0.0.6 + +# Database +sqlalchemy==2.0.25 +alembic==1.13.1 +aiosqlite==0.19.0 + +# Task queue +celery==5.3.6 +redis==5.0.1 + +# Image processing +Pillow==10.2.0 +imagehash==4.3.1 +imagededup==0.3.3.post2 + +# HTTP clients +httpx==0.26.0 +aiohttp==3.9.3 + +# Search +duckduckgo-search + +# Utilities +python-dotenv==1.0.0 +pydantic==2.5.3 +pydantic-settings==2.1.0 + +# Testing +pytest==7.4.4 +pytest-asyncio==0.23.3 diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 0000000..007eb95 --- /dev/null +++ b/backend/tests/__init__.py @@ -0,0 +1 @@ +# Tests diff --git a/docker-compose.unraid.yml b/docker-compose.unraid.yml new file mode 100644 index 0000000..c67687c --- /dev/null +++ b/docker-compose.unraid.yml @@ -0,0 +1,114 @@ +# Docker Compose for Unraid +# +# Access at http://YOUR_UNRAID_IP:8580 +# +# ============================================ +# CONFIGURE THESE PATHS FOR YOUR UNRAID SETUP +# ============================================ +# Edit the left side of the colon (:) for each volume mount +# +# DATABASE_PATH: Where to store the SQLite database +# IMAGES_PATH: Where to store downloaded images (can be large, 100GB+) +# EXPORTS_PATH: Where to store generated export zip files +# IMPORTS_PATH: Where to place images for bulk import (source/species/images) +# LOGS_PATH: Where to store scraper log files for debugging + +services: + backend: + build: + context: /mnt/user/appdata/PlantGuideScraper/backend + dockerfile: Dockerfile + container_name: plant-scraper-backend + restart: unless-stopped + volumes: + - /mnt/user/appdata/PlantGuideScraper/backend:/app:ro + # === CONFIGURABLE DATA PATHS === + - /mnt/user/downloads/PlantGuideDocker/database:/data/db # DATABASE_PATH + - /mnt/user/downloads/PlantGuideDocker/images:/data/images # IMAGES_PATH + - /mnt/user/downloads/PlantGuideDocker/exports:/data/exports # EXPORTS_PATH + - /mnt/user/downloads/PlantGuideDocker/imports:/data/imports # IMPORTS_PATH + - /mnt/user/downloads/PlantGuideDocker/logs:/data/logs # LOGS_PATH + environment: + - DATABASE_URL=sqlite:////data/db/plants.sqlite + - REDIS_URL=redis://plant-scraper-redis:6379/0 + - IMAGES_PATH=/data/images + - EXPORTS_PATH=/data/exports + - IMPORTS_PATH=/data/imports + - LOGS_PATH=/data/logs + depends_on: + - redis + command: uvicorn app.main:app --host 0.0.0.0 --port 8000 + networks: + - plant-scraper + + celery: + build: + context: /mnt/user/appdata/PlantGuideScraper/backend + dockerfile: Dockerfile + container_name: plant-scraper-celery + restart: unless-stopped + volumes: + - /mnt/user/appdata/PlantGuideScraper/backend:/app:ro + # === CONFIGURABLE DATA PATHS (must match backend) === + - /mnt/user/downloads/PlantGuideDocker/database:/data/db # DATABASE_PATH + - /mnt/user/downloads/PlantGuideDocker/images:/data/images # IMAGES_PATH + - /mnt/user/downloads/PlantGuideDocker/exports:/data/exports # EXPORTS_PATH + - /mnt/user/downloads/PlantGuideDocker/imports:/data/imports # IMPORTS_PATH + - /mnt/user/downloads/PlantGuideDocker/logs:/data/logs # LOGS_PATH + environment: + - DATABASE_URL=sqlite:////data/db/plants.sqlite + - REDIS_URL=redis://plant-scraper-redis:6379/0 + - IMAGES_PATH=/data/images + - EXPORTS_PATH=/data/exports + - IMPORTS_PATH=/data/imports + - LOGS_PATH=/data/logs + depends_on: + - redis + command: celery -A app.workers.celery_app worker --beat --loglevel=info --concurrency=4 + networks: + - plant-scraper + + redis: + image: redis:7-alpine + container_name: plant-scraper-redis + restart: unless-stopped + volumes: + - /mnt/user/appdata/PlantGuideScraper/redis:/data + networks: + - plant-scraper + + frontend: + build: + context: /mnt/user/appdata/PlantGuideScraper/frontend + dockerfile: Dockerfile + container_name: plant-scraper-frontend + restart: unless-stopped + volumes: + - /mnt/user/appdata/PlantGuideScraper/frontend:/app + - plant-scraper-node-modules:/app/node_modules + environment: + - VITE_API_URL= + command: npm run dev -- --host + networks: + - plant-scraper + + nginx: + image: nginx:alpine + container_name: plant-scraper-nginx + restart: unless-stopped + ports: + - "8580:80" + volumes: + - /mnt/user/appdata/PlantGuideScraper/nginx/nginx.conf:/etc/nginx/nginx.conf:ro + depends_on: + - backend + - frontend + networks: + - plant-scraper + +networks: + plant-scraper: + name: plant-scraper + +volumes: + plant-scraper-node-modules: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9093603 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,73 @@ +services: + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: plant-scraper-backend + # Port exposed only internally, nginx proxies to it + volumes: + - ./backend:/app + - ./data:/data + environment: + - DATABASE_URL=sqlite:////data/db/plants.sqlite + - REDIS_URL=redis://redis:6379/0 + - IMAGES_PATH=/data/images + - EXPORTS_PATH=/data/exports + - IMPORTS_PATH=/data/imports + - LOGS_PATH=/data/logs + depends_on: + - redis + command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload + + celery: + build: + context: ./backend + dockerfile: Dockerfile + container_name: plant-scraper-celery + volumes: + - ./backend:/app + - ./data:/data + environment: + - DATABASE_URL=sqlite:////data/db/plants.sqlite + - REDIS_URL=redis://redis:6379/0 + - IMAGES_PATH=/data/images + - EXPORTS_PATH=/data/exports + - IMPORTS_PATH=/data/imports + - LOGS_PATH=/data/logs + depends_on: + - redis + command: celery -A app.workers.celery_app worker --beat --loglevel=info --concurrency=4 + + redis: + image: redis:7-alpine + container_name: plant-scraper-redis + # Port exposed only internally, not to host (avoid conflicts) + volumes: + - redis_data:/data + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + container_name: plant-scraper-frontend + # Port exposed only internally, nginx proxies to it + volumes: + - ./frontend:/app + - /app/node_modules + environment: + - VITE_API_URL= + command: npm run dev -- --host + + nginx: + image: nginx:alpine + container_name: plant-scraper-nginx + ports: + - "80:80" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + depends_on: + - backend + - frontend + +volumes: + redis_data: diff --git a/docs/master_plan.md b/docs/master_plan.md new file mode 100644 index 0000000..8fb0006 --- /dev/null +++ b/docs/master_plan.md @@ -0,0 +1,564 @@ +# Houseplant Image Scraper - Master Plan + +## Overview + +Web-based interface for managing a multi-source image scraping pipeline targeting 5-10K houseplant species with 1-5M total images. Runs on Unraid via Docker, exports datasets for CoreML training. + +--- + +## Requirements Summary + +| Requirement | Value | +|-------------|-------| +| Platform | Web app in Docker on Unraid | +| Sources | iNaturalist/GBIF, Flickr, Wikimedia Commons, Trefle, USDA PLANTS, EOL | +| API keys | Configurable per service | +| Species list | Manual import (CSV/paste) | +| Grouping | Species, genus, source, license (faceted) | +| Search/filter | Yes | +| Quality filter | Automatic (hash dedup, blur, size) | +| Progress | Real-time dashboard | +| Storage | `/species_name/image.jpg` + SQLite DB | +| Export | Filtered zip for CoreML, downloadable anytime | +| Auth | None (single user) | +| Deployment | Docker Compose | + +--- + +## Create ML Export Requirements + +Per [Apple's documentation](https://developer.apple.com/documentation/createml/creating-an-image-classifier-model): + +- **Folder structure**: `/SpeciesName/image001.jpg` (folder name = class label) +- **Train/Test split**: 80/20 recommended, separate folders +- **Balance**: Roughly equal images per class (avoid bias) +- **No metadata needed**: Create ML uses folder names as labels + +### Export Format + +``` +dataset_export/ +├── Training/ +│ ├── Monstera_deliciosa/ +│ │ ├── img001.jpg +│ │ └── ... +│ ├── Philodendron_hederaceum/ +│ └── ... +└── Testing/ + ├── Monstera_deliciosa/ + └── ... +``` + +--- + +## Data Sources + +| Source | API/Method | License Filter | Rate Limits | Notes | +|--------|------------|----------------|-------------|-------| +| **iNaturalist/GBIF** | Bulk DwC-A export + API | CC0, CC-BY | 1 req/sec, 10k/day, 5GB/hr media | Best source: Research Grade = verified | +| **Flickr** | flickr.photos.search | license=4,9 (CC-BY, CC0) | 3600 req/hr | Good supplemental | +| **Wikimedia Commons** | MediaWiki API + pyWikiCommons | CC-BY, CC-BY-SA, PD | Generous | Category-based search | +| **Trefle.io** | REST API | Open source | Free tier | Species metadata + some images | +| **USDA PLANTS** | REST API | Public Domain | Generous | US-focused, limited images | +| **Plant.id** | REST API | Commercial | Paid | For validation, not scraping | +| **Encyclopedia of Life** | API | Mixed | Check each | Aggregator | + +### Source References + +- iNaturalist: https://www.inaturalist.org/pages/developers +- iNaturalist bulk download: https://forum.inaturalist.org/t/one-time-bulk-download-dataset/18741 +- Flickr API: https://www.flickr.com/services/api/flickr.photos.search.html +- Wikimedia Commons API: https://commons.wikimedia.org/wiki/Commons:API +- pyWikiCommons: https://pypi.org/project/pyWikiCommons/ +- Trefle.io: https://trefle.io/ +- USDA PLANTS: https://data.nal.usda.gov/dataset/usda-plants-database-api-r + +### Flickr License IDs + +| ID | License | +|----|---------| +| 0 | All Rights Reserved | +| 1 | CC BY-NC-SA 2.0 | +| 2 | CC BY-NC 2.0 | +| 3 | CC BY-NC-ND 2.0 | +| 4 | CC BY 2.0 (Commercial OK) | +| 5 | CC BY-SA 2.0 | +| 6 | CC BY-ND 2.0 | +| 7 | No known copyright restrictions | +| 8 | United States Government Work | +| 9 | Public Domain (CC0) | + +**For commercial use**: Filter to license IDs 4, 7, 8, 9 only. + +--- + +## Image Quality Pipeline + +| Stage | Library | Purpose | +|-------|---------|---------| +| **Deduplication** | imagededup | Perceptual hash (CNN + hash methods) | +| **Blur detection** | scipy + Sobel variance | Reject blurry images | +| **Size filter** | Pillow | Min 256x256 | +| **Resize** | Pillow | Normalize to 512x512 | + +### Library References + +- imagededup: https://github.com/idealo/imagededup +- imagehash: https://github.com/JohannesBuchner/imagehash + +--- + +## Technology Stack + +| Component | Choice | Rationale | +|-----------|--------|-----------| +| **Backend** | FastAPI (Python) | Async, fast, ML ecosystem, great docs | +| **Frontend** | React + Tailwind | Fast dev, good component libraries | +| **Database** | SQLite (+ FTS5) | Simple, no separate container, sufficient for single-user | +| **Task Queue** | Celery + Redis | Long-running scrape jobs, good monitoring | +| **Containers** | Docker Compose | Multi-service orchestration | + +Reference: https://github.com/fastapi/full-stack-fastapi-template + +--- + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ DOCKER COMPOSE ON UNRAID │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────┐ ┌─────────────────────────────────────────────────┐ │ +│ │ NGINX │ │ FASTAPI BACKEND │ │ +│ │ :80 │───▶│ /api/species - CRUD species list │ │ +│ │ │ │ /api/sources - API key management │ │ +│ └──────┬──────┘ │ /api/jobs - Scrape job control │ │ +│ │ │ /api/images - Search, filter, browse │ │ +│ ▼ │ /api/export - Generate zip for CoreML │ │ +│ ┌─────────────┐ │ /api/stats - Dashboard metrics │ │ +│ │ REACT │ └─────────────────────────────────────────────────┘ │ +│ │ SPA │ │ │ +│ │ :3000 │ ▼ │ +│ └─────────────┘ ┌─────────────────────────────────────────────────┐ │ +│ │ CELERY WORKERS │ │ +│ ┌─────────────┐ │ - iNaturalist scraper │ │ +│ │ REDIS │◀───│ - Flickr scraper │ │ +│ │ :6379 │ │ - Wikimedia scraper │ │ +│ └─────────────┘ │ - Quality filter pipeline │ │ +│ │ - Export generator │ │ +│ └─────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────────┐│ +│ │ STORAGE (Bind Mounts) ││ +│ │ /data/db/plants.sqlite - Species, images metadata, jobs ││ +│ │ /data/images/{species}/ - Downloaded images ││ +│ │ /data/exports/ - Generated zip files ││ +│ └─────────────────────────────────────────────────────────────────────┘│ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Database Schema + +```sql +-- Species master list (imported from CSV) +CREATE TABLE species ( + id INTEGER PRIMARY KEY, + scientific_name TEXT UNIQUE NOT NULL, + common_name TEXT, + genus TEXT, + family TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Full-text search index +CREATE VIRTUAL TABLE species_fts USING fts5( + scientific_name, + common_name, + genus, + content='species', + content_rowid='id' +); + +-- API credentials +CREATE TABLE api_keys ( + id INTEGER PRIMARY KEY, + source TEXT UNIQUE NOT NULL, -- 'flickr', 'inaturalist', 'wikimedia', 'trefle' + api_key TEXT NOT NULL, + api_secret TEXT, + rate_limit_per_sec REAL DEFAULT 1.0, + enabled BOOLEAN DEFAULT TRUE +); + +-- Downloaded images +CREATE TABLE images ( + id INTEGER PRIMARY KEY, + species_id INTEGER REFERENCES species(id), + source TEXT NOT NULL, + source_id TEXT, -- Original ID from source + url TEXT NOT NULL, + local_path TEXT, + license TEXT NOT NULL, + attribution TEXT, + width INTEGER, + height INTEGER, + phash TEXT, -- Perceptual hash for dedup + quality_score REAL, -- Blur/quality metric + status TEXT DEFAULT 'pending', -- pending, downloaded, rejected, deleted + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(source, source_id) +); + +-- Index for common queries +CREATE INDEX idx_images_species ON images(species_id); +CREATE INDEX idx_images_status ON images(status); +CREATE INDEX idx_images_source ON images(source); +CREATE INDEX idx_images_phash ON images(phash); + +-- Scrape jobs +CREATE TABLE jobs ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + source TEXT NOT NULL, + species_filter TEXT, -- JSON array of species IDs or NULL for all + status TEXT DEFAULT 'pending', -- pending, running, paused, completed, failed + progress_current INTEGER DEFAULT 0, + progress_total INTEGER DEFAULT 0, + images_downloaded INTEGER DEFAULT 0, + images_rejected INTEGER DEFAULT 0, + started_at TIMESTAMP, + completed_at TIMESTAMP, + error_message TEXT +); + +-- Export jobs +CREATE TABLE exports ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + filter_criteria TEXT, -- JSON: min_images, licenses, min_quality, species_ids + train_split REAL DEFAULT 0.8, + status TEXT DEFAULT 'pending', -- pending, generating, completed, failed + file_path TEXT, + file_size INTEGER, + species_count INTEGER, + image_count INTEGER, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + completed_at TIMESTAMP +); +``` + +--- + +## API Endpoints + +### Species + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/species` | List species (paginated, searchable) | +| POST | `/api/species` | Create single species | +| POST | `/api/species/import` | Bulk import from CSV | +| GET | `/api/species/{id}` | Get species details | +| PUT | `/api/species/{id}` | Update species | +| DELETE | `/api/species/{id}` | Delete species | + +### API Keys + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/sources` | List configured sources | +| PUT | `/api/sources/{source}` | Update source config (key, rate limit) | + +### Jobs + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/jobs` | List jobs | +| POST | `/api/jobs` | Create scrape job | +| GET | `/api/jobs/{id}` | Get job status | +| POST | `/api/jobs/{id}/pause` | Pause job | +| POST | `/api/jobs/{id}/resume` | Resume job | +| POST | `/api/jobs/{id}/cancel` | Cancel job | + +### Images + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/images` | List images (paginated, filterable) | +| GET | `/api/images/{id}` | Get image details | +| DELETE | `/api/images/{id}` | Delete image | +| POST | `/api/images/bulk-delete` | Bulk delete | + +### Export + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/exports` | List exports | +| POST | `/api/exports` | Create export job | +| GET | `/api/exports/{id}` | Get export status | +| GET | `/api/exports/{id}/download` | Download zip file | + +### Stats + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/stats` | Dashboard statistics | +| GET | `/api/stats/sources` | Per-source breakdown | +| GET | `/api/stats/species` | Per-species image counts | + +--- + +## UI Screens + +### 1. Dashboard + +- Total species, images by source, images by license +- Active jobs with progress bars +- Quick stats: images/sec, disk usage +- Recent activity feed + +### 2. Species Management + +- Table: scientific name, common name, genus, image count +- Import CSV button (drag-and-drop) +- Search/filter by name, genus +- Bulk select → "Start Scrape" button +- Inline editing + +### 3. API Keys + +- Card per source with: + - API key input (masked) + - API secret input (if applicable) + - Rate limit slider + - Enable/disable toggle + - Test connection button + +### 4. Image Browser + +- Grid view with thumbnails (lazy-loaded) +- Filters sidebar: + - Species (autocomplete) + - Source (checkboxes) + - License (checkboxes) + - Quality score (range slider) + - Status (tabs: all, pending, downloaded, rejected) +- Sort by: date, quality, species +- Bulk select → actions (delete, re-queue) +- Click to view full-size + metadata + +### 5. Jobs + +- Table: name, source, status, progress, dates +- Real-time progress updates (WebSocket) +- Actions: pause, resume, cancel, view logs + +### 6. Export + +- Filter builder: + - Min images per species + - License whitelist + - Min quality score + - Species selection (all or specific) +- Train/test split slider (default 80/20) +- Preview: estimated species count, image count, file size +- "Generate Zip" button +- Download history with re-download links + +--- + +## Tradeoffs + +| Decision | Alternative | Why This Choice | +|----------|-------------|-----------------| +| SQLite | PostgreSQL | Single-user, simpler Docker setup, sufficient for millions of rows | +| Celery+Redis | RQ, Dramatiq | Battle-tested, good monitoring (Flower) | +| React | Vue, Svelte | Largest ecosystem, more component libraries | +| Separate workers | Threads in FastAPI | Better isolation, can scale workers independently | +| Nginx reverse proxy | Traefik | Simpler config for single-app deployment | + +--- + +## Risks & Mitigations + +| Risk | Likelihood | Mitigation | +|------|------------|------------| +| iNaturalist rate limits (5GB/hr) | High | Throttle downloads, prioritize species with low counts | +| Disk fills up | Medium | Dashboard shows disk usage, configurable storage limits | +| Scrape jobs crash mid-run | Medium | Job state in DB, resume from last checkpoint | +| Perceptual hash collisions | Low | Store hash, allow manual review of flagged duplicates | +| API keys exposed | Low | Environment variables, not stored in code | +| SQLite write contention | Low | WAL mode, single writer pattern via Celery | + +--- + +## Implementation Phases + +### Phase 1: Foundation +- [ ] Docker Compose setup (FastAPI, React, Redis, Nginx) +- [ ] Database schema + migrations (Alembic) +- [ ] Basic FastAPI skeleton with health checks +- [ ] React app scaffolding with Tailwind + +### Phase 2: Core Data Management +- [ ] Species CRUD API +- [ ] CSV import endpoint +- [ ] Species list UI with search/filter +- [ ] API keys management UI + +### Phase 3: iNaturalist Scraper +- [ ] Celery worker setup +- [ ] iNaturalist/GBIF scraper task +- [ ] Job management API +- [ ] Real-time progress (WebSocket or polling) + +### Phase 4: Quality Pipeline +- [ ] Image download worker +- [ ] Perceptual hash deduplication +- [ ] Blur detection + quality scoring +- [ ] Resize to 512x512 + +### Phase 5: Image Browser +- [ ] Image listing API with filters +- [ ] Thumbnail generation +- [ ] Grid view UI +- [ ] Bulk operations + +### Phase 6: Additional Scrapers +- [ ] Flickr scraper +- [ ] Wikimedia Commons scraper +- [ ] Trefle scraper (metadata + images) +- [ ] USDA PLANTS scraper + +### Phase 7: Export +- [ ] Export job API +- [ ] Train/test split logic +- [ ] Zip generation worker +- [ ] Download endpoint +- [ ] Export UI with filters + +### Phase 8: Dashboard & Polish +- [ ] Stats API +- [ ] Dashboard UI with charts +- [ ] Job monitoring UI +- [ ] Error handling + logging +- [ ] Documentation + +--- + +## File Structure + +``` +PlantGuideScraper/ +├── docker-compose.yml +├── .env.example +├── docs/ +│ └── master_plan.md +├── backend/ +│ ├── Dockerfile +│ ├── requirements.txt +│ ├── alembic/ +│ │ └── versions/ +│ ├── app/ +│ │ ├── __init__.py +│ │ ├── main.py +│ │ ├── config.py +│ │ ├── database.py +│ │ ├── models/ +│ │ │ ├── species.py +│ │ │ ├── image.py +│ │ │ ├── job.py +│ │ │ └── export.py +│ │ ├── schemas/ +│ │ │ └── ... +│ │ ├── api/ +│ │ │ ├── species.py +│ │ │ ├── images.py +│ │ │ ├── jobs.py +│ │ │ ├── exports.py +│ │ │ └── stats.py +│ │ ├── scrapers/ +│ │ │ ├── base.py +│ │ │ ├── inaturalist.py +│ │ │ ├── flickr.py +│ │ │ ├── wikimedia.py +│ │ │ └── trefle.py +│ │ ├── workers/ +│ │ │ ├── celery_app.py +│ │ │ ├── scrape_tasks.py +│ │ │ ├── quality_tasks.py +│ │ │ └── export_tasks.py +│ │ └── utils/ +│ │ ├── image_quality.py +│ │ └── dedup.py +│ └── tests/ +├── frontend/ +│ ├── Dockerfile +│ ├── package.json +│ ├── src/ +│ │ ├── App.tsx +│ │ ├── components/ +│ │ ├── pages/ +│ │ │ ├── Dashboard.tsx +│ │ │ ├── Species.tsx +│ │ │ ├── Images.tsx +│ │ │ ├── Jobs.tsx +│ │ │ ├── Export.tsx +│ │ │ └── Settings.tsx +│ │ ├── hooks/ +│ │ └── api/ +│ └── public/ +├── nginx/ +│ └── nginx.conf +└── data/ # Bind mount (not in repo) + ├── db/ + ├── images/ + └── exports/ +``` + +--- + +## Environment Variables + +```bash +# Backend +DATABASE_URL=sqlite:///data/db/plants.sqlite +REDIS_URL=redis://redis:6379/0 +IMAGES_PATH=/data/images +EXPORTS_PATH=/data/exports + +# API Keys (user-provided) +FLICKR_API_KEY= +FLICKR_API_SECRET= +INATURALIST_APP_ID= +INATURALIST_APP_SECRET= +TREFLE_API_KEY= + +# Optional +LOG_LEVEL=INFO +CELERY_CONCURRENCY=4 +``` + +--- + +## Commands + +```bash +# Development +docker-compose up --build + +# Production +docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d + +# Run migrations +docker-compose exec backend alembic upgrade head + +# View Celery logs +docker-compose logs -f celery + +# Access Redis CLI +docker-compose exec redis redis-cli +``` diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..c2cf1d6 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,14 @@ +FROM node:20-alpine + +WORKDIR /app + +# Install dependencies +COPY package*.json ./ +RUN npm install + +# Copy source +COPY . . + +EXPOSE 3000 + +CMD ["npm", "run", "dev", "--", "--host"] diff --git a/frontend/dist/assets/index-BXIq8BNP.js b/frontend/dist/assets/index-BXIq8BNP.js new file mode 100644 index 0000000..7da682b --- /dev/null +++ b/frontend/dist/assets/index-BXIq8BNP.js @@ -0,0 +1,283 @@ +var I0=e=>{throw TypeError(e)};var Eh=(e,t,r)=>t.has(e)||I0("Cannot "+r);var E=(e,t,r)=>(Eh(e,t,"read from private field"),r?r.call(e):t.get(e)),Z=(e,t,r)=>t.has(e)?I0("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,r),G=(e,t,r,n)=>(Eh(e,t,"write to private field"),n?n.call(e,r):t.set(e,r),r),ae=(e,t,r)=>(Eh(e,t,"access private method"),r);var Au=(e,t,r,n)=>({set _(i){G(e,t,i,r)},get _(){return E(e,t,n)}});function E2(e,t){for(var r=0;rn[i]})}}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))n(i);new MutationObserver(i=>{for(const a of i)if(a.type==="childList")for(const o of a.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&n(o)}).observe(document,{childList:!0,subtree:!0});function r(i){const a={};return i.integrity&&(a.integrity=i.integrity),i.referrerPolicy&&(a.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?a.credentials="include":i.crossOrigin==="anonymous"?a.credentials="omit":a.credentials="same-origin",a}function n(i){if(i.ep)return;i.ep=!0;const a=r(i);fetch(i.href,a)}})();var Eu=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function Se(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var NO={exports:{}},id={},MO={exports:{}},ce={};/** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var su=Symbol.for("react.element"),j2=Symbol.for("react.portal"),T2=Symbol.for("react.fragment"),C2=Symbol.for("react.strict_mode"),$2=Symbol.for("react.profiler"),k2=Symbol.for("react.provider"),N2=Symbol.for("react.context"),M2=Symbol.for("react.forward_ref"),I2=Symbol.for("react.suspense"),R2=Symbol.for("react.memo"),D2=Symbol.for("react.lazy"),R0=Symbol.iterator;function L2(e){return e===null||typeof e!="object"?null:(e=R0&&e[R0]||e["@@iterator"],typeof e=="function"?e:null)}var IO={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},RO=Object.assign,DO={};function Go(e,t,r){this.props=e,this.context=t,this.refs=DO,this.updater=r||IO}Go.prototype.isReactComponent={};Go.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};Go.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function LO(){}LO.prototype=Go.prototype;function Ov(e,t,r){this.props=e,this.context=t,this.refs=DO,this.updater=r||IO}var _v=Ov.prototype=new LO;_v.constructor=Ov;RO(_v,Go.prototype);_v.isPureReactComponent=!0;var D0=Array.isArray,BO=Object.prototype.hasOwnProperty,Pv={current:null},FO={key:!0,ref:!0,__self:!0,__source:!0};function UO(e,t,r){var n,i={},a=null,o=null;if(t!=null)for(n in t.ref!==void 0&&(o=t.ref),t.key!==void 0&&(a=""+t.key),t)BO.call(t,n)&&!FO.hasOwnProperty(n)&&(i[n]=t[n]);var s=arguments.length-2;if(s===1)i.children=r;else if(1>>1,H=k[K];if(0>>1;Ki(Oe,U))Hei(rr,Oe)?(k[K]=rr,k[He]=U,K=He):(k[K]=Oe,k[le]=U,K=le);else if(Hei(rr,U))k[K]=rr,k[He]=U,K=He;else break e}}return F}function i(k,F){var U=k.sortIndex-F.sortIndex;return U!==0?U:k.id-F.id}if(typeof performance=="object"&&typeof performance.now=="function"){var a=performance;e.unstable_now=function(){return a.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var l=[],u=[],f=1,c=null,d=3,h=!1,p=!1,m=!1,y=typeof setTimeout=="function"?setTimeout:null,v=typeof clearTimeout=="function"?clearTimeout:null,g=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function b(k){for(var F=r(u);F!==null;){if(F.callback===null)n(u);else if(F.startTime<=k)n(u),F.sortIndex=F.expirationTime,t(l,F);else break;F=r(u)}}function w(k){if(m=!1,b(k),!p)if(r(l)!==null)p=!0,B(x);else{var F=r(u);F!==null&&z(w,F.startTime-k)}}function x(k,F){p=!1,m&&(m=!1,v(P),P=-1),h=!0;var U=d;try{for(b(F),c=r(l);c!==null&&(!(c.expirationTime>F)||k&&!N());){var K=c.callback;if(typeof K=="function"){c.callback=null,d=c.priorityLevel;var H=K(c.expirationTime<=F);F=e.unstable_now(),typeof H=="function"?c.callback=H:c===r(l)&&n(l),b(F)}else n(l);c=r(l)}if(c!==null)var J=!0;else{var le=r(u);le!==null&&z(w,le.startTime-F),J=!1}return J}finally{c=null,d=U,h=!1}}var S=!1,_=null,P=-1,A=5,C=-1;function N(){return!(e.unstable_now()-Ck||125K?(k.sortIndex=U,t(u,k),r(l)===null&&k===r(u)&&(m?(v(P),P=-1):m=!0,z(w,U-K))):(k.sortIndex=H,t(l,k),p||h||(p=!0,B(x))),k},e.unstable_shouldYield=N,e.unstable_wrapCallback=function(k){var F=d;return function(){var U=d;d=F;try{return k.apply(this,arguments)}finally{d=U}}}})(KO);qO.exports=KO;var Q2=qO.exports;/** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Y2=j,Zt=Q2;function q(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,r=1;r"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Mp=Object.prototype.hasOwnProperty,J2=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,B0={},F0={};function Z2(e){return Mp.call(F0,e)?!0:Mp.call(B0,e)?!1:J2.test(e)?F0[e]=!0:(B0[e]=!0,!1)}function eC(e,t,r,n){if(r!==null&&r.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return n?!1:r!==null?!r.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function tC(e,t,r,n){if(t===null||typeof t>"u"||eC(e,t,r,n))return!0;if(n)return!1;if(r!==null)switch(r.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function $t(e,t,r,n,i,a,o){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=n,this.attributeNamespace=i,this.mustUseProperty=r,this.propertyName=e,this.type=t,this.sanitizeURL=a,this.removeEmptyString=o}var pt={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){pt[e]=new $t(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];pt[t]=new $t(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){pt[e]=new $t(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){pt[e]=new $t(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){pt[e]=new $t(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){pt[e]=new $t(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){pt[e]=new $t(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){pt[e]=new $t(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){pt[e]=new $t(e,5,!1,e.toLowerCase(),null,!1,!1)});var Ev=/[\-:]([a-z])/g;function jv(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(Ev,jv);pt[t]=new $t(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(Ev,jv);pt[t]=new $t(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(Ev,jv);pt[t]=new $t(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){pt[e]=new $t(e,1,!1,e.toLowerCase(),null,!1,!1)});pt.xlinkHref=new $t("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){pt[e]=new $t(e,1,!1,e.toLowerCase(),null,!0,!0)});function Tv(e,t,r,n){var i=pt.hasOwnProperty(t)?pt[t]:null;(i!==null?i.type!==0:n||!(2s||i[o]!==a[s]){var l=` +`+i[o].replace(" at new "," at ");return e.displayName&&l.includes("")&&(l=l.replace("",e.displayName)),l}while(1<=o&&0<=s);break}}}finally{Ch=!1,Error.prepareStackTrace=r}return(e=e?e.displayName||e.name:"")?ks(e):""}function rC(e){switch(e.tag){case 5:return ks(e.type);case 16:return ks("Lazy");case 13:return ks("Suspense");case 19:return ks("SuspenseList");case 0:case 2:case 15:return e=$h(e.type,!1),e;case 11:return e=$h(e.type.render,!1),e;case 1:return e=$h(e.type,!0),e;default:return""}}function Lp(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Ta:return"Fragment";case ja:return"Portal";case Ip:return"Profiler";case Cv:return"StrictMode";case Rp:return"Suspense";case Dp:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case XO:return(e.displayName||"Context")+".Consumer";case GO:return(e._context.displayName||"Context")+".Provider";case $v:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case kv:return t=e.displayName||null,t!==null?t:Lp(e.type)||"Memo";case Nn:t=e._payload,e=e._init;try{return Lp(e(t))}catch{}}return null}function nC(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return Lp(t);case 8:return t===Cv?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function ci(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function YO(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function iC(e){var t=YO(e)?"checked":"value",r=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),n=""+e[t];if(!e.hasOwnProperty(t)&&typeof r<"u"&&typeof r.get=="function"&&typeof r.set=="function"){var i=r.get,a=r.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return i.call(this)},set:function(o){n=""+o,a.call(this,o)}}),Object.defineProperty(e,t,{enumerable:r.enumerable}),{getValue:function(){return n},setValue:function(o){n=""+o},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function Cu(e){e._valueTracker||(e._valueTracker=iC(e))}function JO(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var r=t.getValue(),n="";return e&&(n=YO(e)?e.checked?"true":"false":e.value),e=n,e!==r?(t.setValue(e),!0):!1}function Ac(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function Bp(e,t){var r=t.checked;return Le({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:r??e._wrapperState.initialChecked})}function z0(e,t){var r=t.defaultValue==null?"":t.defaultValue,n=t.checked!=null?t.checked:t.defaultChecked;r=ci(t.value!=null?t.value:r),e._wrapperState={initialChecked:n,initialValue:r,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function ZO(e,t){t=t.checked,t!=null&&Tv(e,"checked",t,!1)}function Fp(e,t){ZO(e,t);var r=ci(t.value),n=t.type;if(r!=null)n==="number"?(r===0&&e.value===""||e.value!=r)&&(e.value=""+r):e.value!==""+r&&(e.value=""+r);else if(n==="submit"||n==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?Up(e,t.type,r):t.hasOwnProperty("defaultValue")&&Up(e,t.type,ci(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function W0(e,t,r){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var n=t.type;if(!(n!=="submit"&&n!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,r||t===e.value||(e.value=t),e.defaultValue=t}r=e.name,r!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,r!==""&&(e.name=r)}function Up(e,t,r){(t!=="number"||Ac(e.ownerDocument)!==e)&&(r==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+r&&(e.defaultValue=""+r))}var Ns=Array.isArray;function qa(e,t,r,n){if(e=e.options,t){t={};for(var i=0;i"+t.valueOf().toString()+"",t=$u.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function rl(e,t){if(t){var r=e.firstChild;if(r&&r===e.lastChild&&r.nodeType===3){r.nodeValue=t;return}}e.textContent=t}var Bs={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},aC=["Webkit","ms","Moz","O"];Object.keys(Bs).forEach(function(e){aC.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Bs[t]=Bs[e]})});function n_(e,t,r){return t==null||typeof t=="boolean"||t===""?"":r||typeof t!="number"||t===0||Bs.hasOwnProperty(e)&&Bs[e]?(""+t).trim():t+"px"}function i_(e,t){e=e.style;for(var r in t)if(t.hasOwnProperty(r)){var n=r.indexOf("--")===0,i=n_(r,t[r],n);r==="float"&&(r="cssFloat"),n?e.setProperty(r,i):e[r]=i}}var oC=Le({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function Hp(e,t){if(t){if(oC[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(q(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(q(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(q(61))}if(t.style!=null&&typeof t.style!="object")throw Error(q(62))}}function qp(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var Kp=null;function Nv(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var Vp=null,Ka=null,Va=null;function K0(e){if(e=cu(e)){if(typeof Vp!="function")throw Error(q(280));var t=e.stateNode;t&&(t=ud(t),Vp(e.stateNode,e.type,t))}}function a_(e){Ka?Va?Va.push(e):Va=[e]:Ka=e}function o_(){if(Ka){var e=Ka,t=Va;if(Va=Ka=null,K0(e),t)for(e=0;e>>=0,e===0?32:31-(vC(e)/gC|0)|0}var ku=64,Nu=4194304;function Ms(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Cc(e,t){var r=e.pendingLanes;if(r===0)return 0;var n=0,i=e.suspendedLanes,a=e.pingedLanes,o=r&268435455;if(o!==0){var s=o&~i;s!==0?n=Ms(s):(a&=o,a!==0&&(n=Ms(a)))}else o=r&~i,o!==0?n=Ms(o):a!==0&&(n=Ms(a));if(n===0)return 0;if(t!==0&&t!==n&&!(t&i)&&(i=n&-n,a=t&-t,i>=a||i===16&&(a&4194240)!==0))return t;if(n&4&&(n|=r&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=n;0r;r++)t.push(e);return t}function lu(e,t,r){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Cr(t),e[t]=r}function SC(e,t){var r=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var n=e.eventTimes;for(e=e.expirationTimes;0=Us),tb=" ",rb=!1;function A_(e,t){switch(e){case"keyup":return QC.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function E_(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Ca=!1;function JC(e,t){switch(e){case"compositionend":return E_(t);case"keypress":return t.which!==32?null:(rb=!0,tb);case"textInput":return e=t.data,e===tb&&rb?null:e;default:return null}}function ZC(e,t){if(Ca)return e==="compositionend"||!Uv&&A_(e,t)?(e=__(),fc=Lv=Gn=null,Ca=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:r,offset:t-e};e=n}e:{for(;r;){if(r.nextSibling){r=r.nextSibling;break e}r=r.parentNode}r=void 0}r=ob(r)}}function $_(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?$_(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function k_(){for(var e=window,t=Ac();t instanceof e.HTMLIFrameElement;){try{var r=typeof t.contentWindow.location.href=="string"}catch{r=!1}if(r)e=t.contentWindow;else break;t=Ac(e.document)}return t}function zv(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function l$(e){var t=k_(),r=e.focusedElem,n=e.selectionRange;if(t!==r&&r&&r.ownerDocument&&$_(r.ownerDocument.documentElement,r)){if(n!==null&&zv(r)){if(t=n.start,e=n.end,e===void 0&&(e=t),"selectionStart"in r)r.selectionStart=t,r.selectionEnd=Math.min(e,r.value.length);else if(e=(t=r.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var i=r.textContent.length,a=Math.min(n.start,i);n=n.end===void 0?a:Math.min(n.end,i),!e.extend&&a>n&&(i=n,n=a,a=i),i=sb(r,a);var o=sb(r,n);i&&o&&(e.rangeCount!==1||e.anchorNode!==i.node||e.anchorOffset!==i.offset||e.focusNode!==o.node||e.focusOffset!==o.offset)&&(t=t.createRange(),t.setStart(i.node,i.offset),e.removeAllRanges(),a>n?(e.addRange(t),e.extend(o.node,o.offset)):(t.setEnd(o.node,o.offset),e.addRange(t)))}}for(t=[],e=r;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof r.focus=="function"&&r.focus(),r=0;r=document.documentMode,$a=null,Zp=null,Ws=null,em=!1;function lb(e,t,r){var n=r.window===r?r.document:r.nodeType===9?r:r.ownerDocument;em||$a==null||$a!==Ac(n)||(n=$a,"selectionStart"in n&&zv(n)?n={start:n.selectionStart,end:n.selectionEnd}:(n=(n.ownerDocument&&n.ownerDocument.defaultView||window).getSelection(),n={anchorNode:n.anchorNode,anchorOffset:n.anchorOffset,focusNode:n.focusNode,focusOffset:n.focusOffset}),Ws&&ll(Ws,n)||(Ws=n,n=Nc(Zp,"onSelect"),0Ma||(e.current=om[Ma],om[Ma]=null,Ma--)}function Ee(e,t){Ma++,om[Ma]=e.current,e.current=t}var fi={},St=hi(fi),Bt=hi(!1),na=fi;function mo(e,t){var r=e.type.contextTypes;if(!r)return fi;var n=e.stateNode;if(n&&n.__reactInternalMemoizedUnmaskedChildContext===t)return n.__reactInternalMemoizedMaskedChildContext;var i={},a;for(a in r)i[a]=t[a];return n&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=i),i}function Ft(e){return e=e.childContextTypes,e!=null}function Ic(){Ne(Bt),Ne(St)}function mb(e,t,r){if(St.current!==fi)throw Error(q(168));Ee(St,t),Ee(Bt,r)}function U_(e,t,r){var n=e.stateNode;if(t=t.childContextTypes,typeof n.getChildContext!="function")return r;n=n.getChildContext();for(var i in n)if(!(i in t))throw Error(q(108,nC(e)||"Unknown",i));return Le({},r,n)}function Rc(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||fi,na=St.current,Ee(St,e),Ee(Bt,Bt.current),!0}function yb(e,t,r){var n=e.stateNode;if(!n)throw Error(q(169));r?(e=U_(e,t,na),n.__reactInternalMemoizedMergedChildContext=e,Ne(Bt),Ne(St),Ee(St,e)):Ne(Bt),Ee(Bt,r)}var en=null,cd=!1,qh=!1;function z_(e){en===null?en=[e]:en.push(e)}function x$(e){cd=!0,z_(e)}function pi(){if(!qh&&en!==null){qh=!0;var e=0,t=be;try{var r=en;for(be=1;e>=o,i-=o,on=1<<32-Cr(t)+i|r<P?(A=_,_=null):A=_.sibling;var C=d(v,_,b[P],w);if(C===null){_===null&&(_=A);break}e&&_&&C.alternate===null&&t(v,_),g=a(C,g,P),S===null?x=C:S.sibling=C,S=C,_=A}if(P===b.length)return r(v,_),Me&&Ai(v,P),x;if(_===null){for(;PP?(A=_,_=null):A=_.sibling;var N=d(v,_,C.value,w);if(N===null){_===null&&(_=A);break}e&&_&&N.alternate===null&&t(v,_),g=a(N,g,P),S===null?x=N:S.sibling=N,S=N,_=A}if(C.done)return r(v,_),Me&&Ai(v,P),x;if(_===null){for(;!C.done;P++,C=b.next())C=c(v,C.value,w),C!==null&&(g=a(C,g,P),S===null?x=C:S.sibling=C,S=C);return Me&&Ai(v,P),x}for(_=n(v,_);!C.done;P++,C=b.next())C=h(_,v,P,C.value,w),C!==null&&(e&&C.alternate!==null&&_.delete(C.key===null?P:C.key),g=a(C,g,P),S===null?x=C:S.sibling=C,S=C);return e&&_.forEach(function($){return t(v,$)}),Me&&Ai(v,P),x}function y(v,g,b,w){if(typeof b=="object"&&b!==null&&b.type===Ta&&b.key===null&&(b=b.props.children),typeof b=="object"&&b!==null){switch(b.$$typeof){case Tu:e:{for(var x=b.key,S=g;S!==null;){if(S.key===x){if(x=b.type,x===Ta){if(S.tag===7){r(v,S.sibling),g=i(S,b.props.children),g.return=v,v=g;break e}}else if(S.elementType===x||typeof x=="object"&&x!==null&&x.$$typeof===Nn&&bb(x)===S.type){r(v,S.sibling),g=i(S,b.props),g.ref=gs(v,S,b),g.return=v,v=g;break e}r(v,S);break}else t(v,S);S=S.sibling}b.type===Ta?(g=Zi(b.props.children,v.mode,w,b.key),g.return=v,v=g):(w=bc(b.type,b.key,b.props,null,v.mode,w),w.ref=gs(v,g,b),w.return=v,v=w)}return o(v);case ja:e:{for(S=b.key;g!==null;){if(g.key===S)if(g.tag===4&&g.stateNode.containerInfo===b.containerInfo&&g.stateNode.implementation===b.implementation){r(v,g.sibling),g=i(g,b.children||[]),g.return=v,v=g;break e}else{r(v,g);break}else t(v,g);g=g.sibling}g=Zh(b,v.mode,w),g.return=v,v=g}return o(v);case Nn:return S=b._init,y(v,g,S(b._payload),w)}if(Ns(b))return p(v,g,b,w);if(hs(b))return m(v,g,b,w);Fu(v,b)}return typeof b=="string"&&b!==""||typeof b=="number"?(b=""+b,g!==null&&g.tag===6?(r(v,g.sibling),g=i(g,b),g.return=v,v=g):(r(v,g),g=Jh(b,v.mode,w),g.return=v,v=g),o(v)):r(v,g)}return y}var vo=K_(!0),V_=K_(!1),Bc=hi(null),Fc=null,Da=null,Kv=null;function Vv(){Kv=Da=Fc=null}function Gv(e){var t=Bc.current;Ne(Bc),e._currentValue=t}function um(e,t,r){for(;e!==null;){var n=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,n!==null&&(n.childLanes|=t)):n!==null&&(n.childLanes&t)!==t&&(n.childLanes|=t),e===r)break;e=e.return}}function Xa(e,t){Fc=e,Kv=Da=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(Dt=!0),e.firstContext=null)}function mr(e){var t=e._currentValue;if(Kv!==e)if(e={context:e,memoizedValue:t,next:null},Da===null){if(Fc===null)throw Error(q(308));Da=e,Fc.dependencies={lanes:0,firstContext:e}}else Da=Da.next=e;return t}var Ii=null;function Xv(e){Ii===null?Ii=[e]:Ii.push(e)}function G_(e,t,r,n){var i=t.interleaved;return i===null?(r.next=r,Xv(t)):(r.next=i.next,i.next=r),t.interleaved=r,gn(e,n)}function gn(e,t){e.lanes|=t;var r=e.alternate;for(r!==null&&(r.lanes|=t),r=e,e=e.return;e!==null;)e.childLanes|=t,r=e.alternate,r!==null&&(r.childLanes|=t),r=e,e=e.return;return r.tag===3?r.stateNode:null}var Mn=!1;function Qv(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function X_(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function cn(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function ri(e,t,r){var n=e.updateQueue;if(n===null)return null;if(n=n.shared,pe&2){var i=n.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),n.pending=t,gn(e,r)}return i=n.interleaved,i===null?(t.next=t,Xv(n)):(t.next=i.next,i.next=t),n.interleaved=t,gn(e,r)}function hc(e,t,r){if(t=t.updateQueue,t!==null&&(t=t.shared,(r&4194240)!==0)){var n=t.lanes;n&=e.pendingLanes,r|=n,t.lanes=r,Iv(e,r)}}function xb(e,t){var r=e.updateQueue,n=e.alternate;if(n!==null&&(n=n.updateQueue,r===n)){var i=null,a=null;if(r=r.firstBaseUpdate,r!==null){do{var o={eventTime:r.eventTime,lane:r.lane,tag:r.tag,payload:r.payload,callback:r.callback,next:null};a===null?i=a=o:a=a.next=o,r=r.next}while(r!==null);a===null?i=a=t:a=a.next=t}else i=a=t;r={baseState:n.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:n.shared,effects:n.effects},e.updateQueue=r;return}e=r.lastBaseUpdate,e===null?r.firstBaseUpdate=t:e.next=t,r.lastBaseUpdate=t}function Uc(e,t,r,n){var i=e.updateQueue;Mn=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var l=s,u=l.next;l.next=null,o===null?a=u:o.next=u,o=l;var f=e.alternate;f!==null&&(f=f.updateQueue,s=f.lastBaseUpdate,s!==o&&(s===null?f.firstBaseUpdate=u:s.next=u,f.lastBaseUpdate=l))}if(a!==null){var c=i.baseState;o=0,f=u=l=null,s=a;do{var d=s.lane,h=s.eventTime;if((n&d)===d){f!==null&&(f=f.next={eventTime:h,lane:0,tag:s.tag,payload:s.payload,callback:s.callback,next:null});e:{var p=e,m=s;switch(d=t,h=r,m.tag){case 1:if(p=m.payload,typeof p=="function"){c=p.call(h,c,d);break e}c=p;break e;case 3:p.flags=p.flags&-65537|128;case 0:if(p=m.payload,d=typeof p=="function"?p.call(h,c,d):p,d==null)break e;c=Le({},c,d);break e;case 2:Mn=!0}}s.callback!==null&&s.lane!==0&&(e.flags|=64,d=i.effects,d===null?i.effects=[s]:d.push(s))}else h={eventTime:h,lane:d,tag:s.tag,payload:s.payload,callback:s.callback,next:null},f===null?(u=f=h,l=c):f=f.next=h,o|=d;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;d=s,s=d.next,d.next=null,i.lastBaseUpdate=d,i.shared.pending=null}}while(!0);if(f===null&&(l=c),i.baseState=l,i.firstBaseUpdate=u,i.lastBaseUpdate=f,t=i.shared.interleaved,t!==null){i=t;do o|=i.lane,i=i.next;while(i!==t)}else a===null&&(i.shared.lanes=0);oa|=o,e.lanes=o,e.memoizedState=c}}function wb(e,t,r){if(e=t.effects,t.effects=null,e!==null)for(t=0;tr?r:4,e(!0);var n=Vh.transition;Vh.transition={};try{e(!1),t()}finally{be=r,Vh.transition=n}}function dP(){return yr().memoizedState}function _$(e,t,r){var n=ii(e);if(r={lane:n,action:r,hasEagerState:!1,eagerState:null,next:null},hP(e))pP(t,r);else if(r=G_(e,t,r,n),r!==null){var i=Tt();$r(r,e,n,i),mP(r,t,n)}}function P$(e,t,r){var n=ii(e),i={lane:n,action:r,hasEagerState:!1,eagerState:null,next:null};if(hP(e))pP(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,r);if(i.hasEagerState=!0,i.eagerState=s,kr(s,o)){var l=t.interleaved;l===null?(i.next=i,Xv(t)):(i.next=l.next,l.next=i),t.interleaved=i;return}}catch{}finally{}r=G_(e,t,i,n),r!==null&&(i=Tt(),$r(r,e,n,i),mP(r,t,n))}}function hP(e){var t=e.alternate;return e===De||t!==null&&t===De}function pP(e,t){Hs=Wc=!0;var r=e.pending;r===null?t.next=t:(t.next=r.next,r.next=t),e.pending=t}function mP(e,t,r){if(r&4194240){var n=t.lanes;n&=e.pendingLanes,r|=n,t.lanes=r,Iv(e,r)}}var Hc={readContext:mr,useCallback:mt,useContext:mt,useEffect:mt,useImperativeHandle:mt,useInsertionEffect:mt,useLayoutEffect:mt,useMemo:mt,useReducer:mt,useRef:mt,useState:mt,useDebugValue:mt,useDeferredValue:mt,useTransition:mt,useMutableSource:mt,useSyncExternalStore:mt,useId:mt,unstable_isNewReconciler:!1},A$={readContext:mr,useCallback:function(e,t){return Dr().memoizedState=[e,t===void 0?null:t],e},useContext:mr,useEffect:Ob,useImperativeHandle:function(e,t,r){return r=r!=null?r.concat([e]):null,mc(4194308,4,sP.bind(null,t,e),r)},useLayoutEffect:function(e,t){return mc(4194308,4,e,t)},useInsertionEffect:function(e,t){return mc(4,2,e,t)},useMemo:function(e,t){var r=Dr();return t=t===void 0?null:t,e=e(),r.memoizedState=[e,t],e},useReducer:function(e,t,r){var n=Dr();return t=r!==void 0?r(t):t,n.memoizedState=n.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},n.queue=e,e=e.dispatch=_$.bind(null,De,e),[n.memoizedState,e]},useRef:function(e){var t=Dr();return e={current:e},t.memoizedState=e},useState:Sb,useDebugValue:ig,useDeferredValue:function(e){return Dr().memoizedState=e},useTransition:function(){var e=Sb(!1),t=e[0];return e=O$.bind(null,e[1]),Dr().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,r){var n=De,i=Dr();if(Me){if(r===void 0)throw Error(q(407));r=r()}else{if(r=t(),ut===null)throw Error(q(349));aa&30||Z_(n,t,r)}i.memoizedState=r;var a={value:r,getSnapshot:t};return i.queue=a,Ob(tP.bind(null,n,a,e),[e]),n.flags|=2048,yl(9,eP.bind(null,n,a,r,t),void 0,null),r},useId:function(){var e=Dr(),t=ut.identifierPrefix;if(Me){var r=sn,n=on;r=(n&~(1<<32-Cr(n)-1)).toString(32)+r,t=":"+t+"R"+r,r=pl++,0<\/script>",e=e.removeChild(e.firstChild)):typeof n.is=="string"?e=o.createElement(r,{is:n.is}):(e=o.createElement(r),r==="select"&&(o=e,n.multiple?o.multiple=!0:n.size&&(o.size=n.size))):e=o.createElementNS(e,r),e[Ur]=t,e[fl]=n,PP(e,t,!1,!1),t.stateNode=e;e:{switch(o=qp(r,n),r){case"dialog":Ce("cancel",e),Ce("close",e),i=n;break;case"iframe":case"object":case"embed":Ce("load",e),i=n;break;case"video":case"audio":for(i=0;ixo&&(t.flags|=128,n=!0,bs(a,!1),t.lanes=4194304)}else{if(!n)if(e=zc(o),e!==null){if(t.flags|=128,n=!0,r=e.updateQueue,r!==null&&(t.updateQueue=r,t.flags|=4),bs(a,!0),a.tail===null&&a.tailMode==="hidden"&&!o.alternate&&!Me)return yt(t),null}else 2*qe()-a.renderingStartTime>xo&&r!==1073741824&&(t.flags|=128,n=!0,bs(a,!1),t.lanes=4194304);a.isBackwards?(o.sibling=t.child,t.child=o):(r=a.last,r!==null?r.sibling=o:t.child=o,a.last=o)}return a.tail!==null?(t=a.tail,a.rendering=t,a.tail=t.sibling,a.renderingStartTime=qe(),t.sibling=null,r=Re.current,Ee(Re,n?r&1|2:r&1),t):(yt(t),null);case 22:case 23:return cg(),n=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==n&&(t.flags|=8192),n&&t.mode&1?Vt&1073741824&&(yt(t),t.subtreeFlags&6&&(t.flags|=8192)):yt(t),null;case 24:return null;case 25:return null}throw Error(q(156,t.tag))}function M$(e,t){switch(Hv(t),t.tag){case 1:return Ft(t.type)&&Ic(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return go(),Ne(Bt),Ne(St),Zv(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return Jv(t),null;case 13:if(Ne(Re),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(q(340));yo()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return Ne(Re),null;case 4:return go(),null;case 10:return Gv(t.type._context),null;case 22:case 23:return cg(),null;case 24:return null;default:return null}}var zu=!1,bt=!1,I$=typeof WeakSet=="function"?WeakSet:Set,Q=null;function La(e,t){var r=e.ref;if(r!==null)if(typeof r=="function")try{r(null)}catch(n){Ue(e,t,n)}else r.current=null}function gm(e,t,r){try{r()}catch(n){Ue(e,t,n)}}var Mb=!1;function R$(e,t){if(tm=$c,e=k_(),zv(e)){if("selectionStart"in e)var r={start:e.selectionStart,end:e.selectionEnd};else e:{r=(r=e.ownerDocument)&&r.defaultView||window;var n=r.getSelection&&r.getSelection();if(n&&n.rangeCount!==0){r=n.anchorNode;var i=n.anchorOffset,a=n.focusNode;n=n.focusOffset;try{r.nodeType,a.nodeType}catch{r=null;break e}var o=0,s=-1,l=-1,u=0,f=0,c=e,d=null;t:for(;;){for(var h;c!==r||i!==0&&c.nodeType!==3||(s=o+i),c!==a||n!==0&&c.nodeType!==3||(l=o+n),c.nodeType===3&&(o+=c.nodeValue.length),(h=c.firstChild)!==null;)d=c,c=h;for(;;){if(c===e)break t;if(d===r&&++u===i&&(s=o),d===a&&++f===n&&(l=o),(h=c.nextSibling)!==null)break;c=d,d=c.parentNode}c=h}r=s===-1||l===-1?null:{start:s,end:l}}else r=null}r=r||{start:0,end:0}}else r=null;for(rm={focusedElem:e,selectionRange:r},$c=!1,Q=t;Q!==null;)if(t=Q,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,Q=e;else for(;Q!==null;){t=Q;try{var p=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(p!==null){var m=p.memoizedProps,y=p.memoizedState,v=t.stateNode,g=v.getSnapshotBeforeUpdate(t.elementType===t.type?m:Sr(t.type,m),y);v.__reactInternalSnapshotBeforeUpdate=g}break;case 3:var b=t.stateNode.containerInfo;b.nodeType===1?b.textContent="":b.nodeType===9&&b.documentElement&&b.removeChild(b.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(q(163))}}catch(w){Ue(t,t.return,w)}if(e=t.sibling,e!==null){e.return=t.return,Q=e;break}Q=t.return}return p=Mb,Mb=!1,p}function qs(e,t,r){var n=t.updateQueue;if(n=n!==null?n.lastEffect:null,n!==null){var i=n=n.next;do{if((i.tag&e)===e){var a=i.destroy;i.destroy=void 0,a!==void 0&&gm(t,r,a)}i=i.next}while(i!==n)}}function hd(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var r=t=t.next;do{if((r.tag&e)===e){var n=r.create;r.destroy=n()}r=r.next}while(r!==t)}}function bm(e){var t=e.ref;if(t!==null){var r=e.stateNode;switch(e.tag){case 5:e=r;break;default:e=r}typeof t=="function"?t(e):t.current=e}}function jP(e){var t=e.alternate;t!==null&&(e.alternate=null,jP(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Ur],delete t[fl],delete t[am],delete t[g$],delete t[b$])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function TP(e){return e.tag===5||e.tag===3||e.tag===4}function Ib(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||TP(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function xm(e,t,r){var n=e.tag;if(n===5||n===6)e=e.stateNode,t?r.nodeType===8?r.parentNode.insertBefore(e,t):r.insertBefore(e,t):(r.nodeType===8?(t=r.parentNode,t.insertBefore(e,r)):(t=r,t.appendChild(e)),r=r._reactRootContainer,r!=null||t.onclick!==null||(t.onclick=Mc));else if(n!==4&&(e=e.child,e!==null))for(xm(e,t,r),e=e.sibling;e!==null;)xm(e,t,r),e=e.sibling}function wm(e,t,r){var n=e.tag;if(n===5||n===6)e=e.stateNode,t?r.insertBefore(e,t):r.appendChild(e);else if(n!==4&&(e=e.child,e!==null))for(wm(e,t,r),e=e.sibling;e!==null;)wm(e,t,r),e=e.sibling}var dt=null,Pr=!1;function $n(e,t,r){for(r=r.child;r!==null;)CP(e,t,r),r=r.sibling}function CP(e,t,r){if(Hr&&typeof Hr.onCommitFiberUnmount=="function")try{Hr.onCommitFiberUnmount(ad,r)}catch{}switch(r.tag){case 5:bt||La(r,t);case 6:var n=dt,i=Pr;dt=null,$n(e,t,r),dt=n,Pr=i,dt!==null&&(Pr?(e=dt,r=r.stateNode,e.nodeType===8?e.parentNode.removeChild(r):e.removeChild(r)):dt.removeChild(r.stateNode));break;case 18:dt!==null&&(Pr?(e=dt,r=r.stateNode,e.nodeType===8?Hh(e.parentNode,r):e.nodeType===1&&Hh(e,r),ol(e)):Hh(dt,r.stateNode));break;case 4:n=dt,i=Pr,dt=r.stateNode.containerInfo,Pr=!0,$n(e,t,r),dt=n,Pr=i;break;case 0:case 11:case 14:case 15:if(!bt&&(n=r.updateQueue,n!==null&&(n=n.lastEffect,n!==null))){i=n=n.next;do{var a=i,o=a.destroy;a=a.tag,o!==void 0&&(a&2||a&4)&&gm(r,t,o),i=i.next}while(i!==n)}$n(e,t,r);break;case 1:if(!bt&&(La(r,t),n=r.stateNode,typeof n.componentWillUnmount=="function"))try{n.props=r.memoizedProps,n.state=r.memoizedState,n.componentWillUnmount()}catch(s){Ue(r,t,s)}$n(e,t,r);break;case 21:$n(e,t,r);break;case 22:r.mode&1?(bt=(n=bt)||r.memoizedState!==null,$n(e,t,r),bt=n):$n(e,t,r);break;default:$n(e,t,r)}}function Rb(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var r=e.stateNode;r===null&&(r=e.stateNode=new I$),t.forEach(function(n){var i=q$.bind(null,e,n);r.has(n)||(r.add(n),n.then(i,i))})}}function xr(e,t){var r=t.deletions;if(r!==null)for(var n=0;ni&&(i=o),n&=~a}if(n=i,n=qe()-n,n=(120>n?120:480>n?480:1080>n?1080:1920>n?1920:3e3>n?3e3:4320>n?4320:1960*L$(n/1960))-n,10e?16:e,Xn===null)var n=!1;else{if(e=Xn,Xn=null,Vc=0,pe&6)throw Error(q(331));var i=pe;for(pe|=4,Q=e.current;Q!==null;){var a=Q,o=a.child;if(Q.flags&16){var s=a.deletions;if(s!==null){for(var l=0;lqe()-lg?Ji(e,0):sg|=r),Ut(e,t)}function LP(e,t){t===0&&(e.mode&1?(t=Nu,Nu<<=1,!(Nu&130023424)&&(Nu=4194304)):t=1);var r=Tt();e=gn(e,t),e!==null&&(lu(e,t,r),Ut(e,r))}function H$(e){var t=e.memoizedState,r=0;t!==null&&(r=t.retryLane),LP(e,r)}function q$(e,t){var r=0;switch(e.tag){case 13:var n=e.stateNode,i=e.memoizedState;i!==null&&(r=i.retryLane);break;case 19:n=e.stateNode;break;default:throw Error(q(314))}n!==null&&n.delete(t),LP(e,r)}var BP;BP=function(e,t,r){if(e!==null)if(e.memoizedProps!==t.pendingProps||Bt.current)Dt=!0;else{if(!(e.lanes&r)&&!(t.flags&128))return Dt=!1,k$(e,t,r);Dt=!!(e.flags&131072)}else Dt=!1,Me&&t.flags&1048576&&W_(t,Lc,t.index);switch(t.lanes=0,t.tag){case 2:var n=t.type;yc(e,t),e=t.pendingProps;var i=mo(t,St.current);Xa(t,r),i=tg(null,t,n,e,i,r);var a=rg();return t.flags|=1,typeof i=="object"&&i!==null&&typeof i.render=="function"&&i.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,Ft(n)?(a=!0,Rc(t)):a=!1,t.memoizedState=i.state!==null&&i.state!==void 0?i.state:null,Qv(t),i.updater=dd,t.stateNode=i,i._reactInternals=t,fm(t,n,e,r),t=pm(null,t,n,!0,a,r)):(t.tag=0,Me&&a&&Wv(t),_t(null,t,i,r),t=t.child),t;case 16:n=t.elementType;e:{switch(yc(e,t),e=t.pendingProps,i=n._init,n=i(n._payload),t.type=n,i=t.tag=V$(n),e=Sr(n,e),i){case 0:t=hm(null,t,n,e,r);break e;case 1:t=$b(null,t,n,e,r);break e;case 11:t=Tb(null,t,n,e,r);break e;case 14:t=Cb(null,t,n,Sr(n.type,e),r);break e}throw Error(q(306,n,""))}return t;case 0:return n=t.type,i=t.pendingProps,i=t.elementType===n?i:Sr(n,i),hm(e,t,n,i,r);case 1:return n=t.type,i=t.pendingProps,i=t.elementType===n?i:Sr(n,i),$b(e,t,n,i,r);case 3:e:{if(SP(t),e===null)throw Error(q(387));n=t.pendingProps,a=t.memoizedState,i=a.element,X_(e,t),Uc(t,n,null,r);var o=t.memoizedState;if(n=o.element,a.isDehydrated)if(a={element:n,isDehydrated:!1,cache:o.cache,pendingSuspenseBoundaries:o.pendingSuspenseBoundaries,transitions:o.transitions},t.updateQueue.baseState=a,t.memoizedState=a,t.flags&256){i=bo(Error(q(423)),t),t=kb(e,t,n,r,i);break e}else if(n!==i){i=bo(Error(q(424)),t),t=kb(e,t,n,r,i);break e}else for(Qt=ti(t.stateNode.containerInfo.firstChild),Yt=t,Me=!0,jr=null,r=V_(t,null,n,r),t.child=r;r;)r.flags=r.flags&-3|4096,r=r.sibling;else{if(yo(),n===i){t=bn(e,t,r);break e}_t(e,t,n,r)}t=t.child}return t;case 5:return Q_(t),e===null&&lm(t),n=t.type,i=t.pendingProps,a=e!==null?e.memoizedProps:null,o=i.children,nm(n,i)?o=null:a!==null&&nm(n,a)&&(t.flags|=32),wP(e,t),_t(e,t,o,r),t.child;case 6:return e===null&&lm(t),null;case 13:return OP(e,t,r);case 4:return Yv(t,t.stateNode.containerInfo),n=t.pendingProps,e===null?t.child=vo(t,null,n,r):_t(e,t,n,r),t.child;case 11:return n=t.type,i=t.pendingProps,i=t.elementType===n?i:Sr(n,i),Tb(e,t,n,i,r);case 7:return _t(e,t,t.pendingProps,r),t.child;case 8:return _t(e,t,t.pendingProps.children,r),t.child;case 12:return _t(e,t,t.pendingProps.children,r),t.child;case 10:e:{if(n=t.type._context,i=t.pendingProps,a=t.memoizedProps,o=i.value,Ee(Bc,n._currentValue),n._currentValue=o,a!==null)if(kr(a.value,o)){if(a.children===i.children&&!Bt.current){t=bn(e,t,r);break e}}else for(a=t.child,a!==null&&(a.return=t);a!==null;){var s=a.dependencies;if(s!==null){o=a.child;for(var l=s.firstContext;l!==null;){if(l.context===n){if(a.tag===1){l=cn(-1,r&-r),l.tag=2;var u=a.updateQueue;if(u!==null){u=u.shared;var f=u.pending;f===null?l.next=l:(l.next=f.next,f.next=l),u.pending=l}}a.lanes|=r,l=a.alternate,l!==null&&(l.lanes|=r),um(a.return,r,t),s.lanes|=r;break}l=l.next}}else if(a.tag===10)o=a.type===t.type?null:a.child;else if(a.tag===18){if(o=a.return,o===null)throw Error(q(341));o.lanes|=r,s=o.alternate,s!==null&&(s.lanes|=r),um(o,r,t),o=a.sibling}else o=a.child;if(o!==null)o.return=a;else for(o=a;o!==null;){if(o===t){o=null;break}if(a=o.sibling,a!==null){a.return=o.return,o=a;break}o=o.return}a=o}_t(e,t,i.children,r),t=t.child}return t;case 9:return i=t.type,n=t.pendingProps.children,Xa(t,r),i=mr(i),n=n(i),t.flags|=1,_t(e,t,n,r),t.child;case 14:return n=t.type,i=Sr(n,t.pendingProps),i=Sr(n.type,i),Cb(e,t,n,i,r);case 15:return bP(e,t,t.type,t.pendingProps,r);case 17:return n=t.type,i=t.pendingProps,i=t.elementType===n?i:Sr(n,i),yc(e,t),t.tag=1,Ft(n)?(e=!0,Rc(t)):e=!1,Xa(t,r),yP(t,n,i),fm(t,n,i,r),pm(null,t,n,!0,e,r);case 19:return _P(e,t,r);case 22:return xP(e,t,r)}throw Error(q(156,t.tag))};function FP(e,t){return h_(e,t)}function K$(e,t,r,n){this.tag=e,this.key=r,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=n,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function fr(e,t,r,n){return new K$(e,t,r,n)}function dg(e){return e=e.prototype,!(!e||!e.isReactComponent)}function V$(e){if(typeof e=="function")return dg(e)?1:0;if(e!=null){if(e=e.$$typeof,e===$v)return 11;if(e===kv)return 14}return 2}function ai(e,t){var r=e.alternate;return r===null?(r=fr(e.tag,t,e.key,e.mode),r.elementType=e.elementType,r.type=e.type,r.stateNode=e.stateNode,r.alternate=e,e.alternate=r):(r.pendingProps=t,r.type=e.type,r.flags=0,r.subtreeFlags=0,r.deletions=null),r.flags=e.flags&14680064,r.childLanes=e.childLanes,r.lanes=e.lanes,r.child=e.child,r.memoizedProps=e.memoizedProps,r.memoizedState=e.memoizedState,r.updateQueue=e.updateQueue,t=e.dependencies,r.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},r.sibling=e.sibling,r.index=e.index,r.ref=e.ref,r}function bc(e,t,r,n,i,a){var o=2;if(n=e,typeof e=="function")dg(e)&&(o=1);else if(typeof e=="string")o=5;else e:switch(e){case Ta:return Zi(r.children,i,a,t);case Cv:o=8,i|=8;break;case Ip:return e=fr(12,r,t,i|2),e.elementType=Ip,e.lanes=a,e;case Rp:return e=fr(13,r,t,i),e.elementType=Rp,e.lanes=a,e;case Dp:return e=fr(19,r,t,i),e.elementType=Dp,e.lanes=a,e;case QO:return md(r,i,a,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case GO:o=10;break e;case XO:o=9;break e;case $v:o=11;break e;case kv:o=14;break e;case Nn:o=16,n=null;break e}throw Error(q(130,e==null?e:typeof e,""))}return t=fr(o,r,t,i),t.elementType=e,t.type=n,t.lanes=a,t}function Zi(e,t,r,n){return e=fr(7,e,n,t),e.lanes=r,e}function md(e,t,r,n){return e=fr(22,e,n,t),e.elementType=QO,e.lanes=r,e.stateNode={isHidden:!1},e}function Jh(e,t,r){return e=fr(6,e,null,t),e.lanes=r,e}function Zh(e,t,r){return t=fr(4,e.children!==null?e.children:[],e.key,t),t.lanes=r,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function G$(e,t,r,n,i){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Nh(0),this.expirationTimes=Nh(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Nh(0),this.identifierPrefix=n,this.onRecoverableError=i,this.mutableSourceEagerHydrationData=null}function hg(e,t,r,n,i,a,o,s,l){return e=new G$(e,t,r,s,l),t===1?(t=1,a===!0&&(t|=8)):t=0,a=fr(3,null,null,t),e.current=a,a.stateNode=e,a.memoizedState={element:n,isDehydrated:r,cache:null,transitions:null,pendingSuspenseBoundaries:null},Qv(a),e}function X$(e,t,r){var n=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(HP)}catch(e){console.error(e)}}HP(),HO.exports=er;var ek=HO.exports,Hb=ek;Np.createRoot=Hb.createRoot,Np.hydrateRoot=Hb.hydrateRoot;var Yo=class{constructor(){this.listeners=new Set,this.subscribe=this.subscribe.bind(this)}subscribe(e){return this.listeners.add(e),this.onSubscribe(),()=>{this.listeners.delete(e),this.onUnsubscribe()}}hasListeners(){return this.listeners.size>0}onSubscribe(){}onUnsubscribe(){}},tk={setTimeout:(e,t)=>setTimeout(e,t),clearTimeout:e=>clearTimeout(e),setInterval:(e,t)=>setInterval(e,t),clearInterval:e=>clearInterval(e)},Bn,Sv,SO,rk=(SO=class{constructor(){Z(this,Bn,tk);Z(this,Sv,!1)}setTimeoutProvider(e){G(this,Bn,e)}setTimeout(e,t){return E(this,Bn).setTimeout(e,t)}clearTimeout(e){E(this,Bn).clearTimeout(e)}setInterval(e,t){return E(this,Bn).setInterval(e,t)}clearInterval(e){E(this,Bn).clearInterval(e)}},Bn=new WeakMap,Sv=new WeakMap,SO),Di=new rk;function nk(e){setTimeout(e,0)}var la=typeof window>"u"||"Deno"in globalThis;function Pt(){}function ik(e,t){return typeof e=="function"?e(t):e}function Am(e){return typeof e=="number"&&e>=0&&e!==1/0}function qP(e,t){return Math.max(e+(t||0)-Date.now(),0)}function oi(e,t){return typeof e=="function"?e(t):e}function sr(e,t){return typeof e=="function"?e(t):e}function qb(e,t){const{type:r="all",exact:n,fetchStatus:i,predicate:a,queryKey:o,stale:s}=e;if(o){if(n){if(t.queryHash!==vg(o,t.options))return!1}else if(!gl(t.queryKey,o))return!1}if(r!=="all"){const l=t.isActive();if(r==="active"&&!l||r==="inactive"&&l)return!1}return!(typeof s=="boolean"&&t.isStale()!==s||i&&i!==t.state.fetchStatus||a&&!a(t))}function Kb(e,t){const{exact:r,status:n,predicate:i,mutationKey:a}=e;if(a){if(!t.options.mutationKey)return!1;if(r){if(ua(t.options.mutationKey)!==ua(a))return!1}else if(!gl(t.options.mutationKey,a))return!1}return!(n&&t.state.status!==n||i&&!i(t))}function vg(e,t){return((t==null?void 0:t.queryKeyHashFn)||ua)(e)}function ua(e){return JSON.stringify(e,(t,r)=>Em(r)?Object.keys(r).sort().reduce((n,i)=>(n[i]=r[i],n),{}):r)}function gl(e,t){return e===t?!0:typeof e!=typeof t?!1:e&&t&&typeof e=="object"&&typeof t=="object"?Object.keys(t).every(r=>gl(e[r],t[r])):!1}var ak=Object.prototype.hasOwnProperty;function KP(e,t,r=0){if(e===t)return e;if(r>500)return t;const n=Vb(e)&&Vb(t);if(!n&&!(Em(e)&&Em(t)))return t;const a=(n?e:Object.keys(e)).length,o=n?t:Object.keys(t),s=o.length,l=n?new Array(s):{};let u=0;for(let f=0;f{Di.setTimeout(t,e)})}function jm(e,t,r){return typeof r.structuralSharing=="function"?r.structuralSharing(e,t):r.structuralSharing!==!1?KP(e,t):t}function sk(e,t,r=0){const n=[...e,t];return r&&n.length>r?n.slice(1):n}function lk(e,t,r=0){const n=[t,...e];return r&&n.length>r?n.slice(0,-1):n}var gg=Symbol();function VP(e,t){return!e.queryFn&&(t!=null&&t.initialPromise)?()=>t.initialPromise:!e.queryFn||e.queryFn===gg?()=>Promise.reject(new Error(`Missing queryFn: '${e.queryHash}'`)):e.queryFn}function bg(e,t){return typeof e=="function"?e(...t):!!e}function uk(e,t,r){let n=!1,i;return Object.defineProperty(e,"signal",{enumerable:!0,get:()=>(i??(i=t()),n||(n=!0,i.aborted?r():i.addEventListener("abort",r,{once:!0})),i)}),e}var Wi,Fn,to,OO,ck=(OO=class extends Yo{constructor(){super();Z(this,Wi);Z(this,Fn);Z(this,to);G(this,to,t=>{if(!la&&window.addEventListener){const r=()=>t();return window.addEventListener("visibilitychange",r,!1),()=>{window.removeEventListener("visibilitychange",r)}}})}onSubscribe(){E(this,Fn)||this.setEventListener(E(this,to))}onUnsubscribe(){var t;this.hasListeners()||((t=E(this,Fn))==null||t.call(this),G(this,Fn,void 0))}setEventListener(t){var r;G(this,to,t),(r=E(this,Fn))==null||r.call(this),G(this,Fn,t(n=>{typeof n=="boolean"?this.setFocused(n):this.onFocus()}))}setFocused(t){E(this,Wi)!==t&&(G(this,Wi,t),this.onFocus())}onFocus(){const t=this.isFocused();this.listeners.forEach(r=>{r(t)})}isFocused(){var t;return typeof E(this,Wi)=="boolean"?E(this,Wi):((t=globalThis.document)==null?void 0:t.visibilityState)!=="hidden"}},Wi=new WeakMap,Fn=new WeakMap,to=new WeakMap,OO),xg=new ck;function Tm(){let e,t;const r=new Promise((i,a)=>{e=i,t=a});r.status="pending",r.catch(()=>{});function n(i){Object.assign(r,i),delete r.resolve,delete r.reject}return r.resolve=i=>{n({status:"fulfilled",value:i}),e(i)},r.reject=i=>{n({status:"rejected",reason:i}),t(i)},r}var fk=nk;function dk(){let e=[],t=0,r=s=>{s()},n=s=>{s()},i=fk;const a=s=>{t?e.push(s):i(()=>{r(s)})},o=()=>{const s=e;e=[],s.length&&i(()=>{n(()=>{s.forEach(l=>{r(l)})})})};return{batch:s=>{let l;t++;try{l=s()}finally{t--,t||o()}return l},batchCalls:s=>(...l)=>{a(()=>{s(...l)})},schedule:a,setNotifyFunction:s=>{r=s},setBatchNotifyFunction:s=>{n=s},setScheduler:s=>{i=s}}}var tt=dk(),ro,Un,no,_O,hk=(_O=class extends Yo{constructor(){super();Z(this,ro,!0);Z(this,Un);Z(this,no);G(this,no,t=>{if(!la&&window.addEventListener){const r=()=>t(!0),n=()=>t(!1);return window.addEventListener("online",r,!1),window.addEventListener("offline",n,!1),()=>{window.removeEventListener("online",r),window.removeEventListener("offline",n)}}})}onSubscribe(){E(this,Un)||this.setEventListener(E(this,no))}onUnsubscribe(){var t;this.hasListeners()||((t=E(this,Un))==null||t.call(this),G(this,Un,void 0))}setEventListener(t){var r;G(this,no,t),(r=E(this,Un))==null||r.call(this),G(this,Un,t(this.setOnline.bind(this)))}setOnline(t){E(this,ro)!==t&&(G(this,ro,t),this.listeners.forEach(n=>{n(t)}))}isOnline(){return E(this,ro)}},ro=new WeakMap,Un=new WeakMap,no=new WeakMap,_O),Yc=new hk;function pk(e){return Math.min(1e3*2**e,3e4)}function GP(e){return(e??"online")==="online"?Yc.isOnline():!0}var Cm=class extends Error{constructor(e){super("CancelledError"),this.revert=e==null?void 0:e.revert,this.silent=e==null?void 0:e.silent}};function XP(e){let t=!1,r=0,n;const i=Tm(),a=()=>i.status!=="pending",o=m=>{var y;if(!a()){const v=new Cm(m);d(v),(y=e.onCancel)==null||y.call(e,v)}},s=()=>{t=!0},l=()=>{t=!1},u=()=>xg.isFocused()&&(e.networkMode==="always"||Yc.isOnline())&&e.canRun(),f=()=>GP(e.networkMode)&&e.canRun(),c=m=>{a()||(n==null||n(),i.resolve(m))},d=m=>{a()||(n==null||n(),i.reject(m))},h=()=>new Promise(m=>{var y;n=v=>{(a()||u())&&m(v)},(y=e.onPause)==null||y.call(e)}).then(()=>{var m;n=void 0,a()||(m=e.onContinue)==null||m.call(e)}),p=()=>{if(a())return;let m;const y=r===0?e.initialPromise:void 0;try{m=y??e.fn()}catch(v){m=Promise.reject(v)}Promise.resolve(m).then(c).catch(v=>{var S;if(a())return;const g=e.retry??(la?0:3),b=e.retryDelay??pk,w=typeof b=="function"?b(r,v):b,x=g===!0||typeof g=="number"&&ru()?void 0:h()).then(()=>{t?d(v):p()})})};return{promise:i,status:()=>i.status,cancel:o,continue:()=>(n==null||n(),i),cancelRetry:s,continueRetry:l,canStart:f,start:()=>(f()?p():h().then(p),i)}}var Hi,PO,QP=(PO=class{constructor(){Z(this,Hi)}destroy(){this.clearGcTimeout()}scheduleGc(){this.clearGcTimeout(),Am(this.gcTime)&&G(this,Hi,Di.setTimeout(()=>{this.optionalRemove()},this.gcTime))}updateGcTime(e){this.gcTime=Math.max(this.gcTime||0,e??(la?1/0:5*60*1e3))}clearGcTimeout(){E(this,Hi)&&(Di.clearTimeout(E(this,Hi)),G(this,Hi,void 0))}},Hi=new WeakMap,PO),qi,io,or,Ki,at,ru,Vi,Or,Yr,AO,mk=(AO=class extends QP{constructor(t){super();Z(this,Or);Z(this,qi);Z(this,io);Z(this,or);Z(this,Ki);Z(this,at);Z(this,ru);Z(this,Vi);G(this,Vi,!1),G(this,ru,t.defaultOptions),this.setOptions(t.options),this.observers=[],G(this,Ki,t.client),G(this,or,E(this,Ki).getQueryCache()),this.queryKey=t.queryKey,this.queryHash=t.queryHash,G(this,qi,Qb(this.options)),this.state=t.state??E(this,qi),this.scheduleGc()}get meta(){return this.options.meta}get promise(){var t;return(t=E(this,at))==null?void 0:t.promise}setOptions(t){if(this.options={...E(this,ru),...t},this.updateGcTime(this.options.gcTime),this.state&&this.state.data===void 0){const r=Qb(this.options);r.data!==void 0&&(this.setState(Xb(r.data,r.dataUpdatedAt)),G(this,qi,r))}}optionalRemove(){!this.observers.length&&this.state.fetchStatus==="idle"&&E(this,or).remove(this)}setData(t,r){const n=jm(this.state.data,t,this.options);return ae(this,Or,Yr).call(this,{data:n,type:"success",dataUpdatedAt:r==null?void 0:r.updatedAt,manual:r==null?void 0:r.manual}),n}setState(t,r){ae(this,Or,Yr).call(this,{type:"setState",state:t,setStateOptions:r})}cancel(t){var n,i;const r=(n=E(this,at))==null?void 0:n.promise;return(i=E(this,at))==null||i.cancel(t),r?r.then(Pt).catch(Pt):Promise.resolve()}destroy(){super.destroy(),this.cancel({silent:!0})}reset(){this.destroy(),this.setState(E(this,qi))}isActive(){return this.observers.some(t=>sr(t.options.enabled,this)!==!1)}isDisabled(){return this.getObserversCount()>0?!this.isActive():this.options.queryFn===gg||this.state.dataUpdateCount+this.state.errorUpdateCount===0}isStatic(){return this.getObserversCount()>0?this.observers.some(t=>oi(t.options.staleTime,this)==="static"):!1}isStale(){return this.getObserversCount()>0?this.observers.some(t=>t.getCurrentResult().isStale):this.state.data===void 0||this.state.isInvalidated}isStaleByTime(t=0){return this.state.data===void 0?!0:t==="static"?!1:this.state.isInvalidated?!0:!qP(this.state.dataUpdatedAt,t)}onFocus(){var r;const t=this.observers.find(n=>n.shouldFetchOnWindowFocus());t==null||t.refetch({cancelRefetch:!1}),(r=E(this,at))==null||r.continue()}onOnline(){var r;const t=this.observers.find(n=>n.shouldFetchOnReconnect());t==null||t.refetch({cancelRefetch:!1}),(r=E(this,at))==null||r.continue()}addObserver(t){this.observers.includes(t)||(this.observers.push(t),this.clearGcTimeout(),E(this,or).notify({type:"observerAdded",query:this,observer:t}))}removeObserver(t){this.observers.includes(t)&&(this.observers=this.observers.filter(r=>r!==t),this.observers.length||(E(this,at)&&(E(this,Vi)?E(this,at).cancel({revert:!0}):E(this,at).cancelRetry()),this.scheduleGc()),E(this,or).notify({type:"observerRemoved",query:this,observer:t}))}getObserversCount(){return this.observers.length}invalidate(){this.state.isInvalidated||ae(this,Or,Yr).call(this,{type:"invalidate"})}async fetch(t,r){var l,u,f,c,d,h,p,m,y,v,g,b;if(this.state.fetchStatus!=="idle"&&((l=E(this,at))==null?void 0:l.status())!=="rejected"){if(this.state.data!==void 0&&(r!=null&&r.cancelRefetch))this.cancel({silent:!0});else if(E(this,at))return E(this,at).continueRetry(),E(this,at).promise}if(t&&this.setOptions(t),!this.options.queryFn){const w=this.observers.find(x=>x.options.queryFn);w&&this.setOptions(w.options)}const n=new AbortController,i=w=>{Object.defineProperty(w,"signal",{enumerable:!0,get:()=>(G(this,Vi,!0),n.signal)})},a=()=>{const w=VP(this.options,r),S=(()=>{const _={client:E(this,Ki),queryKey:this.queryKey,meta:this.meta};return i(_),_})();return G(this,Vi,!1),this.options.persister?this.options.persister(w,S,this):w(S)},s=(()=>{const w={fetchOptions:r,options:this.options,queryKey:this.queryKey,client:E(this,Ki),state:this.state,fetchFn:a};return i(w),w})();(u=this.options.behavior)==null||u.onFetch(s,this),G(this,io,this.state),(this.state.fetchStatus==="idle"||this.state.fetchMeta!==((f=s.fetchOptions)==null?void 0:f.meta))&&ae(this,Or,Yr).call(this,{type:"fetch",meta:(c=s.fetchOptions)==null?void 0:c.meta}),G(this,at,XP({initialPromise:r==null?void 0:r.initialPromise,fn:s.fetchFn,onCancel:w=>{w instanceof Cm&&w.revert&&this.setState({...E(this,io),fetchStatus:"idle"}),n.abort()},onFail:(w,x)=>{ae(this,Or,Yr).call(this,{type:"failed",failureCount:w,error:x})},onPause:()=>{ae(this,Or,Yr).call(this,{type:"pause"})},onContinue:()=>{ae(this,Or,Yr).call(this,{type:"continue"})},retry:s.options.retry,retryDelay:s.options.retryDelay,networkMode:s.options.networkMode,canRun:()=>!0}));try{const w=await E(this,at).start();if(w===void 0)throw new Error(`${this.queryHash} data is undefined`);return this.setData(w),(h=(d=E(this,or).config).onSuccess)==null||h.call(d,w,this),(m=(p=E(this,or).config).onSettled)==null||m.call(p,w,this.state.error,this),w}catch(w){if(w instanceof Cm){if(w.silent)return E(this,at).promise;if(w.revert){if(this.state.data===void 0)throw w;return this.state.data}}throw ae(this,Or,Yr).call(this,{type:"error",error:w}),(v=(y=E(this,or).config).onError)==null||v.call(y,w,this),(b=(g=E(this,or).config).onSettled)==null||b.call(g,this.state.data,w,this),w}finally{this.scheduleGc()}}},qi=new WeakMap,io=new WeakMap,or=new WeakMap,Ki=new WeakMap,at=new WeakMap,ru=new WeakMap,Vi=new WeakMap,Or=new WeakSet,Yr=function(t){const r=n=>{switch(t.type){case"failed":return{...n,fetchFailureCount:t.failureCount,fetchFailureReason:t.error};case"pause":return{...n,fetchStatus:"paused"};case"continue":return{...n,fetchStatus:"fetching"};case"fetch":return{...n,...YP(n.data,this.options),fetchMeta:t.meta??null};case"success":const i={...n,...Xb(t.data,t.dataUpdatedAt),dataUpdateCount:n.dataUpdateCount+1,...!t.manual&&{fetchStatus:"idle",fetchFailureCount:0,fetchFailureReason:null}};return G(this,io,t.manual?i:void 0),i;case"error":const a=t.error;return{...n,error:a,errorUpdateCount:n.errorUpdateCount+1,errorUpdatedAt:Date.now(),fetchFailureCount:n.fetchFailureCount+1,fetchFailureReason:a,fetchStatus:"idle",status:"error",isInvalidated:!0};case"invalidate":return{...n,isInvalidated:!0};case"setState":return{...n,...t.state}}};this.state=r(this.state),tt.batch(()=>{this.observers.forEach(n=>{n.onQueryUpdate()}),E(this,or).notify({query:this,type:"updated",action:t})})},AO);function YP(e,t){return{fetchFailureCount:0,fetchFailureReason:null,fetchStatus:GP(t.networkMode)?"fetching":"paused",...e===void 0&&{error:null,status:"pending"}}}function Xb(e,t){return{data:e,dataUpdatedAt:t??Date.now(),error:null,isInvalidated:!1,status:"success"}}function Qb(e){const t=typeof e.initialData=="function"?e.initialData():e.initialData,r=t!==void 0,n=r?typeof e.initialDataUpdatedAt=="function"?e.initialDataUpdatedAt():e.initialDataUpdatedAt:0;return{data:t,dataUpdateCount:0,dataUpdatedAt:r?n??Date.now():0,error:null,errorUpdateCount:0,errorUpdatedAt:0,fetchFailureCount:0,fetchFailureReason:null,fetchMeta:null,isInvalidated:!1,status:r?"success":"pending",fetchStatus:"idle"}}var Mt,fe,nu,Ot,Gi,ao,tn,zn,iu,oo,so,Xi,Qi,Wn,lo,ve,Rs,$m,km,Nm,Mm,Im,Rm,Dm,JP,EO,yk=(EO=class extends Yo{constructor(t,r){super();Z(this,ve);Z(this,Mt);Z(this,fe);Z(this,nu);Z(this,Ot);Z(this,Gi);Z(this,ao);Z(this,tn);Z(this,zn);Z(this,iu);Z(this,oo);Z(this,so);Z(this,Xi);Z(this,Qi);Z(this,Wn);Z(this,lo,new Set);this.options=r,G(this,Mt,t),G(this,zn,null),G(this,tn,Tm()),this.bindMethods(),this.setOptions(r)}bindMethods(){this.refetch=this.refetch.bind(this)}onSubscribe(){this.listeners.size===1&&(E(this,fe).addObserver(this),Yb(E(this,fe),this.options)?ae(this,ve,Rs).call(this):this.updateResult(),ae(this,ve,Mm).call(this))}onUnsubscribe(){this.hasListeners()||this.destroy()}shouldFetchOnReconnect(){return Lm(E(this,fe),this.options,this.options.refetchOnReconnect)}shouldFetchOnWindowFocus(){return Lm(E(this,fe),this.options,this.options.refetchOnWindowFocus)}destroy(){this.listeners=new Set,ae(this,ve,Im).call(this),ae(this,ve,Rm).call(this),E(this,fe).removeObserver(this)}setOptions(t){const r=this.options,n=E(this,fe);if(this.options=E(this,Mt).defaultQueryOptions(t),this.options.enabled!==void 0&&typeof this.options.enabled!="boolean"&&typeof this.options.enabled!="function"&&typeof sr(this.options.enabled,E(this,fe))!="boolean")throw new Error("Expected enabled to be a boolean or a callback that returns a boolean");ae(this,ve,Dm).call(this),E(this,fe).setOptions(this.options),r._defaulted&&!Qc(this.options,r)&&E(this,Mt).getQueryCache().notify({type:"observerOptionsUpdated",query:E(this,fe),observer:this});const i=this.hasListeners();i&&Jb(E(this,fe),n,this.options,r)&&ae(this,ve,Rs).call(this),this.updateResult(),i&&(E(this,fe)!==n||sr(this.options.enabled,E(this,fe))!==sr(r.enabled,E(this,fe))||oi(this.options.staleTime,E(this,fe))!==oi(r.staleTime,E(this,fe)))&&ae(this,ve,$m).call(this);const a=ae(this,ve,km).call(this);i&&(E(this,fe)!==n||sr(this.options.enabled,E(this,fe))!==sr(r.enabled,E(this,fe))||a!==E(this,Wn))&&ae(this,ve,Nm).call(this,a)}getOptimisticResult(t){const r=E(this,Mt).getQueryCache().build(E(this,Mt),t),n=this.createResult(r,t);return gk(this,n)&&(G(this,Ot,n),G(this,ao,this.options),G(this,Gi,E(this,fe).state)),n}getCurrentResult(){return E(this,Ot)}trackResult(t,r){return new Proxy(t,{get:(n,i)=>(this.trackProp(i),r==null||r(i),i==="promise"&&(this.trackProp("data"),!this.options.experimental_prefetchInRender&&E(this,tn).status==="pending"&&E(this,tn).reject(new Error("experimental_prefetchInRender feature flag is not enabled"))),Reflect.get(n,i))})}trackProp(t){E(this,lo).add(t)}getCurrentQuery(){return E(this,fe)}refetch({...t}={}){return this.fetch({...t})}fetchOptimistic(t){const r=E(this,Mt).defaultQueryOptions(t),n=E(this,Mt).getQueryCache().build(E(this,Mt),r);return n.fetch().then(()=>this.createResult(n,r))}fetch(t){return ae(this,ve,Rs).call(this,{...t,cancelRefetch:t.cancelRefetch??!0}).then(()=>(this.updateResult(),E(this,Ot)))}createResult(t,r){var A;const n=E(this,fe),i=this.options,a=E(this,Ot),o=E(this,Gi),s=E(this,ao),u=t!==n?t.state:E(this,nu),{state:f}=t;let c={...f},d=!1,h;if(r._optimisticResults){const C=this.hasListeners(),N=!C&&Yb(t,r),$=C&&Jb(t,n,r,i);(N||$)&&(c={...c,...YP(f.data,t.options)}),r._optimisticResults==="isRestoring"&&(c.fetchStatus="idle")}let{error:p,errorUpdatedAt:m,status:y}=c;h=c.data;let v=!1;if(r.placeholderData!==void 0&&h===void 0&&y==="pending"){let C;a!=null&&a.isPlaceholderData&&r.placeholderData===(s==null?void 0:s.placeholderData)?(C=a.data,v=!0):C=typeof r.placeholderData=="function"?r.placeholderData((A=E(this,so))==null?void 0:A.state.data,E(this,so)):r.placeholderData,C!==void 0&&(y="success",h=jm(a==null?void 0:a.data,C,r),d=!0)}if(r.select&&h!==void 0&&!v)if(a&&h===(o==null?void 0:o.data)&&r.select===E(this,iu))h=E(this,oo);else try{G(this,iu,r.select),h=r.select(h),h=jm(a==null?void 0:a.data,h,r),G(this,oo,h),G(this,zn,null)}catch(C){G(this,zn,C)}E(this,zn)&&(p=E(this,zn),h=E(this,oo),m=Date.now(),y="error");const g=c.fetchStatus==="fetching",b=y==="pending",w=y==="error",x=b&&g,S=h!==void 0,P={status:y,fetchStatus:c.fetchStatus,isPending:b,isSuccess:y==="success",isError:w,isInitialLoading:x,isLoading:x,data:h,dataUpdatedAt:c.dataUpdatedAt,error:p,errorUpdatedAt:m,failureCount:c.fetchFailureCount,failureReason:c.fetchFailureReason,errorUpdateCount:c.errorUpdateCount,isFetched:c.dataUpdateCount>0||c.errorUpdateCount>0,isFetchedAfterMount:c.dataUpdateCount>u.dataUpdateCount||c.errorUpdateCount>u.errorUpdateCount,isFetching:g,isRefetching:g&&!b,isLoadingError:w&&!S,isPaused:c.fetchStatus==="paused",isPlaceholderData:d,isRefetchError:w&&S,isStale:wg(t,r),refetch:this.refetch,promise:E(this,tn),isEnabled:sr(r.enabled,t)!==!1};if(this.options.experimental_prefetchInRender){const C=P.data!==void 0,N=P.status==="error"&&!C,$=R=>{N?R.reject(P.error):C&&R.resolve(P.data)},L=()=>{const R=G(this,tn,P.promise=Tm());$(R)},I=E(this,tn);switch(I.status){case"pending":t.queryHash===n.queryHash&&$(I);break;case"fulfilled":(N||P.data!==I.value)&&L();break;case"rejected":(!N||P.error!==I.reason)&&L();break}}return P}updateResult(){const t=E(this,Ot),r=this.createResult(E(this,fe),this.options);if(G(this,Gi,E(this,fe).state),G(this,ao,this.options),E(this,Gi).data!==void 0&&G(this,so,E(this,fe)),Qc(r,t))return;G(this,Ot,r);const n=()=>{if(!t)return!0;const{notifyOnChangeProps:i}=this.options,a=typeof i=="function"?i():i;if(a==="all"||!a&&!E(this,lo).size)return!0;const o=new Set(a??E(this,lo));return this.options.throwOnError&&o.add("error"),Object.keys(E(this,Ot)).some(s=>{const l=s;return E(this,Ot)[l]!==t[l]&&o.has(l)})};ae(this,ve,JP).call(this,{listeners:n()})}onQueryUpdate(){this.updateResult(),this.hasListeners()&&ae(this,ve,Mm).call(this)}},Mt=new WeakMap,fe=new WeakMap,nu=new WeakMap,Ot=new WeakMap,Gi=new WeakMap,ao=new WeakMap,tn=new WeakMap,zn=new WeakMap,iu=new WeakMap,oo=new WeakMap,so=new WeakMap,Xi=new WeakMap,Qi=new WeakMap,Wn=new WeakMap,lo=new WeakMap,ve=new WeakSet,Rs=function(t){ae(this,ve,Dm).call(this);let r=E(this,fe).fetch(this.options,t);return t!=null&&t.throwOnError||(r=r.catch(Pt)),r},$m=function(){ae(this,ve,Im).call(this);const t=oi(this.options.staleTime,E(this,fe));if(la||E(this,Ot).isStale||!Am(t))return;const n=qP(E(this,Ot).dataUpdatedAt,t)+1;G(this,Xi,Di.setTimeout(()=>{E(this,Ot).isStale||this.updateResult()},n))},km=function(){return(typeof this.options.refetchInterval=="function"?this.options.refetchInterval(E(this,fe)):this.options.refetchInterval)??!1},Nm=function(t){ae(this,ve,Rm).call(this),G(this,Wn,t),!(la||sr(this.options.enabled,E(this,fe))===!1||!Am(E(this,Wn))||E(this,Wn)===0)&&G(this,Qi,Di.setInterval(()=>{(this.options.refetchIntervalInBackground||xg.isFocused())&&ae(this,ve,Rs).call(this)},E(this,Wn)))},Mm=function(){ae(this,ve,$m).call(this),ae(this,ve,Nm).call(this,ae(this,ve,km).call(this))},Im=function(){E(this,Xi)&&(Di.clearTimeout(E(this,Xi)),G(this,Xi,void 0))},Rm=function(){E(this,Qi)&&(Di.clearInterval(E(this,Qi)),G(this,Qi,void 0))},Dm=function(){const t=E(this,Mt).getQueryCache().build(E(this,Mt),this.options);if(t===E(this,fe))return;const r=E(this,fe);G(this,fe,t),G(this,nu,t.state),this.hasListeners()&&(r==null||r.removeObserver(this),t.addObserver(this))},JP=function(t){tt.batch(()=>{t.listeners&&this.listeners.forEach(r=>{r(E(this,Ot))}),E(this,Mt).getQueryCache().notify({query:E(this,fe),type:"observerResultsUpdated"})})},EO);function vk(e,t){return sr(t.enabled,e)!==!1&&e.state.data===void 0&&!(e.state.status==="error"&&t.retryOnMount===!1)}function Yb(e,t){return vk(e,t)||e.state.data!==void 0&&Lm(e,t,t.refetchOnMount)}function Lm(e,t,r){if(sr(t.enabled,e)!==!1&&oi(t.staleTime,e)!=="static"){const n=typeof r=="function"?r(e):r;return n==="always"||n!==!1&&wg(e,t)}return!1}function Jb(e,t,r,n){return(e!==t||sr(n.enabled,e)===!1)&&(!r.suspense||e.state.status!=="error")&&wg(e,r)}function wg(e,t){return sr(t.enabled,e)!==!1&&e.isStaleByTime(oi(t.staleTime,e))}function gk(e,t){return!Qc(e.getCurrentResult(),t)}function Zb(e){return{onFetch:(t,r)=>{var f,c,d,h,p;const n=t.options,i=(d=(c=(f=t.fetchOptions)==null?void 0:f.meta)==null?void 0:c.fetchMore)==null?void 0:d.direction,a=((h=t.state.data)==null?void 0:h.pages)||[],o=((p=t.state.data)==null?void 0:p.pageParams)||[];let s={pages:[],pageParams:[]},l=0;const u=async()=>{let m=!1;const y=b=>{uk(b,()=>t.signal,()=>m=!0)},v=VP(t.options,t.fetchOptions),g=async(b,w,x)=>{if(m)return Promise.reject();if(w==null&&b.pages.length)return Promise.resolve(b);const _=(()=>{const N={client:t.client,queryKey:t.queryKey,pageParam:w,direction:x?"backward":"forward",meta:t.options.meta};return y(N),N})(),P=await v(_),{maxPages:A}=t.options,C=x?lk:sk;return{pages:C(b.pages,P,A),pageParams:C(b.pageParams,w,A)}};if(i&&a.length){const b=i==="backward",w=b?bk:ex,x={pages:a,pageParams:o},S=w(n,x);s=await g(x,S,b)}else{const b=e??a.length;do{const w=l===0?o[0]??n.initialPageParam:ex(n,s);if(l>0&&w==null)break;s=await g(s,w),l++}while(l{var m,y;return(y=(m=t.options).persister)==null?void 0:y.call(m,u,{client:t.client,queryKey:t.queryKey,meta:t.options.meta,signal:t.signal},r)}:t.fetchFn=u}}}function ex(e,{pages:t,pageParams:r}){const n=t.length-1;return t.length>0?e.getNextPageParam(t[n],t,r[n],r):void 0}function bk(e,{pages:t,pageParams:r}){var n;return t.length>0?(n=e.getPreviousPageParam)==null?void 0:n.call(e,t[0],t,r[0],r):void 0}var au,Lr,vt,Yi,Br,kn,jO,xk=(jO=class extends QP{constructor(t){super();Z(this,Br);Z(this,au);Z(this,Lr);Z(this,vt);Z(this,Yi);G(this,au,t.client),this.mutationId=t.mutationId,G(this,vt,t.mutationCache),G(this,Lr,[]),this.state=t.state||ZP(),this.setOptions(t.options),this.scheduleGc()}setOptions(t){this.options=t,this.updateGcTime(this.options.gcTime)}get meta(){return this.options.meta}addObserver(t){E(this,Lr).includes(t)||(E(this,Lr).push(t),this.clearGcTimeout(),E(this,vt).notify({type:"observerAdded",mutation:this,observer:t}))}removeObserver(t){G(this,Lr,E(this,Lr).filter(r=>r!==t)),this.scheduleGc(),E(this,vt).notify({type:"observerRemoved",mutation:this,observer:t})}optionalRemove(){E(this,Lr).length||(this.state.status==="pending"?this.scheduleGc():E(this,vt).remove(this))}continue(){var t;return((t=E(this,Yi))==null?void 0:t.continue())??this.execute(this.state.variables)}async execute(t){var o,s,l,u,f,c,d,h,p,m,y,v,g,b,w,x,S,_;const r=()=>{ae(this,Br,kn).call(this,{type:"continue"})},n={client:E(this,au),meta:this.options.meta,mutationKey:this.options.mutationKey};G(this,Yi,XP({fn:()=>this.options.mutationFn?this.options.mutationFn(t,n):Promise.reject(new Error("No mutationFn found")),onFail:(P,A)=>{ae(this,Br,kn).call(this,{type:"failed",failureCount:P,error:A})},onPause:()=>{ae(this,Br,kn).call(this,{type:"pause"})},onContinue:r,retry:this.options.retry??0,retryDelay:this.options.retryDelay,networkMode:this.options.networkMode,canRun:()=>E(this,vt).canRun(this)}));const i=this.state.status==="pending",a=!E(this,Yi).canStart();try{if(i)r();else{ae(this,Br,kn).call(this,{type:"pending",variables:t,isPaused:a}),E(this,vt).config.onMutate&&await E(this,vt).config.onMutate(t,this,n);const A=await((s=(o=this.options).onMutate)==null?void 0:s.call(o,t,n));A!==this.state.context&&ae(this,Br,kn).call(this,{type:"pending",context:A,variables:t,isPaused:a})}const P=await E(this,Yi).start();return await((u=(l=E(this,vt).config).onSuccess)==null?void 0:u.call(l,P,t,this.state.context,this,n)),await((c=(f=this.options).onSuccess)==null?void 0:c.call(f,P,t,this.state.context,n)),await((h=(d=E(this,vt).config).onSettled)==null?void 0:h.call(d,P,null,this.state.variables,this.state.context,this,n)),await((m=(p=this.options).onSettled)==null?void 0:m.call(p,P,null,t,this.state.context,n)),ae(this,Br,kn).call(this,{type:"success",data:P}),P}catch(P){try{await((v=(y=E(this,vt).config).onError)==null?void 0:v.call(y,P,t,this.state.context,this,n))}catch(A){Promise.reject(A)}try{await((b=(g=this.options).onError)==null?void 0:b.call(g,P,t,this.state.context,n))}catch(A){Promise.reject(A)}try{await((x=(w=E(this,vt).config).onSettled)==null?void 0:x.call(w,void 0,P,this.state.variables,this.state.context,this,n))}catch(A){Promise.reject(A)}try{await((_=(S=this.options).onSettled)==null?void 0:_.call(S,void 0,P,t,this.state.context,n))}catch(A){Promise.reject(A)}throw ae(this,Br,kn).call(this,{type:"error",error:P}),P}finally{E(this,vt).runNext(this)}}},au=new WeakMap,Lr=new WeakMap,vt=new WeakMap,Yi=new WeakMap,Br=new WeakSet,kn=function(t){const r=n=>{switch(t.type){case"failed":return{...n,failureCount:t.failureCount,failureReason:t.error};case"pause":return{...n,isPaused:!0};case"continue":return{...n,isPaused:!1};case"pending":return{...n,context:t.context,data:void 0,failureCount:0,failureReason:null,error:null,isPaused:t.isPaused,status:"pending",variables:t.variables,submittedAt:Date.now()};case"success":return{...n,data:t.data,failureCount:0,failureReason:null,error:null,status:"success",isPaused:!1};case"error":return{...n,data:void 0,error:t.error,failureCount:n.failureCount+1,failureReason:t.error,isPaused:!1,status:"error"}}};this.state=r(this.state),tt.batch(()=>{E(this,Lr).forEach(n=>{n.onMutationUpdate(t)}),E(this,vt).notify({mutation:this,type:"updated",action:t})})},jO);function ZP(){return{context:void 0,data:void 0,error:null,failureCount:0,failureReason:null,isPaused:!1,status:"idle",variables:void 0,submittedAt:0}}var rn,_r,ou,TO,wk=(TO=class extends Yo{constructor(t={}){super();Z(this,rn);Z(this,_r);Z(this,ou);this.config=t,G(this,rn,new Set),G(this,_r,new Map),G(this,ou,0)}build(t,r,n){const i=new xk({client:t,mutationCache:this,mutationId:++Au(this,ou)._,options:t.defaultMutationOptions(r),state:n});return this.add(i),i}add(t){E(this,rn).add(t);const r=qu(t);if(typeof r=="string"){const n=E(this,_r).get(r);n?n.push(t):E(this,_r).set(r,[t])}this.notify({type:"added",mutation:t})}remove(t){if(E(this,rn).delete(t)){const r=qu(t);if(typeof r=="string"){const n=E(this,_r).get(r);if(n)if(n.length>1){const i=n.indexOf(t);i!==-1&&n.splice(i,1)}else n[0]===t&&E(this,_r).delete(r)}}this.notify({type:"removed",mutation:t})}canRun(t){const r=qu(t);if(typeof r=="string"){const n=E(this,_r).get(r),i=n==null?void 0:n.find(a=>a.state.status==="pending");return!i||i===t}else return!0}runNext(t){var n;const r=qu(t);if(typeof r=="string"){const i=(n=E(this,_r).get(r))==null?void 0:n.find(a=>a!==t&&a.state.isPaused);return(i==null?void 0:i.continue())??Promise.resolve()}else return Promise.resolve()}clear(){tt.batch(()=>{E(this,rn).forEach(t=>{this.notify({type:"removed",mutation:t})}),E(this,rn).clear(),E(this,_r).clear()})}getAll(){return Array.from(E(this,rn))}find(t){const r={exact:!0,...t};return this.getAll().find(n=>Kb(r,n))}findAll(t={}){return this.getAll().filter(r=>Kb(t,r))}notify(t){tt.batch(()=>{this.listeners.forEach(r=>{r(t)})})}resumePausedMutations(){const t=this.getAll().filter(r=>r.state.isPaused);return tt.batch(()=>Promise.all(t.map(r=>r.continue().catch(Pt))))}},rn=new WeakMap,_r=new WeakMap,ou=new WeakMap,TO);function qu(e){var t;return(t=e.options.scope)==null?void 0:t.id}var nn,Hn,It,an,mn,xc,Bm,CO,Sk=(CO=class extends Yo{constructor(r,n){super();Z(this,mn);Z(this,nn);Z(this,Hn);Z(this,It);Z(this,an);G(this,nn,r),this.setOptions(n),this.bindMethods(),ae(this,mn,xc).call(this)}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(r){var i;const n=this.options;this.options=E(this,nn).defaultMutationOptions(r),Qc(this.options,n)||E(this,nn).getMutationCache().notify({type:"observerOptionsUpdated",mutation:E(this,It),observer:this}),n!=null&&n.mutationKey&&this.options.mutationKey&&ua(n.mutationKey)!==ua(this.options.mutationKey)?this.reset():((i=E(this,It))==null?void 0:i.state.status)==="pending"&&E(this,It).setOptions(this.options)}onUnsubscribe(){var r;this.hasListeners()||(r=E(this,It))==null||r.removeObserver(this)}onMutationUpdate(r){ae(this,mn,xc).call(this),ae(this,mn,Bm).call(this,r)}getCurrentResult(){return E(this,Hn)}reset(){var r;(r=E(this,It))==null||r.removeObserver(this),G(this,It,void 0),ae(this,mn,xc).call(this),ae(this,mn,Bm).call(this)}mutate(r,n){var i;return G(this,an,n),(i=E(this,It))==null||i.removeObserver(this),G(this,It,E(this,nn).getMutationCache().build(E(this,nn),this.options)),E(this,It).addObserver(this),E(this,It).execute(r)}},nn=new WeakMap,Hn=new WeakMap,It=new WeakMap,an=new WeakMap,mn=new WeakSet,xc=function(){var n;const r=((n=E(this,It))==null?void 0:n.state)??ZP();G(this,Hn,{...r,isPending:r.status==="pending",isSuccess:r.status==="success",isError:r.status==="error",isIdle:r.status==="idle",mutate:this.mutate,reset:this.reset})},Bm=function(r){tt.batch(()=>{var n,i,a,o,s,l,u,f;if(E(this,an)&&this.hasListeners()){const c=E(this,Hn).variables,d=E(this,Hn).context,h={client:E(this,nn),meta:this.options.meta,mutationKey:this.options.mutationKey};if((r==null?void 0:r.type)==="success"){try{(i=(n=E(this,an)).onSuccess)==null||i.call(n,r.data,c,d,h)}catch(p){Promise.reject(p)}try{(o=(a=E(this,an)).onSettled)==null||o.call(a,r.data,null,c,d,h)}catch(p){Promise.reject(p)}}else if((r==null?void 0:r.type)==="error"){try{(l=(s=E(this,an)).onError)==null||l.call(s,r.error,c,d,h)}catch(p){Promise.reject(p)}try{(f=(u=E(this,an)).onSettled)==null||f.call(u,void 0,r.error,c,d,h)}catch(p){Promise.reject(p)}}}this.listeners.forEach(c=>{c(E(this,Hn))})})},CO),Fr,$O,Ok=($O=class extends Yo{constructor(t={}){super();Z(this,Fr);this.config=t,G(this,Fr,new Map)}build(t,r,n){const i=r.queryKey,a=r.queryHash??vg(i,r);let o=this.get(a);return o||(o=new mk({client:t,queryKey:i,queryHash:a,options:t.defaultQueryOptions(r),state:n,defaultOptions:t.getQueryDefaults(i)}),this.add(o)),o}add(t){E(this,Fr).has(t.queryHash)||(E(this,Fr).set(t.queryHash,t),this.notify({type:"added",query:t}))}remove(t){const r=E(this,Fr).get(t.queryHash);r&&(t.destroy(),r===t&&E(this,Fr).delete(t.queryHash),this.notify({type:"removed",query:t}))}clear(){tt.batch(()=>{this.getAll().forEach(t=>{this.remove(t)})})}get(t){return E(this,Fr).get(t)}getAll(){return[...E(this,Fr).values()]}find(t){const r={exact:!0,...t};return this.getAll().find(n=>qb(r,n))}findAll(t={}){const r=this.getAll();return Object.keys(t).length>0?r.filter(n=>qb(t,n)):r}notify(t){tt.batch(()=>{this.listeners.forEach(r=>{r(t)})})}onFocus(){tt.batch(()=>{this.getAll().forEach(t=>{t.onFocus()})})}onOnline(){tt.batch(()=>{this.getAll().forEach(t=>{t.onOnline()})})}},Fr=new WeakMap,$O),Fe,qn,Kn,uo,co,Vn,fo,ho,kO,_k=(kO=class{constructor(e={}){Z(this,Fe);Z(this,qn);Z(this,Kn);Z(this,uo);Z(this,co);Z(this,Vn);Z(this,fo);Z(this,ho);G(this,Fe,e.queryCache||new Ok),G(this,qn,e.mutationCache||new wk),G(this,Kn,e.defaultOptions||{}),G(this,uo,new Map),G(this,co,new Map),G(this,Vn,0)}mount(){Au(this,Vn)._++,E(this,Vn)===1&&(G(this,fo,xg.subscribe(async e=>{e&&(await this.resumePausedMutations(),E(this,Fe).onFocus())})),G(this,ho,Yc.subscribe(async e=>{e&&(await this.resumePausedMutations(),E(this,Fe).onOnline())})))}unmount(){var e,t;Au(this,Vn)._--,E(this,Vn)===0&&((e=E(this,fo))==null||e.call(this),G(this,fo,void 0),(t=E(this,ho))==null||t.call(this),G(this,ho,void 0))}isFetching(e){return E(this,Fe).findAll({...e,fetchStatus:"fetching"}).length}isMutating(e){return E(this,qn).findAll({...e,status:"pending"}).length}getQueryData(e){var r;const t=this.defaultQueryOptions({queryKey:e});return(r=E(this,Fe).get(t.queryHash))==null?void 0:r.state.data}ensureQueryData(e){const t=this.defaultQueryOptions(e),r=E(this,Fe).build(this,t),n=r.state.data;return n===void 0?this.fetchQuery(e):(e.revalidateIfStale&&r.isStaleByTime(oi(t.staleTime,r))&&this.prefetchQuery(t),Promise.resolve(n))}getQueriesData(e){return E(this,Fe).findAll(e).map(({queryKey:t,state:r})=>{const n=r.data;return[t,n]})}setQueryData(e,t,r){const n=this.defaultQueryOptions({queryKey:e}),i=E(this,Fe).get(n.queryHash),a=i==null?void 0:i.state.data,o=ik(t,a);if(o!==void 0)return E(this,Fe).build(this,n).setData(o,{...r,manual:!0})}setQueriesData(e,t,r){return tt.batch(()=>E(this,Fe).findAll(e).map(({queryKey:n})=>[n,this.setQueryData(n,t,r)]))}getQueryState(e){var r;const t=this.defaultQueryOptions({queryKey:e});return(r=E(this,Fe).get(t.queryHash))==null?void 0:r.state}removeQueries(e){const t=E(this,Fe);tt.batch(()=>{t.findAll(e).forEach(r=>{t.remove(r)})})}resetQueries(e,t){const r=E(this,Fe);return tt.batch(()=>(r.findAll(e).forEach(n=>{n.reset()}),this.refetchQueries({type:"active",...e},t)))}cancelQueries(e,t={}){const r={revert:!0,...t},n=tt.batch(()=>E(this,Fe).findAll(e).map(i=>i.cancel(r)));return Promise.all(n).then(Pt).catch(Pt)}invalidateQueries(e,t={}){return tt.batch(()=>(E(this,Fe).findAll(e).forEach(r=>{r.invalidate()}),(e==null?void 0:e.refetchType)==="none"?Promise.resolve():this.refetchQueries({...e,type:(e==null?void 0:e.refetchType)??(e==null?void 0:e.type)??"active"},t)))}refetchQueries(e,t={}){const r={...t,cancelRefetch:t.cancelRefetch??!0},n=tt.batch(()=>E(this,Fe).findAll(e).filter(i=>!i.isDisabled()&&!i.isStatic()).map(i=>{let a=i.fetch(void 0,r);return r.throwOnError||(a=a.catch(Pt)),i.state.fetchStatus==="paused"?Promise.resolve():a}));return Promise.all(n).then(Pt)}fetchQuery(e){const t=this.defaultQueryOptions(e);t.retry===void 0&&(t.retry=!1);const r=E(this,Fe).build(this,t);return r.isStaleByTime(oi(t.staleTime,r))?r.fetch(t):Promise.resolve(r.state.data)}prefetchQuery(e){return this.fetchQuery(e).then(Pt).catch(Pt)}fetchInfiniteQuery(e){return e.behavior=Zb(e.pages),this.fetchQuery(e)}prefetchInfiniteQuery(e){return this.fetchInfiniteQuery(e).then(Pt).catch(Pt)}ensureInfiniteQueryData(e){return e.behavior=Zb(e.pages),this.ensureQueryData(e)}resumePausedMutations(){return Yc.isOnline()?E(this,qn).resumePausedMutations():Promise.resolve()}getQueryCache(){return E(this,Fe)}getMutationCache(){return E(this,qn)}getDefaultOptions(){return E(this,Kn)}setDefaultOptions(e){G(this,Kn,e)}setQueryDefaults(e,t){E(this,uo).set(ua(e),{queryKey:e,defaultOptions:t})}getQueryDefaults(e){const t=[...E(this,uo).values()],r={};return t.forEach(n=>{gl(e,n.queryKey)&&Object.assign(r,n.defaultOptions)}),r}setMutationDefaults(e,t){E(this,co).set(ua(e),{mutationKey:e,defaultOptions:t})}getMutationDefaults(e){const t=[...E(this,co).values()],r={};return t.forEach(n=>{gl(e,n.mutationKey)&&Object.assign(r,n.defaultOptions)}),r}defaultQueryOptions(e){if(e._defaulted)return e;const t={...E(this,Kn).queries,...this.getQueryDefaults(e.queryKey),...e,_defaulted:!0};return t.queryHash||(t.queryHash=vg(t.queryKey,t)),t.refetchOnReconnect===void 0&&(t.refetchOnReconnect=t.networkMode!=="always"),t.throwOnError===void 0&&(t.throwOnError=!!t.suspense),!t.networkMode&&t.persister&&(t.networkMode="offlineFirst"),t.queryFn===gg&&(t.enabled=!1),t}defaultMutationOptions(e){return e!=null&&e._defaulted?e:{...E(this,Kn).mutations,...(e==null?void 0:e.mutationKey)&&this.getMutationDefaults(e.mutationKey),...e,_defaulted:!0}}clear(){E(this,Fe).clear(),E(this,qn).clear()}},Fe=new WeakMap,qn=new WeakMap,Kn=new WeakMap,uo=new WeakMap,co=new WeakMap,Vn=new WeakMap,fo=new WeakMap,ho=new WeakMap,kO),eA=j.createContext(void 0),Pn=e=>{const t=j.useContext(eA);if(!t)throw new Error("No QueryClient set, use QueryClientProvider to set one");return t},Pk=({client:e,children:t})=>(j.useEffect(()=>(e.mount(),()=>{e.unmount()}),[e]),O.jsx(eA.Provider,{value:e,children:t})),tA=j.createContext(!1),Ak=()=>j.useContext(tA);tA.Provider;function Ek(){let e=!1;return{clearReset:()=>{e=!1},reset:()=>{e=!0},isReset:()=>e}}var jk=j.createContext(Ek()),Tk=()=>j.useContext(jk),Ck=(e,t,r)=>{const n=r!=null&&r.state.error&&typeof e.throwOnError=="function"?bg(e.throwOnError,[r.state.error,r]):e.throwOnError;(e.suspense||e.experimental_prefetchInRender||n)&&(t.isReset()||(e.retryOnMount=!1))},$k=e=>{j.useEffect(()=>{e.clearReset()},[e])},kk=({result:e,errorResetBoundary:t,throwOnError:r,query:n,suspense:i})=>e.isError&&!t.isReset()&&!e.isFetching&&n&&(i&&e.data===void 0||bg(r,[e.error,n])),Nk=e=>{if(e.suspense){const r=i=>i==="static"?i:Math.max(i??1e3,1e3),n=e.staleTime;e.staleTime=typeof n=="function"?(...i)=>r(n(...i)):r(n),typeof e.gcTime=="number"&&(e.gcTime=Math.max(e.gcTime,1e3))}},Mk=(e,t)=>e.isLoading&&e.isFetching&&!t,Ik=(e,t)=>(e==null?void 0:e.suspense)&&t.isPending,tx=(e,t,r)=>t.fetchOptimistic(e).catch(()=>{r.clearReset()});function Rk(e,t,r){var d,h,p,m;const n=Ak(),i=Tk(),a=Pn(),o=a.defaultQueryOptions(e);(h=(d=a.getDefaultOptions().queries)==null?void 0:d._experimental_beforeQuery)==null||h.call(d,o);const s=a.getQueryCache().get(o.queryHash);o._optimisticResults=n?"isRestoring":"optimistic",Nk(o),Ck(o,i,s),$k(i);const l=!a.getQueryCache().get(o.queryHash),[u]=j.useState(()=>new t(a,o)),f=u.getOptimisticResult(o),c=!n&&e.subscribed!==!1;if(j.useSyncExternalStore(j.useCallback(y=>{const v=c?u.subscribe(tt.batchCalls(y)):Pt;return u.updateResult(),v},[u,c]),()=>u.getCurrentResult(),()=>u.getCurrentResult()),j.useEffect(()=>{u.setOptions(o)},[o,u]),Ik(o,f))throw tx(o,u,i);if(kk({result:f,errorResetBoundary:i,throwOnError:o.throwOnError,query:s,suspense:o.suspense}))throw f.error;if((m=(p=a.getDefaultOptions().queries)==null?void 0:p._experimental_afterQuery)==null||m.call(p,o,f),o.experimental_prefetchInRender&&!la&&Mk(f,n)){const y=l?tx(o,u,i):s==null?void 0:s.promise;y==null||y.catch(Pt).finally(()=>{u.updateResult()})}return o.notifyOnChangeProps?f:u.trackResult(f)}function zr(e,t){return Rk(e,yk)}function zt(e,t){const r=Pn(),[n]=j.useState(()=>new Sk(r,e));j.useEffect(()=>{n.setOptions(e)},[n,e]);const i=j.useSyncExternalStore(j.useCallback(o=>n.subscribe(tt.batchCalls(o)),[n]),()=>n.getCurrentResult(),()=>n.getCurrentResult()),a=j.useCallback((o,s)=>{n.mutate(o,s).catch(Pt)},[n]);if(i.error&&bg(n.options.throwOnError,[i.error]))throw i.error;return{...i,mutate:a,mutateAsync:i.mutate}}/** + * @remix-run/router v1.23.2 + * + * Copyright (c) Remix Software Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE.md file in the root directory of this source tree. + * + * @license MIT + */function bl(){return bl=Object.assign?Object.assign.bind():function(e){for(var t=1;t"u")throw new Error(t)}function Sg(e,t){if(!e){typeof console<"u"&&console.warn(t);try{throw new Error(t)}catch{}}}function Lk(){return Math.random().toString(36).substr(2,8)}function nx(e,t){return{usr:e.state,key:e.key,idx:t}}function Fm(e,t,r,n){return r===void 0&&(r=null),bl({pathname:typeof e=="string"?e:e.pathname,search:"",hash:""},typeof t=="string"?Jo(t):t,{state:r,key:t&&t.key||n||Lk()})}function Jc(e){let{pathname:t="/",search:r="",hash:n=""}=e;return r&&r!=="?"&&(t+=r.charAt(0)==="?"?r:"?"+r),n&&n!=="#"&&(t+=n.charAt(0)==="#"?n:"#"+n),t}function Jo(e){let t={};if(e){let r=e.indexOf("#");r>=0&&(t.hash=e.substr(r),e=e.substr(0,r));let n=e.indexOf("?");n>=0&&(t.search=e.substr(n),e=e.substr(0,n)),e&&(t.pathname=e)}return t}function Bk(e,t,r,n){n===void 0&&(n={});let{window:i=document.defaultView,v5Compat:a=!1}=n,o=i.history,s=Qn.Pop,l=null,u=f();u==null&&(u=0,o.replaceState(bl({},o.state,{idx:u}),""));function f(){return(o.state||{idx:null}).idx}function c(){s=Qn.Pop;let y=f(),v=y==null?null:y-u;u=y,l&&l({action:s,location:m.location,delta:v})}function d(y,v){s=Qn.Push;let g=Fm(m.location,y,v);u=f()+1;let b=nx(g,u),w=m.createHref(g);try{o.pushState(b,"",w)}catch(x){if(x instanceof DOMException&&x.name==="DataCloneError")throw x;i.location.assign(w)}a&&l&&l({action:s,location:m.location,delta:1})}function h(y,v){s=Qn.Replace;let g=Fm(m.location,y,v);u=f();let b=nx(g,u),w=m.createHref(g);o.replaceState(b,"",w),a&&l&&l({action:s,location:m.location,delta:0})}function p(y){let v=i.location.origin!=="null"?i.location.origin:i.location.href,g=typeof y=="string"?y:Jc(y);return g=g.replace(/ $/,"%20"),We(v,"No window.location.(origin|href) available to create URL for href: "+g),new URL(g,v)}let m={get action(){return s},get location(){return e(i,o)},listen(y){if(l)throw new Error("A history only accepts one active listener");return i.addEventListener(rx,c),l=y,()=>{i.removeEventListener(rx,c),l=null}},createHref(y){return t(i,y)},createURL:p,encodeLocation(y){let v=p(y);return{pathname:v.pathname,search:v.search,hash:v.hash}},push:d,replace:h,go(y){return o.go(y)}};return m}var ix;(function(e){e.data="data",e.deferred="deferred",e.redirect="redirect",e.error="error"})(ix||(ix={}));function Fk(e,t,r){return r===void 0&&(r="/"),Uk(e,t,r)}function Uk(e,t,r,n){let i=typeof t=="string"?Jo(t):t,a=wo(i.pathname||"/",r);if(a==null)return null;let o=rA(e);zk(o);let s=null;for(let l=0;s==null&&l{let l={relativePath:s===void 0?a.path||"":s,caseSensitive:a.caseSensitive===!0,childrenIndex:o,route:a};l.relativePath.startsWith("/")&&(We(l.relativePath.startsWith(n),'Absolute route path "'+l.relativePath+'" nested under path '+('"'+n+'" is not valid. An absolute child route path ')+"must start with the combined path of all its parent routes."),l.relativePath=l.relativePath.slice(n.length));let u=si([n,l.relativePath]),f=r.concat(l);a.children&&a.children.length>0&&(We(a.index!==!0,"Index routes must not have child routes. Please remove "+('all child routes from route path "'+u+'".')),rA(a.children,t,f,u)),!(a.path==null&&!a.index)&&t.push({path:u,score:Xk(u,a.index),routesMeta:f})};return e.forEach((a,o)=>{var s;if(a.path===""||!((s=a.path)!=null&&s.includes("?")))i(a,o);else for(let l of nA(a.path))i(a,o,l)}),t}function nA(e){let t=e.split("/");if(t.length===0)return[];let[r,...n]=t,i=r.endsWith("?"),a=r.replace(/\?$/,"");if(n.length===0)return i?[a,""]:[a];let o=nA(n.join("/")),s=[];return s.push(...o.map(l=>l===""?a:[a,l].join("/"))),i&&s.push(...o),s.map(l=>e.startsWith("/")&&l===""?"/":l)}function zk(e){e.sort((t,r)=>t.score!==r.score?r.score-t.score:Qk(t.routesMeta.map(n=>n.childrenIndex),r.routesMeta.map(n=>n.childrenIndex)))}const Wk=/^:[\w-]+$/,Hk=3,qk=2,Kk=1,Vk=10,Gk=-2,ax=e=>e==="*";function Xk(e,t){let r=e.split("/"),n=r.length;return r.some(ax)&&(n+=Gk),t&&(n+=qk),r.filter(i=>!ax(i)).reduce((i,a)=>i+(Wk.test(a)?Hk:a===""?Kk:Vk),n)}function Qk(e,t){return e.length===t.length&&e.slice(0,-1).every((n,i)=>n===t[i])?e[e.length-1]-t[t.length-1]:0}function Yk(e,t,r){let{routesMeta:n}=e,i={},a="/",o=[];for(let s=0;s{let{paramName:d,isOptional:h}=f;if(d==="*"){let m=s[c]||"";o=a.slice(0,a.length-m.length).replace(/(.)\/+$/,"$1")}const p=s[c];return h&&!p?u[d]=void 0:u[d]=(p||"").replace(/%2F/g,"/"),u},{}),pathname:a,pathnameBase:o,pattern:e}}function Jk(e,t,r){t===void 0&&(t=!1),r===void 0&&(r=!0),Sg(e==="*"||!e.endsWith("*")||e.endsWith("/*"),'Route path "'+e+'" will be treated as if it were '+('"'+e.replace(/\*$/,"/*")+'" because the `*` character must ')+"always follow a `/` in the pattern. To get rid of this warning, "+('please change the route path to "'+e.replace(/\*$/,"/*")+'".'));let n=[],i="^"+e.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(o,s,l)=>(n.push({paramName:s,isOptional:l!=null}),l?"/?([^\\/]+)?":"/([^\\/]+)"));return e.endsWith("*")?(n.push({paramName:"*"}),i+=e==="*"||e==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):r?i+="\\/*$":e!==""&&e!=="/"&&(i+="(?:(?=\\/|$))"),[new RegExp(i,t?void 0:"i"),n]}function Zk(e){try{return e.split("/").map(t=>decodeURIComponent(t).replace(/\//g,"%2F")).join("/")}catch(t){return Sg(!1,'The URL path "'+e+'" could not be decoded because it is is a malformed URL segment. This is probably due to a bad percent '+("encoding ("+t+").")),e}}function wo(e,t){if(t==="/")return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let r=t.endsWith("/")?t.length-1:t.length,n=e.charAt(r);return n&&n!=="/"?null:e.slice(r)||"/"}const eN=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,tN=e=>eN.test(e);function rN(e,t){t===void 0&&(t="/");let{pathname:r,search:n="",hash:i=""}=typeof e=="string"?Jo(e):e,a;if(r)if(tN(r))a=r;else{if(r.includes("//")){let o=r;r=r.replace(/\/\/+/g,"/"),Sg(!1,"Pathnames cannot have embedded double slashes - normalizing "+(o+" -> "+r))}r.startsWith("/")?a=ox(r.substring(1),"/"):a=ox(r,t)}else a=t;return{pathname:a,search:aN(n),hash:oN(i)}}function ox(e,t){let r=t.replace(/\/+$/,"").split("/");return e.split("/").forEach(i=>{i===".."?r.length>1&&r.pop():i!=="."&&r.push(i)}),r.length>1?r.join("/"):"/"}function ep(e,t,r,n){return"Cannot include a '"+e+"' character in a manually specified "+("`to."+t+"` field ["+JSON.stringify(n)+"]. Please separate it out to the ")+("`to."+r+"` field. Alternatively you may provide the full path as ")+'a string in and the router will parse it for you.'}function nN(e){return e.filter((t,r)=>r===0||t.route.path&&t.route.path.length>0)}function iA(e,t){let r=nN(e);return t?r.map((n,i)=>i===r.length-1?n.pathname:n.pathnameBase):r.map(n=>n.pathnameBase)}function aA(e,t,r,n){n===void 0&&(n=!1);let i;typeof e=="string"?i=Jo(e):(i=bl({},e),We(!i.pathname||!i.pathname.includes("?"),ep("?","pathname","search",i)),We(!i.pathname||!i.pathname.includes("#"),ep("#","pathname","hash",i)),We(!i.search||!i.search.includes("#"),ep("#","search","hash",i)));let a=e===""||i.pathname==="",o=a?"/":i.pathname,s;if(o==null)s=r;else{let c=t.length-1;if(!n&&o.startsWith("..")){let d=o.split("/");for(;d[0]==="..";)d.shift(),c-=1;i.pathname=d.join("/")}s=c>=0?t[c]:"/"}let l=rN(i,s),u=o&&o!=="/"&&o.endsWith("/"),f=(a||o===".")&&r.endsWith("/");return!l.pathname.endsWith("/")&&(u||f)&&(l.pathname+="/"),l}const si=e=>e.join("/").replace(/\/\/+/g,"/"),iN=e=>e.replace(/\/+$/,"").replace(/^\/*/,"/"),aN=e=>!e||e==="?"?"":e.startsWith("?")?e:"?"+e,oN=e=>!e||e==="#"?"":e.startsWith("#")?e:"#"+e;function sN(e){return e!=null&&typeof e.status=="number"&&typeof e.statusText=="string"&&typeof e.internal=="boolean"&&"data"in e}const oA=["post","put","patch","delete"];new Set(oA);const lN=["get",...oA];new Set(lN);/** + * React Router v6.30.3 + * + * Copyright (c) Remix Software Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE.md file in the root directory of this source tree. + * + * @license MIT + */function xl(){return xl=Object.assign?Object.assign.bind():function(e){for(var t=1;t{s.current=!0}),j.useCallback(function(u,f){if(f===void 0&&(f={}),!s.current)return;if(typeof u=="number"){n.go(u);return}let c=aA(u,JSON.parse(o),a,f.relative==="path");e==null&&t!=="/"&&(c.pathname=c.pathname==="/"?t:si([t,c.pathname])),(f.replace?n.replace:n.push)(c,f.state,f)},[t,n,o,a,e])}function Sd(e,t){let{relative:r}=t===void 0?{}:t,{future:n}=j.useContext(mi),{matches:i}=j.useContext(ga),{pathname:a}=hu(),o=JSON.stringify(iA(i,n.v7_relativeSplatPath));return j.useMemo(()=>aA(e,JSON.parse(o),a,r==="path"),[e,o,a,r])}function dN(e,t){return hN(e,t)}function hN(e,t,r,n){du()||We(!1);let{navigator:i}=j.useContext(mi),{matches:a}=j.useContext(ga),o=a[a.length-1],s=o?o.params:{};o&&o.pathname;let l=o?o.pathnameBase:"/";o&&o.route;let u=hu(),f;if(t){var c;let y=typeof t=="string"?Jo(t):t;l==="/"||(c=y.pathname)!=null&&c.startsWith(l)||We(!1),f=y}else f=u;let d=f.pathname||"/",h=d;if(l!=="/"){let y=l.replace(/^\//,"").split("/");h="/"+d.replace(/^\//,"").split("/").slice(y.length).join("/")}let p=Fk(e,{pathname:h}),m=gN(p&&p.map(y=>Object.assign({},y,{params:Object.assign({},s,y.params),pathname:si([l,i.encodeLocation?i.encodeLocation(y.pathname).pathname:y.pathname]),pathnameBase:y.pathnameBase==="/"?l:si([l,i.encodeLocation?i.encodeLocation(y.pathnameBase).pathname:y.pathnameBase])})),a,r,n);return t&&m?j.createElement(wd.Provider,{value:{location:xl({pathname:"/",search:"",hash:"",state:null,key:"default"},f),navigationType:Qn.Pop}},m):m}function pN(){let e=SN(),t=sN(e)?e.status+" "+e.statusText:e instanceof Error?e.message:JSON.stringify(e),r=e instanceof Error?e.stack:null,i={padding:"0.5rem",backgroundColor:"rgba(200,200,200, 0.5)"};return j.createElement(j.Fragment,null,j.createElement("h2",null,"Unexpected Application Error!"),j.createElement("h3",{style:{fontStyle:"italic"}},t),r?j.createElement("pre",{style:i},r):null,null)}const mN=j.createElement(pN,null);class yN extends j.Component{constructor(t){super(t),this.state={location:t.location,revalidation:t.revalidation,error:t.error}}static getDerivedStateFromError(t){return{error:t}}static getDerivedStateFromProps(t,r){return r.location!==t.location||r.revalidation!=="idle"&&t.revalidation==="idle"?{error:t.error,location:t.location,revalidation:t.revalidation}:{error:t.error!==void 0?t.error:r.error,location:r.location,revalidation:t.revalidation||r.revalidation}}componentDidCatch(t,r){console.error("React Router caught the following error during render",t,r)}render(){return this.state.error!==void 0?j.createElement(ga.Provider,{value:this.props.routeContext},j.createElement(lA.Provider,{value:this.state.error,children:this.props.component})):this.props.children}}function vN(e){let{routeContext:t,match:r,children:n}=e,i=j.useContext(xd);return i&&i.static&&i.staticContext&&(r.route.errorElement||r.route.ErrorBoundary)&&(i.staticContext._deepestRenderedBoundaryId=r.route.id),j.createElement(ga.Provider,{value:t},n)}function gN(e,t,r,n){var i;if(t===void 0&&(t=[]),r===void 0&&(r=null),n===void 0&&(n=null),e==null){var a;if(!r)return null;if(r.errors)e=r.matches;else if((a=n)!=null&&a.v7_partialHydration&&t.length===0&&!r.initialized&&r.matches.length>0)e=r.matches;else return null}let o=e,s=(i=r)==null?void 0:i.errors;if(s!=null){let f=o.findIndex(c=>c.route.id&&(s==null?void 0:s[c.route.id])!==void 0);f>=0||We(!1),o=o.slice(0,Math.min(o.length,f+1))}let l=!1,u=-1;if(r&&n&&n.v7_partialHydration)for(let f=0;f=0?o=o.slice(0,u+1):o=[o[0]];break}}}return o.reduceRight((f,c,d)=>{let h,p=!1,m=null,y=null;r&&(h=s&&c.route.id?s[c.route.id]:void 0,m=c.route.errorElement||mN,l&&(u<0&&d===0?(_N("route-fallback"),p=!0,y=null):u===d&&(p=!0,y=c.route.hydrateFallbackElement||null)));let v=t.concat(o.slice(0,d+1)),g=()=>{let b;return h?b=m:p?b=y:c.route.Component?b=j.createElement(c.route.Component,null):c.route.element?b=c.route.element:b=f,j.createElement(vN,{match:c,routeContext:{outlet:f,matches:v,isDataRoute:r!=null},children:b})};return r&&(c.route.ErrorBoundary||c.route.errorElement||d===0)?j.createElement(yN,{location:r.location,revalidation:r.revalidation,component:m,error:h,children:g(),routeContext:{outlet:null,matches:v,isDataRoute:!0}}):g()},null)}var cA=function(e){return e.UseBlocker="useBlocker",e.UseRevalidator="useRevalidator",e.UseNavigateStable="useNavigate",e}(cA||{}),fA=function(e){return e.UseBlocker="useBlocker",e.UseLoaderData="useLoaderData",e.UseActionData="useActionData",e.UseRouteError="useRouteError",e.UseNavigation="useNavigation",e.UseRouteLoaderData="useRouteLoaderData",e.UseMatches="useMatches",e.UseRevalidator="useRevalidator",e.UseNavigateStable="useNavigate",e.UseRouteId="useRouteId",e}(fA||{});function bN(e){let t=j.useContext(xd);return t||We(!1),t}function xN(e){let t=j.useContext(sA);return t||We(!1),t}function wN(e){let t=j.useContext(ga);return t||We(!1),t}function dA(e){let t=wN(),r=t.matches[t.matches.length-1];return r.route.id||We(!1),r.route.id}function SN(){var e;let t=j.useContext(lA),r=xN(),n=dA();return t!==void 0?t:(e=r.errors)==null?void 0:e[n]}function ON(){let{router:e}=bN(cA.UseNavigateStable),t=dA(fA.UseNavigateStable),r=j.useRef(!1);return uA(()=>{r.current=!0}),j.useCallback(function(i,a){a===void 0&&(a={}),r.current&&(typeof i=="number"?e.navigate(i):e.navigate(i,xl({fromRouteId:t},a)))},[e,t])}const sx={};function _N(e,t,r){sx[e]||(sx[e]=!0)}function PN(e,t){e==null||e.v7_startTransition,e==null||e.v7_relativeSplatPath}function ji(e){We(!1)}function AN(e){let{basename:t="/",children:r=null,location:n,navigationType:i=Qn.Pop,navigator:a,static:o=!1,future:s}=e;du()&&We(!1);let l=t.replace(/^\/*/,"/"),u=j.useMemo(()=>({basename:l,navigator:a,static:o,future:xl({v7_relativeSplatPath:!1},s)}),[l,s,a,o]);typeof n=="string"&&(n=Jo(n));let{pathname:f="/",search:c="",hash:d="",state:h=null,key:p="default"}=n,m=j.useMemo(()=>{let y=wo(f,l);return y==null?null:{location:{pathname:y,search:c,hash:d,state:h,key:p},navigationType:i}},[l,f,c,d,h,p,i]);return m==null?null:j.createElement(mi.Provider,{value:u},j.createElement(wd.Provider,{children:r,value:m}))}function EN(e){let{children:t,location:r}=e;return dN(zm(t),r)}new Promise(()=>{});function zm(e,t){t===void 0&&(t=[]);let r=[];return j.Children.forEach(e,(n,i)=>{if(!j.isValidElement(n))return;let a=[...t,i];if(n.type===j.Fragment){r.push.apply(r,zm(n.props.children,a));return}n.type!==ji&&We(!1),!n.props.index||!n.props.children||We(!1);let o={id:n.props.id||a.join("-"),caseSensitive:n.props.caseSensitive,element:n.props.element,Component:n.props.Component,index:n.props.index,path:n.props.path,loader:n.props.loader,action:n.props.action,errorElement:n.props.errorElement,ErrorBoundary:n.props.ErrorBoundary,hasErrorBoundary:n.props.ErrorBoundary!=null||n.props.errorElement!=null,shouldRevalidate:n.props.shouldRevalidate,handle:n.props.handle,lazy:n.props.lazy};n.props.children&&(o.children=zm(n.props.children,a)),r.push(o)}),r}/** + * React Router DOM v6.30.3 + * + * Copyright (c) Remix Software Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE.md file in the root directory of this source tree. + * + * @license MIT + */function Zc(){return Zc=Object.assign?Object.assign.bind():function(e){for(var t=1;t=0)&&(r[i]=e[i]);return r}function jN(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function TN(e,t){return e.button===0&&(!t||t==="_self")&&!jN(e)}const CN=["onClick","relative","reloadDocument","replace","state","target","to","preventScrollReset","viewTransition"],$N=["aria-current","caseSensitive","className","end","style","to","viewTransition","children"],kN="6";try{window.__reactRouterVersion=kN}catch{}const NN=j.createContext({isTransitioning:!1}),MN="startTransition",lx=W2[MN];function IN(e){let{basename:t,children:r,future:n,window:i}=e,a=j.useRef();a.current==null&&(a.current=Dk({window:i,v5Compat:!0}));let o=a.current,[s,l]=j.useState({action:o.action,location:o.location}),{v7_startTransition:u}=n||{},f=j.useCallback(c=>{u&&lx?lx(()=>l(c)):l(c)},[l,u]);return j.useLayoutEffect(()=>o.listen(f),[o,f]),j.useEffect(()=>PN(n),[n]),j.createElement(AN,{basename:t,children:r,location:s.location,navigationType:s.action,navigator:o,future:n})}const RN=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u",DN=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,LN=j.forwardRef(function(t,r){let{onClick:n,relative:i,reloadDocument:a,replace:o,state:s,target:l,to:u,preventScrollReset:f,viewTransition:c}=t,d=hA(t,CN),{basename:h}=j.useContext(mi),p,m=!1;if(typeof u=="string"&&DN.test(u)&&(p=u,RN))try{let b=new URL(window.location.href),w=u.startsWith("//")?new URL(b.protocol+u):new URL(u),x=wo(w.pathname,h);w.origin===b.origin&&x!=null?u=x+w.search+w.hash:m=!0}catch{}let y=uN(u,{relative:i}),v=UN(u,{replace:o,state:s,target:l,preventScrollReset:f,relative:i,viewTransition:c});function g(b){n&&n(b),b.defaultPrevented||v(b)}return j.createElement("a",Zc({},d,{href:p||y,onClick:m||a?n:g,ref:r,target:l}))}),BN=j.forwardRef(function(t,r){let{"aria-current":n="page",caseSensitive:i=!1,className:a="",end:o=!1,style:s,to:l,viewTransition:u,children:f}=t,c=hA(t,$N),d=Sd(l,{relative:c.relative}),h=hu(),p=j.useContext(sA),{navigator:m,basename:y}=j.useContext(mi),v=p!=null&&zN(d)&&u===!0,g=m.encodeLocation?m.encodeLocation(d).pathname:d.pathname,b=h.pathname,w=p&&p.navigation&&p.navigation.location?p.navigation.location.pathname:null;i||(b=b.toLowerCase(),w=w?w.toLowerCase():null,g=g.toLowerCase()),w&&y&&(w=wo(w,y)||w);const x=g!=="/"&&g.endsWith("/")?g.length-1:g.length;let S=b===g||!o&&b.startsWith(g)&&b.charAt(x)==="/",_=w!=null&&(w===g||!o&&w.startsWith(g)&&w.charAt(g.length)==="/"),P={isActive:S,isPending:_,isTransitioning:v},A=S?n:void 0,C;typeof a=="function"?C=a(P):C=[a,S?"active":null,_?"pending":null,v?"transitioning":null].filter(Boolean).join(" ");let N=typeof s=="function"?s(P):s;return j.createElement(LN,Zc({},c,{"aria-current":A,className:C,ref:r,style:N,to:l,viewTransition:u}),typeof f=="function"?f(P):f)});var Wm;(function(e){e.UseScrollRestoration="useScrollRestoration",e.UseSubmit="useSubmit",e.UseSubmitFetcher="useSubmitFetcher",e.UseFetcher="useFetcher",e.useViewTransitionState="useViewTransitionState"})(Wm||(Wm={}));var ux;(function(e){e.UseFetcher="useFetcher",e.UseFetchers="useFetchers",e.UseScrollRestoration="useScrollRestoration"})(ux||(ux={}));function FN(e){let t=j.useContext(xd);return t||We(!1),t}function UN(e,t){let{target:r,replace:n,state:i,preventScrollReset:a,relative:o,viewTransition:s}=t===void 0?{}:t,l=cN(),u=hu(),f=Sd(e,{relative:o});return j.useCallback(c=>{if(TN(c,r)){c.preventDefault();let d=n!==void 0?n:Jc(u)===Jc(f);l(e,{replace:d,state:i,preventScrollReset:a,relative:o,viewTransition:s})}},[u,l,f,n,i,r,e,a,o,s])}function zN(e,t){t===void 0&&(t={});let r=j.useContext(NN);r==null&&We(!1);let{basename:n}=FN(Wm.useViewTransitionState),i=Sd(e,{relative:t.relative});if(!r.isTransitioning)return!1;let a=wo(r.currentLocation.pathname,n)||r.currentLocation.pathname,o=wo(r.nextLocation.pathname,n)||r.nextLocation.pathname;return Um(i.pathname,o)!=null||Um(i.pathname,a)!=null}/** + * @license lucide-react v0.303.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */var WN={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};/** + * @license lucide-react v0.303.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const HN=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase().trim(),Te=(e,t)=>{const r=j.forwardRef(({color:n="currentColor",size:i=24,strokeWidth:a=2,absoluteStrokeWidth:o,className:s="",children:l,...u},f)=>j.createElement("svg",{ref:f,...WN,width:i,height:i,stroke:n,strokeWidth:o?Number(a)*24/Number(i):a,className:["lucide",`lucide-${HN(e)}`,s].join(" "),...u},[...t.map(([c,d])=>j.createElement(c,d)),...Array.isArray(l)?l:[l]]));return r.displayName=`${e}`,r};/** + * @license lucide-react v0.303.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Og=Te("AlertCircle",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["line",{x1:"12",x2:"12",y1:"8",y2:"12",key:"1pkeuh"}],["line",{x1:"12",x2:"12.01",y1:"16",y2:"16",key:"4dfq90"}]]);/** + * @license lucide-react v0.303.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Od=Te("CheckCircle",[["path",{d:"M22 11.08V12a10 10 0 1 1-5.93-9.14",key:"g774vq"}],["path",{d:"m9 11 3 3L22 4",key:"1pflzl"}]]);/** + * @license lucide-react v0.303.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const pA=Te("ChevronLeft",[["path",{d:"m15 18-6-6 6-6",key:"1wnfg3"}]]);/** + * @license lucide-react v0.303.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const mA=Te("ChevronRight",[["path",{d:"m9 18 6-6-6-6",key:"mthhwq"}]]);/** + * @license lucide-react v0.303.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const So=Te("Clock",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["polyline",{points:"12 6 12 12 16 14",key:"68esgv"}]]);/** + * @license lucide-react v0.303.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const yA=Te("Download",[["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["polyline",{points:"7 10 12 15 17 10",key:"2ggqvy"}],["line",{x1:"12",x2:"12",y1:"15",y2:"3",key:"1vk2je"}]]);/** + * @license lucide-react v0.303.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const qN=Te("ExternalLink",[["path",{d:"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6",key:"a6xqqp"}],["polyline",{points:"15 3 21 3 21 9",key:"mznyad"}],["line",{x1:"10",x2:"21",y1:"14",y2:"3",key:"18c3s4"}]]);/** + * @license lucide-react v0.303.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const KN=Te("EyeOff",[["path",{d:"M9.88 9.88a3 3 0 1 0 4.24 4.24",key:"1jxqfv"}],["path",{d:"M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68",key:"9wicm4"}],["path",{d:"M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61",key:"1jreej"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22",key:"a6p6uj"}]]);/** + * @license lucide-react v0.303.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const VN=Te("Eye",[["path",{d:"M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z",key:"rwhkz3"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]]);/** + * @license lucide-react v0.303.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const GN=Te("Filter",[["polygon",{points:"22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3",key:"1yg77f"}]]);/** + * @license lucide-react v0.303.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const XN=Te("HardDrive",[["line",{x1:"22",x2:"2",y1:"12",y2:"12",key:"1y58io"}],["path",{d:"M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z",key:"oot6mr"}],["line",{x1:"6",x2:"6.01",y1:"16",y2:"16",key:"sgf278"}],["line",{x1:"10",x2:"10.01",y1:"16",y2:"16",key:"1l4acy"}]]);/** + * @license lucide-react v0.303.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const vA=Te("Image",[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2",key:"1m3agn"}],["circle",{cx:"9",cy:"9",r:"2",key:"af1f0g"}],["path",{d:"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21",key:"1xmnt7"}]]);/** + * @license lucide-react v0.303.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const QN=Te("Key",[["circle",{cx:"7.5",cy:"15.5",r:"5.5",key:"yqb3hr"}],["path",{d:"m21 2-9.6 9.6",key:"1j0ho8"}],["path",{d:"m15.5 7.5 3 3L22 7l-3-3",key:"1rn1fs"}]]);/** + * @license lucide-react v0.303.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const YN=Te("LayoutDashboard",[["rect",{width:"7",height:"9",x:"3",y:"3",rx:"1",key:"10lvy0"}],["rect",{width:"7",height:"5",x:"14",y:"3",rx:"1",key:"16une8"}],["rect",{width:"7",height:"9",x:"14",y:"12",rx:"1",key:"1hutg5"}],["rect",{width:"7",height:"5",x:"3",y:"16",rx:"1",key:"ldoo1y"}]]);/** + * @license lucide-react v0.303.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const _g=Te("Leaf",[["path",{d:"M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10Z",key:"nnexq3"}],["path",{d:"M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12",key:"mt58a7"}]]);/** + * @license lucide-react v0.303.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const cx=Te("Package",[["path",{d:"m7.5 4.27 9 5.15",key:"1c824w"}],["path",{d:"M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z",key:"hh9hay"}],["path",{d:"m3.3 7 8.7 5 8.7-5",key:"g66t2b"}],["path",{d:"M12 22V12",key:"d0xqtd"}]]);/** + * @license lucide-react v0.303.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const fx=Te("Pause",[["rect",{width:"4",height:"16",x:"6",y:"4",key:"iffhe4"}],["rect",{width:"4",height:"16",x:"14",y:"4",key:"sjin7j"}]]);/** + * @license lucide-react v0.303.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ef=Te("Play",[["polygon",{points:"5 3 19 12 5 21 5 3",key:"191637"}]]);/** + * @license lucide-react v0.303.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const JN=Te("Plus",[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"M12 5v14",key:"s699le"}]]);/** + * @license lucide-react v0.303.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const gA=Te("RefreshCw",[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",key:"v9h5vc"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",key:"3uifl3"}],["path",{d:"M8 16H3v5",key:"1cv678"}]]);/** + * @license lucide-react v0.303.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const bA=Te("Search",[["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}],["path",{d:"m21 21-4.3-4.3",key:"1qie3q"}]]);/** + * @license lucide-react v0.303.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ZN=Te("Settings",[["path",{d:"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z",key:"1qme2f"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]]);/** + * @license lucide-react v0.303.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const tf=Te("Trash2",[["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6",key:"4alrt4"}],["path",{d:"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2",key:"v07s0e"}],["line",{x1:"10",x2:"10",y1:"11",y2:"17",key:"1uufr5"}],["line",{x1:"14",x2:"14",y1:"11",y2:"17",key:"xtxkd"}]]);/** + * @license lucide-react v0.303.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const eM=Te("Upload",[["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["polyline",{points:"17 8 12 3 7 8",key:"t8dd8p"}],["line",{x1:"12",x2:"12",y1:"3",y2:"15",key:"widbto"}]]);/** + * @license lucide-react v0.303.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Pg=Te("XCircle",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m15 9-6 6",key:"1uzhvr"}],["path",{d:"m9 9 6 6",key:"z0biqf"}]]);/** + * @license lucide-react v0.303.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const tM=Te("X",[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]]);function xA(e){var t,r,n="";if(typeof e=="string"||typeof e=="number")n+=e;else if(typeof e=="object")if(Array.isArray(e)){var i=e.length;for(t=0;t-1}var eR=ZI,tR=Pd;function rR(e,t){var r=this.__data__,n=tR(r,e);return n<0?(++this.size,r.push([e,t])):r[n][1]=t,this}var nR=rR,iR=FI,aR=GI,oR=YI,sR=eR,lR=nR;function rs(e){var t=-1,r=e==null?0:e.length;for(this.clear();++t0?1:-1},Li=function(t){return ca(t)&&t.indexOf("%")===t.length-1},V=function(t){return jD(t)&&!mu(t)},kD=function(t){return ue(t)},nt=function(t){return V(t)||ca(t)},ND=0,yu=function(t){var r=++ND;return"".concat(t||"").concat(r)},Et=function(t,r){var n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:0,i=arguments.length>3&&arguments[3]!==void 0?arguments[3]:!1;if(!V(t)&&!ca(t))return n;var a;if(Li(t)){var o=t.indexOf("%");a=r*parseFloat(t.slice(0,o))/100}else a=+t;return mu(a)&&(a=n),i&&a>r&&(a=r),a},Ea=function(t){if(!t)return null;var r=Object.keys(t);return r&&r.length?t[r[0]]:null},MD=function(t){if(!Array.isArray(t))return!1;for(var r=t.length,n={},i=0;i=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function UD(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}var Ox={click:"onClick",mousedown:"onMouseDown",mouseup:"onMouseUp",mouseover:"onMouseOver",mousemove:"onMouseMove",mouseout:"onMouseOut",mouseenter:"onMouseEnter",mouseleave:"onMouseLeave",touchcancel:"onTouchCancel",touchend:"onTouchEnd",touchmove:"onTouchMove",touchstart:"onTouchStart",contextmenu:"onContextMenu",dblclick:"onDoubleClick"},fn=function(t){return typeof t=="string"?t:t?t.displayName||t.name||"Component":""},_x=null,np=null,Dg=function e(t){if(t===_x&&Array.isArray(np))return np;var r=[];return j.Children.forEach(t,function(n){ue(n)||(OD.isFragment(n)?r=r.concat(e(n.props.children)):r.push(n))}),np=r,_x=t,r};function pr(e,t){var r=[],n=[];return Array.isArray(t)?n=t.map(function(i){return fn(i)}):n=[fn(t)],Dg(e).forEach(function(i){var a=Jt(i,"type.displayName")||Jt(i,"type.name");n.indexOf(a)!==-1&&r.push(i)}),r}function Gt(e,t){var r=pr(e,t);return r&&r[0]}var Px=function(t){if(!t||!t.props)return!1;var r=t.props,n=r.width,i=r.height;return!(!V(n)||n<=0||!V(i)||i<=0)},zD=["a","altGlyph","altGlyphDef","altGlyphItem","animate","animateColor","animateMotion","animateTransform","circle","clipPath","color-profile","cursor","defs","desc","ellipse","feBlend","feColormatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence","filter","font","font-face","font-face-format","font-face-name","font-face-url","foreignObject","g","glyph","glyphRef","hkern","image","line","lineGradient","marker","mask","metadata","missing-glyph","mpath","path","pattern","polygon","polyline","radialGradient","rect","script","set","stop","style","svg","switch","symbol","text","textPath","title","tref","tspan","use","view","vkern"],WD=function(t){return t&&t.type&&ca(t.type)&&zD.indexOf(t.type)>=0},HD=function(t,r,n,i){var a,o=(a=rp==null?void 0:rp[i])!==null&&a!==void 0?a:[];return r.startsWith("data-")||!re(t)&&(i&&o.includes(r)||DD.includes(r))||n&&Rg.includes(r)},te=function(t,r,n){if(!t||typeof t=="function"||typeof t=="boolean")return null;var i=t;if(j.isValidElement(t)&&(i=t.props),!es(i))return null;var a={};return Object.keys(i).forEach(function(o){var s;HD((s=i)===null||s===void 0?void 0:s[o],o,r,n)&&(a[o]=i[o])}),a},Km=function e(t,r){if(t===r)return!0;var n=j.Children.count(t);if(n!==j.Children.count(r))return!1;if(n===0)return!0;if(n===1)return Ax(Array.isArray(t)?t[0]:t,Array.isArray(r)?r[0]:r);for(var i=0;i=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function XD(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}function Gm(e){var t=e.children,r=e.width,n=e.height,i=e.viewBox,a=e.className,o=e.style,s=e.title,l=e.desc,u=GD(e,VD),f=i||{width:r,height:n,x:0,y:0},c=oe("recharts-surface",a);return T.createElement("svg",Vm({},te(u,!0,"svg"),{className:c,width:r,height:n,style:o,viewBox:"".concat(f.x," ").concat(f.y," ").concat(f.width," ").concat(f.height)}),T.createElement("title",null,s),T.createElement("desc",null,l),t)}var QD=["children","className"];function Xm(){return Xm=Object.assign?Object.assign.bind():function(e){for(var t=1;t=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function JD(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}var ge=T.forwardRef(function(e,t){var r=e.children,n=e.className,i=YD(e,QD),a=oe("recharts-layer",n);return T.createElement("g",Xm({className:a},te(i,!0),{ref:t}),r)}),dn=function(t,r){for(var n=arguments.length,i=new Array(n>2?n-2:0),a=2;ai?0:i+t),r=r>i?i:r,r<0&&(r+=i),i=t>r?0:r-t>>>0,t>>>=0;for(var a=Array(i);++n=n?e:tL(e,t,r)}var nL=rL,iL="\\ud800-\\udfff",aL="\\u0300-\\u036f",oL="\\ufe20-\\ufe2f",sL="\\u20d0-\\u20ff",lL=aL+oL+sL,uL="\\ufe0e\\ufe0f",cL="\\u200d",fL=RegExp("["+cL+iL+lL+uL+"]");function dL(e){return fL.test(e)}var NA=dL;function hL(e){return e.split("")}var pL=hL,MA="\\ud800-\\udfff",mL="\\u0300-\\u036f",yL="\\ufe20-\\ufe2f",vL="\\u20d0-\\u20ff",gL=mL+yL+vL,bL="\\ufe0e\\ufe0f",xL="["+MA+"]",Qm="["+gL+"]",Ym="\\ud83c[\\udffb-\\udfff]",wL="(?:"+Qm+"|"+Ym+")",IA="[^"+MA+"]",RA="(?:\\ud83c[\\udde6-\\uddff]){2}",DA="[\\ud800-\\udbff][\\udc00-\\udfff]",SL="\\u200d",LA=wL+"?",BA="["+bL+"]?",OL="(?:"+SL+"(?:"+[IA,RA,DA].join("|")+")"+BA+LA+")*",_L=BA+LA+OL,PL="(?:"+[IA+Qm+"?",Qm,RA,DA,xL].join("|")+")",AL=RegExp(Ym+"(?="+Ym+")|"+PL+_L,"g");function EL(e){return e.match(AL)||[]}var jL=EL,TL=pL,CL=NA,$L=jL;function kL(e){return CL(e)?$L(e):TL(e)}var NL=kL,ML=nL,IL=NA,RL=NL,DL=EA;function LL(e){return function(t){t=DL(t);var r=IL(t)?RL(t):void 0,n=r?r[0]:t.charAt(0),i=r?ML(r,1).join(""):t.slice(1);return n[e]()+i}}var BL=LL,FL=BL,UL=FL("toUpperCase"),zL=UL;const Bd=Se(zL);function Ae(e){return function(){return e}}const FA=Math.cos,nf=Math.sin,Nr=Math.sqrt,af=Math.PI,Fd=2*af,Jm=Math.PI,Zm=2*Jm,Ti=1e-6,WL=Zm-Ti;function UA(e){this._+=e[0];for(let t=1,r=e.length;t=0))throw new Error(`invalid digits: ${e}`);if(t>15)return UA;const r=10**t;return function(n){this._+=n[0];for(let i=1,a=n.length;iTi)if(!(Math.abs(c*l-u*f)>Ti)||!a)this._append`L${this._x1=t},${this._y1=r}`;else{let h=n-o,p=i-s,m=l*l+u*u,y=h*h+p*p,v=Math.sqrt(m),g=Math.sqrt(d),b=a*Math.tan((Jm-Math.acos((m+d-y)/(2*v*g)))/2),w=b/g,x=b/v;Math.abs(w-1)>Ti&&this._append`L${t+w*f},${r+w*c}`,this._append`A${a},${a},0,0,${+(c*h>f*p)},${this._x1=t+x*l},${this._y1=r+x*u}`}}arc(t,r,n,i,a,o){if(t=+t,r=+r,n=+n,o=!!o,n<0)throw new Error(`negative radius: ${n}`);let s=n*Math.cos(i),l=n*Math.sin(i),u=t+s,f=r+l,c=1^o,d=o?i-a:a-i;this._x1===null?this._append`M${u},${f}`:(Math.abs(this._x1-u)>Ti||Math.abs(this._y1-f)>Ti)&&this._append`L${u},${f}`,n&&(d<0&&(d=d%Zm+Zm),d>WL?this._append`A${n},${n},0,1,${c},${t-s},${r-l}A${n},${n},0,1,${c},${this._x1=u},${this._y1=f}`:d>Ti&&this._append`A${n},${n},0,${+(d>=Jm)},${c},${this._x1=t+n*Math.cos(a)},${this._y1=r+n*Math.sin(a)}`)}rect(t,r,n,i){this._append`M${this._x0=this._x1=+t},${this._y0=this._y1=+r}h${n=+n}v${+i}h${-n}Z`}toString(){return this._}}function Lg(e){let t=3;return e.digits=function(r){if(!arguments.length)return t;if(r==null)t=null;else{const n=Math.floor(r);if(!(n>=0))throw new RangeError(`invalid digits: ${r}`);t=n}return e},()=>new qL(t)}function Bg(e){return typeof e=="object"&&"length"in e?e:Array.from(e)}function zA(e){this._context=e}zA.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){(this._line||this._line!==0&&this._point===1)&&this._context.closePath(),this._line=1-this._line},point:function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1,this._line?this._context.lineTo(e,t):this._context.moveTo(e,t);break;case 1:this._point=2;default:this._context.lineTo(e,t);break}}};function Ud(e){return new zA(e)}function WA(e){return e[0]}function HA(e){return e[1]}function qA(e,t){var r=Ae(!0),n=null,i=Ud,a=null,o=Lg(s);e=typeof e=="function"?e:e===void 0?WA:Ae(e),t=typeof t=="function"?t:t===void 0?HA:Ae(t);function s(l){var u,f=(l=Bg(l)).length,c,d=!1,h;for(n==null&&(a=i(h=o())),u=0;u<=f;++u)!(u=h;--p)s.point(b[p],w[p]);s.lineEnd(),s.areaEnd()}v&&(b[d]=+e(y,d,c),w[d]=+t(y,d,c),s.point(n?+n(y,d,c):b[d],r?+r(y,d,c):w[d]))}if(g)return s=null,g+""||null}function f(){return qA().defined(i).curve(o).context(a)}return u.x=function(c){return arguments.length?(e=typeof c=="function"?c:Ae(+c),n=null,u):e},u.x0=function(c){return arguments.length?(e=typeof c=="function"?c:Ae(+c),u):e},u.x1=function(c){return arguments.length?(n=c==null?null:typeof c=="function"?c:Ae(+c),u):n},u.y=function(c){return arguments.length?(t=typeof c=="function"?c:Ae(+c),r=null,u):t},u.y0=function(c){return arguments.length?(t=typeof c=="function"?c:Ae(+c),u):t},u.y1=function(c){return arguments.length?(r=c==null?null:typeof c=="function"?c:Ae(+c),u):r},u.lineX0=u.lineY0=function(){return f().x(e).y(t)},u.lineY1=function(){return f().x(e).y(r)},u.lineX1=function(){return f().x(n).y(t)},u.defined=function(c){return arguments.length?(i=typeof c=="function"?c:Ae(!!c),u):i},u.curve=function(c){return arguments.length?(o=c,a!=null&&(s=o(a)),u):o},u.context=function(c){return arguments.length?(c==null?a=s=null:s=o(a=c),u):a},u}class KA{constructor(t,r){this._context=t,this._x=r}areaStart(){this._line=0}areaEnd(){this._line=NaN}lineStart(){this._point=0}lineEnd(){(this._line||this._line!==0&&this._point===1)&&this._context.closePath(),this._line=1-this._line}point(t,r){switch(t=+t,r=+r,this._point){case 0:{this._point=1,this._line?this._context.lineTo(t,r):this._context.moveTo(t,r);break}case 1:this._point=2;default:{this._x?this._context.bezierCurveTo(this._x0=(this._x0+t)/2,this._y0,this._x0,r,t,r):this._context.bezierCurveTo(this._x0,this._y0=(this._y0+r)/2,t,this._y0,t,r);break}}this._x0=t,this._y0=r}}function KL(e){return new KA(e,!0)}function VL(e){return new KA(e,!1)}const Fg={draw(e,t){const r=Nr(t/af);e.moveTo(r,0),e.arc(0,0,r,0,Fd)}},GL={draw(e,t){const r=Nr(t/5)/2;e.moveTo(-3*r,-r),e.lineTo(-r,-r),e.lineTo(-r,-3*r),e.lineTo(r,-3*r),e.lineTo(r,-r),e.lineTo(3*r,-r),e.lineTo(3*r,r),e.lineTo(r,r),e.lineTo(r,3*r),e.lineTo(-r,3*r),e.lineTo(-r,r),e.lineTo(-3*r,r),e.closePath()}},VA=Nr(1/3),XL=VA*2,QL={draw(e,t){const r=Nr(t/XL),n=r*VA;e.moveTo(0,-r),e.lineTo(n,0),e.lineTo(0,r),e.lineTo(-n,0),e.closePath()}},YL={draw(e,t){const r=Nr(t),n=-r/2;e.rect(n,n,r,r)}},JL=.8908130915292852,GA=nf(af/10)/nf(7*af/10),ZL=nf(Fd/10)*GA,e4=-FA(Fd/10)*GA,t4={draw(e,t){const r=Nr(t*JL),n=ZL*r,i=e4*r;e.moveTo(0,-r),e.lineTo(n,i);for(let a=1;a<5;++a){const o=Fd*a/5,s=FA(o),l=nf(o);e.lineTo(l*r,-s*r),e.lineTo(s*n-l*i,l*n+s*i)}e.closePath()}},ip=Nr(3),r4={draw(e,t){const r=-Nr(t/(ip*3));e.moveTo(0,r*2),e.lineTo(-ip*r,-r),e.lineTo(ip*r,-r),e.closePath()}},nr=-.5,ir=Nr(3)/2,ey=1/Nr(12),n4=(ey/2+1)*3,i4={draw(e,t){const r=Nr(t/n4),n=r/2,i=r*ey,a=n,o=r*ey+r,s=-a,l=o;e.moveTo(n,i),e.lineTo(a,o),e.lineTo(s,l),e.lineTo(nr*n-ir*i,ir*n+nr*i),e.lineTo(nr*a-ir*o,ir*a+nr*o),e.lineTo(nr*s-ir*l,ir*s+nr*l),e.lineTo(nr*n+ir*i,nr*i-ir*n),e.lineTo(nr*a+ir*o,nr*o-ir*a),e.lineTo(nr*s+ir*l,nr*l-ir*s),e.closePath()}};function a4(e,t){let r=null,n=Lg(i);e=typeof e=="function"?e:Ae(e||Fg),t=typeof t=="function"?t:Ae(t===void 0?64:+t);function i(){let a;if(r||(r=a=n()),e.apply(this,arguments).draw(r,+t.apply(this,arguments)),a)return r=null,a+""||null}return i.type=function(a){return arguments.length?(e=typeof a=="function"?a:Ae(a),i):e},i.size=function(a){return arguments.length?(t=typeof a=="function"?a:Ae(+a),i):t},i.context=function(a){return arguments.length?(r=a??null,i):r},i}function of(){}function sf(e,t,r){e._context.bezierCurveTo((2*e._x0+e._x1)/3,(2*e._y0+e._y1)/3,(e._x0+2*e._x1)/3,(e._y0+2*e._y1)/3,(e._x0+4*e._x1+t)/6,(e._y0+4*e._y1+r)/6)}function XA(e){this._context=e}XA.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){switch(this._point){case 3:sf(this,this._x1,this._y1);case 2:this._context.lineTo(this._x1,this._y1);break}(this._line||this._line!==0&&this._point===1)&&this._context.closePath(),this._line=1-this._line},point:function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1,this._line?this._context.lineTo(e,t):this._context.moveTo(e,t);break;case 1:this._point=2;break;case 2:this._point=3,this._context.lineTo((5*this._x0+this._x1)/6,(5*this._y0+this._y1)/6);default:sf(this,e,t);break}this._x0=this._x1,this._x1=e,this._y0=this._y1,this._y1=t}};function o4(e){return new XA(e)}function QA(e){this._context=e}QA.prototype={areaStart:of,areaEnd:of,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._y0=this._y1=this._y2=this._y3=this._y4=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:{this._context.moveTo(this._x2,this._y2),this._context.closePath();break}case 2:{this._context.moveTo((this._x2+2*this._x3)/3,(this._y2+2*this._y3)/3),this._context.lineTo((this._x3+2*this._x2)/3,(this._y3+2*this._y2)/3),this._context.closePath();break}case 3:{this.point(this._x2,this._y2),this.point(this._x3,this._y3),this.point(this._x4,this._y4);break}}},point:function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1,this._x2=e,this._y2=t;break;case 1:this._point=2,this._x3=e,this._y3=t;break;case 2:this._point=3,this._x4=e,this._y4=t,this._context.moveTo((this._x0+4*this._x1+e)/6,(this._y0+4*this._y1+t)/6);break;default:sf(this,e,t);break}this._x0=this._x1,this._x1=e,this._y0=this._y1,this._y1=t}};function s4(e){return new QA(e)}function YA(e){this._context=e}YA.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){(this._line||this._line!==0&&this._point===3)&&this._context.closePath(),this._line=1-this._line},point:function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3;var r=(this._x0+4*this._x1+e)/6,n=(this._y0+4*this._y1+t)/6;this._line?this._context.lineTo(r,n):this._context.moveTo(r,n);break;case 3:this._point=4;default:sf(this,e,t);break}this._x0=this._x1,this._x1=e,this._y0=this._y1,this._y1=t}};function l4(e){return new YA(e)}function JA(e){this._context=e}JA.prototype={areaStart:of,areaEnd:of,lineStart:function(){this._point=0},lineEnd:function(){this._point&&this._context.closePath()},point:function(e,t){e=+e,t=+t,this._point?this._context.lineTo(e,t):(this._point=1,this._context.moveTo(e,t))}};function u4(e){return new JA(e)}function jx(e){return e<0?-1:1}function Tx(e,t,r){var n=e._x1-e._x0,i=t-e._x1,a=(e._y1-e._y0)/(n||i<0&&-0),o=(r-e._y1)/(i||n<0&&-0),s=(a*i+o*n)/(n+i);return(jx(a)+jx(o))*Math.min(Math.abs(a),Math.abs(o),.5*Math.abs(s))||0}function Cx(e,t){var r=e._x1-e._x0;return r?(3*(e._y1-e._y0)/r-t)/2:t}function ap(e,t,r){var n=e._x0,i=e._y0,a=e._x1,o=e._y1,s=(a-n)/3;e._context.bezierCurveTo(n+s,i+s*t,a-s,o-s*r,a,o)}function lf(e){this._context=e}lf.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=this._t0=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x1,this._y1);break;case 3:ap(this,this._t0,Cx(this,this._t0));break}(this._line||this._line!==0&&this._point===1)&&this._context.closePath(),this._line=1-this._line},point:function(e,t){var r=NaN;if(e=+e,t=+t,!(e===this._x1&&t===this._y1)){switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(e,t):this._context.moveTo(e,t);break;case 1:this._point=2;break;case 2:this._point=3,ap(this,Cx(this,r=Tx(this,e,t)),r);break;default:ap(this,this._t0,r=Tx(this,e,t));break}this._x0=this._x1,this._x1=e,this._y0=this._y1,this._y1=t,this._t0=r}}};function ZA(e){this._context=new eE(e)}(ZA.prototype=Object.create(lf.prototype)).point=function(e,t){lf.prototype.point.call(this,t,e)};function eE(e){this._context=e}eE.prototype={moveTo:function(e,t){this._context.moveTo(t,e)},closePath:function(){this._context.closePath()},lineTo:function(e,t){this._context.lineTo(t,e)},bezierCurveTo:function(e,t,r,n,i,a){this._context.bezierCurveTo(t,e,n,r,a,i)}};function c4(e){return new lf(e)}function f4(e){return new ZA(e)}function tE(e){this._context=e}tE.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x=[],this._y=[]},lineEnd:function(){var e=this._x,t=this._y,r=e.length;if(r)if(this._line?this._context.lineTo(e[0],t[0]):this._context.moveTo(e[0],t[0]),r===2)this._context.lineTo(e[1],t[1]);else for(var n=$x(e),i=$x(t),a=0,o=1;o=0;--t)i[t]=(o[t]-i[t+1])/a[t];for(a[r-1]=(e[r]+i[r-1])/2,t=0;t=0&&(this._t=1-this._t,this._line=1-this._line)},point:function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1,this._line?this._context.lineTo(e,t):this._context.moveTo(e,t);break;case 1:this._point=2;default:{if(this._t<=0)this._context.lineTo(this._x,t),this._context.lineTo(e,t);else{var r=this._x*(1-this._t)+e*this._t;this._context.lineTo(r,this._y),this._context.lineTo(r,t)}break}}this._x=e,this._y=t}};function h4(e){return new zd(e,.5)}function p4(e){return new zd(e,0)}function m4(e){return new zd(e,1)}function Oo(e,t){if((o=e.length)>1)for(var r=1,n,i,a=e[t[0]],o,s=a.length;r=0;)r[t]=t;return r}function y4(e,t){return e[t]}function v4(e){const t=[];return t.key=e,t}function g4(){var e=Ae([]),t=ty,r=Oo,n=y4;function i(a){var o=Array.from(e.apply(this,arguments),v4),s,l=o.length,u=-1,f;for(const c of a)for(s=0,++u;s0){for(var r,n,i=0,a=e[0].length,o;i0){for(var r=0,n=e[t[0]],i,a=n.length;r0)||!((a=(i=e[t[0]]).length)>0))){for(var r=0,n=1,i,a,o;n=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function E4(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}var rE={symbolCircle:Fg,symbolCross:GL,symbolDiamond:QL,symbolSquare:YL,symbolStar:t4,symbolTriangle:r4,symbolWye:i4},j4=Math.PI/180,T4=function(t){var r="symbol".concat(Bd(t));return rE[r]||Fg},C4=function(t,r,n){if(r==="area")return t;switch(n){case"cross":return 5*t*t/9;case"diamond":return .5*t*t/Math.sqrt(3);case"square":return t*t;case"star":{var i=18*j4;return 1.25*t*t*(Math.tan(i)-Math.tan(i*2)*Math.pow(Math.tan(i),2))}case"triangle":return Math.sqrt(3)*t*t/4;case"wye":return(21-10*Math.sqrt(3))*t*t/8;default:return Math.PI*t*t/4}},$4=function(t,r){rE["symbol".concat(Bd(t))]=r},Ug=function(t){var r=t.type,n=r===void 0?"circle":r,i=t.size,a=i===void 0?64:i,o=t.sizeType,s=o===void 0?"area":o,l=A4(t,S4),u=Nx(Nx({},l),{},{type:n,size:a,sizeType:s}),f=function(){var y=T4(n),v=a4().type(y).size(C4(a,s,n));return v()},c=u.className,d=u.cx,h=u.cy,p=te(u,!0);return d===+d&&h===+h&&a===+a?T.createElement("path",ry({},p,{className:oe("recharts-symbols",c),transform:"translate(".concat(d,", ").concat(h,")"),d:f()})):null};Ug.registerSymbol=$4;function _o(e){"@babel/helpers - typeof";return _o=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},_o(e)}function ny(){return ny=Object.assign?Object.assign.bind():function(e){for(var t=1;t`);var g=h.inactive?u:h.color;return T.createElement("li",ny({className:y,style:c,key:"legend-item-".concat(p)},fa(n.props,h,p)),T.createElement(Gm,{width:o,height:o,viewBox:f,style:d},n.renderIcon(h)),T.createElement("span",{className:"recharts-legend-item-text",style:{color:g}},m?m(v,h,p):v))})}},{key:"render",value:function(){var n=this.props,i=n.payload,a=n.layout,o=n.align;if(!i||!i.length)return null;var s={padding:0,margin:0,textAlign:a==="horizontal"?o:"left"};return T.createElement("ul",{className:"recharts-default-legend",style:s},this.renderItems())}}])}(j.PureComponent);Sl(zg,"displayName","Legend");Sl(zg,"defaultProps",{iconSize:14,layout:"horizontal",align:"center",verticalAlign:"middle",inactiveColor:"#ccc"});var U4=Ad;function z4(){this.__data__=new U4,this.size=0}var W4=z4;function H4(e){var t=this.__data__,r=t.delete(e);return this.size=t.size,r}var q4=H4;function K4(e){return this.__data__.get(e)}var V4=K4;function G4(e){return this.__data__.has(e)}var X4=G4,Q4=Ad,Y4=Tg,J4=Cg,Z4=200;function e3(e,t){var r=this.__data__;if(r instanceof Q4){var n=r.__data__;if(!Y4||n.lengths))return!1;var u=a.get(e),f=a.get(t);if(u&&f)return u==t&&f==e;var c=-1,d=!0,h=r&S3?new g3:void 0;for(a.set(e,t),a.set(t,e);++c-1&&e%1==0&&e-1&&e%1==0&&e<=AB}var Kg=EB,jB=An,TB=Kg,CB=En,$B="[object Arguments]",kB="[object Array]",NB="[object Boolean]",MB="[object Date]",IB="[object Error]",RB="[object Function]",DB="[object Map]",LB="[object Number]",BB="[object Object]",FB="[object RegExp]",UB="[object Set]",zB="[object String]",WB="[object WeakMap]",HB="[object ArrayBuffer]",qB="[object DataView]",KB="[object Float32Array]",VB="[object Float64Array]",GB="[object Int8Array]",XB="[object Int16Array]",QB="[object Int32Array]",YB="[object Uint8Array]",JB="[object Uint8ClampedArray]",ZB="[object Uint16Array]",eF="[object Uint32Array]",$e={};$e[KB]=$e[VB]=$e[GB]=$e[XB]=$e[QB]=$e[YB]=$e[JB]=$e[ZB]=$e[eF]=!0;$e[$B]=$e[kB]=$e[HB]=$e[NB]=$e[qB]=$e[MB]=$e[IB]=$e[RB]=$e[DB]=$e[LB]=$e[BB]=$e[FB]=$e[UB]=$e[zB]=$e[WB]=!1;function tF(e){return CB(e)&&TB(e.length)&&!!$e[jB(e)]}var rF=tF;function nF(e){return function(t){return e(t)}}var hE=nF,df={exports:{}};df.exports;(function(e,t){var r=wA,n=t&&!t.nodeType&&t,i=n&&!0&&e&&!e.nodeType&&e,a=i&&i.exports===n,o=a&&r.process,s=function(){try{var l=i&&i.require&&i.require("util").types;return l||o&&o.binding&&o.binding("util")}catch{}}();e.exports=s})(df,df.exports);var iF=df.exports,aF=rF,oF=hE,Fx=iF,Ux=Fx&&Fx.isTypedArray,sF=Ux?oF(Ux):aF,pE=sF,lF=fB,uF=Hg,cF=qt,fF=dE,dF=qg,hF=pE,pF=Object.prototype,mF=pF.hasOwnProperty;function yF(e,t){var r=cF(e),n=!r&&uF(e),i=!r&&!n&&fF(e),a=!r&&!n&&!i&&hF(e),o=r||n||i||a,s=o?lF(e.length,String):[],l=s.length;for(var u in e)(t||mF.call(e,u))&&!(o&&(u=="length"||i&&(u=="offset"||u=="parent")||a&&(u=="buffer"||u=="byteLength"||u=="byteOffset")||dF(u,l)))&&s.push(u);return s}var vF=yF,gF=Object.prototype;function bF(e){var t=e&&e.constructor,r=typeof t=="function"&&t.prototype||gF;return e===r}var xF=bF;function wF(e,t){return function(r){return e(t(r))}}var mE=wF,SF=mE,OF=SF(Object.keys,Object),_F=OF,PF=xF,AF=_F,EF=Object.prototype,jF=EF.hasOwnProperty;function TF(e){if(!PF(e))return AF(e);var t=[];for(var r in Object(e))jF.call(e,r)&&r!="constructor"&&t.push(r);return t}var CF=TF,$F=Eg,kF=Kg;function NF(e){return e!=null&&kF(e.length)&&!$F(e)}var Wd=NF,MF=vF,IF=CF,RF=Wd;function DF(e){return RF(e)?MF(e):IF(e)}var Vg=DF,LF=Z3,BF=uB,FF=Vg;function UF(e){return LF(e,FF,BF)}var zF=UF,zx=zF,WF=1,HF=Object.prototype,qF=HF.hasOwnProperty;function KF(e,t,r,n,i,a){var o=r&WF,s=zx(e),l=s.length,u=zx(t),f=u.length;if(l!=f&&!o)return!1;for(var c=l;c--;){var d=s[c];if(!(o?d in t:qF.call(t,d)))return!1}var h=a.get(e),p=a.get(t);if(h&&p)return h==t&&p==e;var m=!0;a.set(e,t),a.set(t,e);for(var y=o;++c-1}var q6=H6;function K6(e,t,r){for(var n=-1,i=e==null?0:e.length;++n=sU){var u=t?null:aU(e);if(u)return oU(u);o=!1,i=iU,l=new tU}else l=t?[]:s;e:for(;++n=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function OU(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}function _U(e){return e.value}function PU(e,t){if(T.isValidElement(e))return T.cloneElement(e,t);if(typeof e=="function")return T.createElement(e,t);t.ref;var r=SU(t,pU);return T.createElement(zg,r)}var i1=1,Ja=function(e){function t(){var r;mU(this,t);for(var n=arguments.length,i=new Array(n),a=0;ai1||Math.abs(i.height-this.lastBoundingBox.height)>i1)&&(this.lastBoundingBox.width=i.width,this.lastBoundingBox.height=i.height,n&&n(i)):(this.lastBoundingBox.width!==-1||this.lastBoundingBox.height!==-1)&&(this.lastBoundingBox.width=-1,this.lastBoundingBox.height=-1,n&&n(null))}},{key:"getBBoxSnapshot",value:function(){return this.lastBoundingBox.width>=0&&this.lastBoundingBox.height>=0?Qr({},this.lastBoundingBox):{width:0,height:0}}},{key:"getDefaultPosition",value:function(n){var i=this.props,a=i.layout,o=i.align,s=i.verticalAlign,l=i.margin,u=i.chartWidth,f=i.chartHeight,c,d;if(!n||(n.left===void 0||n.left===null)&&(n.right===void 0||n.right===null))if(o==="center"&&a==="vertical"){var h=this.getBBoxSnapshot();c={left:((u||0)-h.width)/2}}else c=o==="right"?{right:l&&l.right||0}:{left:l&&l.left||0};if(!n||(n.top===void 0||n.top===null)&&(n.bottom===void 0||n.bottom===null))if(s==="middle"){var p=this.getBBoxSnapshot();d={top:((f||0)-p.height)/2}}else d=s==="bottom"?{bottom:l&&l.bottom||0}:{top:l&&l.top||0};return Qr(Qr({},c),d)}},{key:"render",value:function(){var n=this,i=this.props,a=i.content,o=i.width,s=i.height,l=i.wrapperStyle,u=i.payloadUniqBy,f=i.payload,c=Qr(Qr({position:"absolute",width:o||"auto",height:s||"auto"},this.getDefaultPosition(l)),l);return T.createElement("div",{className:"recharts-legend-wrapper",style:c,ref:function(h){n.wrapperNode=h}},PU(a,Qr(Qr({},this.props),{},{payload:wE(f,u,_U)})))}}],[{key:"getWithHeight",value:function(n,i){var a=Qr(Qr({},this.defaultProps),n.props),o=a.layout;return o==="vertical"&&V(n.props.height)?{height:n.props.height}:o==="horizontal"?{width:n.props.width||i}:null}}])}(j.PureComponent);Hd(Ja,"displayName","Legend");Hd(Ja,"defaultProps",{iconSize:14,layout:"horizontal",align:"center",verticalAlign:"bottom"});var a1=pu,AU=Hg,EU=qt,o1=a1?a1.isConcatSpreadable:void 0;function jU(e){return EU(e)||AU(e)||!!(o1&&e&&e[o1])}var TU=jU,CU=cE,$U=TU;function _E(e,t,r,n,i){var a=-1,o=e.length;for(r||(r=$U),i||(i=[]);++a0&&r(s)?t>1?_E(s,t-1,r,n,i):CU(i,s):n||(i[i.length]=s)}return i}var PE=_E;function kU(e){return function(t,r,n){for(var i=-1,a=Object(t),o=n(t),s=o.length;s--;){var l=o[e?s:++i];if(r(a[l],l,a)===!1)break}return t}}var NU=kU,MU=NU,IU=MU(),RU=IU,DU=RU,LU=Vg;function BU(e,t){return e&&DU(e,t,LU)}var AE=BU,FU=Wd;function UU(e,t){return function(r,n){if(r==null)return r;if(!FU(r))return e(r,n);for(var i=r.length,a=t?i:-1,o=Object(r);(t?a--:++at||a&&o&&l&&!s&&!u||n&&o&&l||!r&&l||!i)return 1;if(!n&&!a&&!u&&e=s)return l;var u=r[n];return l*(u=="desc"?-1:1)}}return e.index-t.index}var t8=e8,up=kg,r8=Ng,n8=vi,i8=EE,a8=QU,o8=hE,s8=t8,l8=os,u8=qt;function c8(e,t,r){t.length?t=up(t,function(a){return u8(a)?function(o){return r8(o,a.length===1?a[0]:a)}:a}):t=[l8];var n=-1;t=up(t,o8(n8));var i=i8(e,function(a,o,s){var l=up(t,function(u){return u(a)});return{criteria:l,index:++n,value:a}});return a8(i,function(a,o){return s8(a,o,r)})}var f8=c8;function d8(e,t,r){switch(r.length){case 0:return e.call(t);case 1:return e.call(t,r[0]);case 2:return e.call(t,r[0],r[1]);case 3:return e.call(t,r[0],r[1],r[2])}return e.apply(t,r)}var h8=d8,p8=h8,l1=Math.max;function m8(e,t,r){return t=l1(t===void 0?e.length-1:t,0),function(){for(var n=arguments,i=-1,a=l1(n.length-t,0),o=Array(a);++i0){if(++t>=P8)return arguments[0]}else t=0;return e.apply(void 0,arguments)}}var T8=j8,C8=_8,$8=T8,k8=$8(C8),N8=k8,M8=os,I8=y8,R8=N8;function D8(e,t){return R8(I8(e,t,M8),e+"")}var L8=D8,B8=jg,F8=Wd,U8=qg,z8=yi;function W8(e,t,r){if(!z8(r))return!1;var n=typeof t;return(n=="number"?F8(r)&&U8(t,r.length):n=="string"&&t in r)?B8(r[t],e):!1}var qd=W8,H8=PE,q8=f8,K8=L8,c1=qd,V8=K8(function(e,t){if(e==null)return[];var r=t.length;return r>1&&c1(e,t[0],t[1])?t=[]:r>2&&c1(t[0],t[1],t[2])&&(t=[t[0]]),q8(e,H8(t,1),[])}),G8=V8;const Qg=Se(G8);function Ol(e){"@babel/helpers - typeof";return Ol=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Ol(e)}function fy(){return fy=Object.assign?Object.assign.bind():function(e){for(var t=1;te.length)&&(t=e.length);for(var r=0,n=new Array(t);r=t.x),"".concat(Ss,"-left"),V(r)&&t&&V(t.x)&&r=t.y),"".concat(Ss,"-top"),V(n)&&t&&V(t.y)&&nm?Math.max(f,l[n]):Math.max(c,l[n])}function uz(e){var t=e.translateX,r=e.translateY,n=e.useTranslate3d;return{transform:n?"translate3d(".concat(t,"px, ").concat(r,"px, 0)"):"translate(".concat(t,"px, ").concat(r,"px)")}}function cz(e){var t=e.allowEscapeViewBox,r=e.coordinate,n=e.offsetTopLeft,i=e.position,a=e.reverseDirection,o=e.tooltipBox,s=e.useTranslate3d,l=e.viewBox,u,f,c;return o.height>0&&o.width>0&&r?(f=h1({allowEscapeViewBox:t,coordinate:r,key:"x",offsetTopLeft:n,position:i,reverseDirection:a,tooltipDimension:o.width,viewBox:l,viewBoxDimension:l.width}),c=h1({allowEscapeViewBox:t,coordinate:r,key:"y",offsetTopLeft:n,position:i,reverseDirection:a,tooltipDimension:o.height,viewBox:l,viewBoxDimension:l.height}),u=uz({translateX:f,translateY:c,useTranslate3d:s})):u=sz,{cssProperties:u,cssClasses:lz({translateX:f,translateY:c,coordinate:r})}}function Ao(e){"@babel/helpers - typeof";return Ao=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Ao(e)}function p1(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function m1(e){for(var t=1;ty1||Math.abs(n.height-this.state.lastBoundingBox.height)>y1)&&this.setState({lastBoundingBox:{width:n.width,height:n.height}})}else(this.state.lastBoundingBox.width!==-1||this.state.lastBoundingBox.height!==-1)&&this.setState({lastBoundingBox:{width:-1,height:-1}})}},{key:"componentDidMount",value:function(){document.addEventListener("keydown",this.handleKeyDown),this.updateBBox()}},{key:"componentWillUnmount",value:function(){document.removeEventListener("keydown",this.handleKeyDown)}},{key:"componentDidUpdate",value:function(){var n,i;this.props.active&&this.updateBBox(),this.state.dismissed&&(((n=this.props.coordinate)===null||n===void 0?void 0:n.x)!==this.state.dismissedAtCoordinate.x||((i=this.props.coordinate)===null||i===void 0?void 0:i.y)!==this.state.dismissedAtCoordinate.y)&&(this.state.dismissed=!1)}},{key:"render",value:function(){var n=this,i=this.props,a=i.active,o=i.allowEscapeViewBox,s=i.animationDuration,l=i.animationEasing,u=i.children,f=i.coordinate,c=i.hasPayload,d=i.isAnimationActive,h=i.offset,p=i.position,m=i.reverseDirection,y=i.useTranslate3d,v=i.viewBox,g=i.wrapperStyle,b=cz({allowEscapeViewBox:o,coordinate:f,offsetTopLeft:h,position:p,reverseDirection:m,tooltipBox:this.state.lastBoundingBox,useTranslate3d:y,viewBox:v}),w=b.cssClasses,x=b.cssProperties,S=m1(m1({transition:d&&a?"transform ".concat(s,"ms ").concat(l):void 0},x),{},{pointerEvents:"none",visibility:!this.state.dismissed&&a&&c?"visible":"hidden",position:"absolute",top:0,left:0},g);return T.createElement("div",{tabIndex:-1,className:w,style:S,ref:function(P){n.wrapperNode=P}},u)}}])}(j.PureComponent),xz=function(){return!(typeof window<"u"&&window.document&&window.document.createElement&&window.setTimeout)},ss={isSsr:xz()};function Eo(e){"@babel/helpers - typeof";return Eo=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Eo(e)}function v1(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function g1(e){for(var t=1;t0;return T.createElement(bz,{allowEscapeViewBox:o,animationDuration:s,animationEasing:l,isAnimationActive:d,active:a,coordinate:f,hasPayload:S,offset:h,position:y,reverseDirection:v,useTranslate3d:g,viewBox:b,wrapperStyle:w},Cz(u,g1(g1({},this.props),{},{payload:x})))}}])}(j.PureComponent);Yg(Ar,"displayName","Tooltip");Yg(Ar,"defaultProps",{accessibilityLayer:!1,allowEscapeViewBox:{x:!1,y:!1},animationDuration:400,animationEasing:"ease",contentStyle:{},coordinate:{x:0,y:0},cursor:!0,cursorStyle:{},filterNull:!0,isAnimationActive:!ss.isSsr,itemStyle:{},labelStyle:{},offset:10,reverseDirection:{x:!1,y:!1},separator:" : ",trigger:"hover",useTranslate3d:!1,viewBox:{x:0,y:0,height:0,width:0},wrapperStyle:{}});var $z=Xr,kz=function(){return $z.Date.now()},Nz=kz,Mz=/\s/;function Iz(e){for(var t=e.length;t--&&Mz.test(e.charAt(t)););return t}var Rz=Iz,Dz=Rz,Lz=/^\s+/;function Bz(e){return e&&e.slice(0,Dz(e)+1).replace(Lz,"")}var Fz=Bz,Uz=Fz,b1=yi,zz=Zo,x1=NaN,Wz=/^[-+]0x[0-9a-f]+$/i,Hz=/^0b[01]+$/i,qz=/^0o[0-7]+$/i,Kz=parseInt;function Vz(e){if(typeof e=="number")return e;if(zz(e))return x1;if(b1(e)){var t=typeof e.valueOf=="function"?e.valueOf():e;e=b1(t)?t+"":t}if(typeof e!="string")return e===0?e:+e;e=Uz(e);var r=Hz.test(e);return r||qz.test(e)?Kz(e.slice(2),r?2:8):Wz.test(e)?x1:+e}var NE=Vz,Gz=yi,fp=Nz,w1=NE,Xz="Expected a function",Qz=Math.max,Yz=Math.min;function Jz(e,t,r){var n,i,a,o,s,l,u=0,f=!1,c=!1,d=!0;if(typeof e!="function")throw new TypeError(Xz);t=w1(t)||0,Gz(r)&&(f=!!r.leading,c="maxWait"in r,a=c?Qz(w1(r.maxWait)||0,t):a,d="trailing"in r?!!r.trailing:d);function h(S){var _=n,P=i;return n=i=void 0,u=S,o=e.apply(P,_),o}function p(S){return u=S,s=setTimeout(v,t),f?h(S):o}function m(S){var _=S-l,P=S-u,A=t-_;return c?Yz(A,a-P):A}function y(S){var _=S-l,P=S-u;return l===void 0||_>=t||_<0||c&&P>=a}function v(){var S=fp();if(y(S))return g(S);s=setTimeout(v,m(S))}function g(S){return s=void 0,d&&n?h(S):(n=i=void 0,o)}function b(){s!==void 0&&clearTimeout(s),u=0,n=l=i=s=void 0}function w(){return s===void 0?o:g(fp())}function x(){var S=fp(),_=y(S);if(n=arguments,i=this,l=S,_){if(s===void 0)return p(l);if(c)return clearTimeout(s),s=setTimeout(v,t),h(l)}return s===void 0&&(s=setTimeout(v,t)),o}return x.cancel=b,x.flush=w,x}var Zz=Jz,eW=Zz,tW=yi,rW="Expected a function";function nW(e,t,r){var n=!0,i=!0;if(typeof e!="function")throw new TypeError(rW);return tW(r)&&(n="leading"in r?!!r.leading:n,i="trailing"in r?!!r.trailing:i),eW(e,t,{leading:n,maxWait:t,trailing:i})}var iW=nW;const ME=Se(iW);function Pl(e){"@babel/helpers - typeof";return Pl=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Pl(e)}function S1(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function Xu(e){for(var t=1;te.length)&&(t=e.length);for(var r=0,n=new Array(t);r0&&(L=ME(L,m,{trailing:!0,leading:!1}));var I=new ResizeObserver(L),R=x.current.getBoundingClientRect(),B=R.width,z=R.height;return N(B,z),I.observe(x.current),function(){I.disconnect()}},[N,m]);var $=j.useMemo(function(){var L=A.containerWidth,I=A.containerHeight;if(L<0||I<0)return null;dn(Li(o)||Li(l),`The width(%s) and height(%s) are both fixed numbers, + maybe you don't need to use a ResponsiveContainer.`,o,l),dn(!r||r>0,"The aspect(%s) must be greater than zero.",r);var R=Li(o)?L:o,B=Li(l)?I:l;r&&r>0&&(R?B=R/r:B&&(R=B*r),d&&B>d&&(B=d)),dn(R>0||B>0,`The width(%s) and height(%s) of chart should be greater than 0, + please check the style of container, or the props width(%s) and height(%s), + or add a minWidth(%s) or minHeight(%s) or use aspect(%s) to control the + height and width.`,R,B,o,l,f,c,r);var z=!Array.isArray(h)&&fn(h.type).endsWith("Chart");return T.Children.map(h,function(k){return T.isValidElement(k)?j.cloneElement(k,Xu({width:R,height:B},z?{style:Xu({height:"100%",width:"100%",maxHeight:B,maxWidth:R},k.props.style)}:{})):k})},[r,h,l,d,c,f,A,o]);return T.createElement("div",{id:y?"".concat(y):void 0,className:oe("recharts-responsive-container",v),style:Xu(Xu({},w),{},{width:o,height:l,minWidth:f,minHeight:c,maxHeight:d}),ref:x},$)}),Kd=function(t){return null};Kd.displayName="Cell";function Al(e){"@babel/helpers - typeof";return Al=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Al(e)}function P1(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function my(e){for(var t=1;t1&&arguments[1]!==void 0?arguments[1]:{};if(t==null||ss.isSsr)return{width:0,height:0};var n=gW(r),i=JSON.stringify({text:t,copyStyle:n});if(Pa.widthCache[i])return Pa.widthCache[i];try{var a=document.getElementById(A1);a||(a=document.createElement("span"),a.setAttribute("id",A1),a.setAttribute("aria-hidden","true"),document.body.appendChild(a));var o=my(my({},vW),n);Object.assign(a.style,o),a.textContent="".concat(t);var s=a.getBoundingClientRect(),l={width:s.width,height:s.height};return Pa.widthCache[i]=l,++Pa.cacheCount>yW&&(Pa.cacheCount=0,Pa.widthCache={}),l}catch{return{width:0,height:0}}},bW=function(t){return{top:t.top+window.scrollY-document.documentElement.clientTop,left:t.left+window.scrollX-document.documentElement.clientLeft}};function El(e){"@babel/helpers - typeof";return El=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},El(e)}function yf(e,t){return OW(e)||SW(e,t)||wW(e,t)||xW()}function xW(){throw new TypeError(`Invalid attempt to destructure non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function wW(e,t){if(e){if(typeof e=="string")return E1(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);if(r==="Object"&&e.constructor&&(r=e.constructor.name),r==="Map"||r==="Set")return Array.from(e);if(r==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return E1(e,t)}}function E1(e,t){(t==null||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function DW(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}function N1(e,t){return UW(e)||FW(e,t)||BW(e,t)||LW()}function LW(){throw new TypeError(`Invalid attempt to destructure non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function BW(e,t){if(e){if(typeof e=="string")return M1(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);if(r==="Object"&&e.constructor&&(r=e.constructor.name),r==="Map"||r==="Set")return Array.from(e);if(r==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return M1(e,t)}}function M1(e,t){(t==null||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r0&&arguments[0]!==void 0?arguments[0]:[];return R.reduce(function(B,z){var k=z.word,F=z.width,U=B[B.length-1];if(U&&(i==null||a||U.width+F+nz.width?B:z})};if(!f)return h;for(var m="…",y=function(R){var B=c.slice(0,R),z=LE({breakAll:u,style:l,children:B+m}).wordsWithComputedWidth,k=d(z),F=k.length>o||p(k).width>Number(i);return[F,k]},v=0,g=c.length-1,b=0,w;v<=g&&b<=c.length-1;){var x=Math.floor((v+g)/2),S=x-1,_=y(S),P=N1(_,2),A=P[0],C=P[1],N=y(x),$=N1(N,1),L=$[0];if(!A&&!L&&(v=x+1),A&&L&&(g=x-1),!A&&L){w=C;break}b++}return w||h},I1=function(t){var r=ue(t)?[]:t.toString().split(DE);return[{words:r}]},WW=function(t){var r=t.width,n=t.scaleToFit,i=t.children,a=t.style,o=t.breakAll,s=t.maxLines;if((r||n)&&!ss.isSsr){var l,u,f=LE({breakAll:o,children:i,style:a});if(f){var c=f.wordsWithComputedWidth,d=f.spaceWidth;l=c,u=d}else return I1(i);return zW({breakAll:o,children:i,maxLines:s,style:a},l,u,r,n)}return I1(i)},R1="#808080",da=function(t){var r=t.x,n=r===void 0?0:r,i=t.y,a=i===void 0?0:i,o=t.lineHeight,s=o===void 0?"1em":o,l=t.capHeight,u=l===void 0?"0.71em":l,f=t.scaleToFit,c=f===void 0?!1:f,d=t.textAnchor,h=d===void 0?"start":d,p=t.verticalAnchor,m=p===void 0?"end":p,y=t.fill,v=y===void 0?R1:y,g=k1(t,IW),b=j.useMemo(function(){return WW({breakAll:g.breakAll,children:g.children,maxLines:g.maxLines,scaleToFit:c,style:g.style,width:g.width})},[g.breakAll,g.children,g.maxLines,c,g.style,g.width]),w=g.dx,x=g.dy,S=g.angle,_=g.className,P=g.breakAll,A=k1(g,RW);if(!nt(n)||!nt(a))return null;var C=n+(V(w)?w:0),N=a+(V(x)?x:0),$;switch(m){case"start":$=dp("calc(".concat(u,")"));break;case"middle":$=dp("calc(".concat((b.length-1)/2," * -").concat(s," + (").concat(u," / 2))"));break;default:$=dp("calc(".concat(b.length-1," * -").concat(s,")"));break}var L=[];if(c){var I=b[0].width,R=g.width;L.push("scale(".concat((V(R)?R/I:1)/I,")"))}return S&&L.push("rotate(".concat(S,", ").concat(C,", ").concat(N,")")),L.length&&(A.transform=L.join(" ")),T.createElement("text",yy({},te(A,!0),{x:C,y:N,className:oe("recharts-text",_),textAnchor:h,fill:v.includes("url")?R1:v}),b.map(function(B,z){var k=B.words.join(P?"":" ");return T.createElement("tspan",{x:C,dy:z===0?$:s,key:"".concat(k,"-").concat(z)},k)}))};function li(e,t){return e==null||t==null?NaN:et?1:e>=t?0:NaN}function HW(e,t){return e==null||t==null?NaN:te?1:t>=e?0:NaN}function Jg(e){let t,r,n;e.length!==2?(t=li,r=(s,l)=>li(e(s),l),n=(s,l)=>e(s)-l):(t=e===li||e===HW?e:qW,r=e,n=e);function i(s,l,u=0,f=s.length){if(u>>1;r(s[c],l)<0?u=c+1:f=c}while(u>>1;r(s[c],l)<=0?u=c+1:f=c}while(uu&&n(s[c-1],l)>-n(s[c],l)?c-1:c}return{left:i,center:o,right:a}}function qW(){return 0}function BE(e){return e===null?NaN:+e}function*KW(e,t){for(let r of e)r!=null&&(r=+r)>=r&&(yield r)}const VW=Jg(li),vu=VW.right;Jg(BE).center;class D1 extends Map{constructor(t,r=QW){if(super(),Object.defineProperties(this,{_intern:{value:new Map},_key:{value:r}}),t!=null)for(const[n,i]of t)this.set(n,i)}get(t){return super.get(L1(this,t))}has(t){return super.has(L1(this,t))}set(t,r){return super.set(GW(this,t),r)}delete(t){return super.delete(XW(this,t))}}function L1({_intern:e,_key:t},r){const n=t(r);return e.has(n)?e.get(n):r}function GW({_intern:e,_key:t},r){const n=t(r);return e.has(n)?e.get(n):(e.set(n,r),r)}function XW({_intern:e,_key:t},r){const n=t(r);return e.has(n)&&(r=e.get(n),e.delete(n)),r}function QW(e){return e!==null&&typeof e=="object"?e.valueOf():e}function YW(e=li){if(e===li)return FE;if(typeof e!="function")throw new TypeError("compare is not a function");return(t,r)=>{const n=e(t,r);return n||n===0?n:(e(r,r)===0)-(e(t,t)===0)}}function FE(e,t){return(e==null||!(e>=e))-(t==null||!(t>=t))||(et?1:0)}const JW=Math.sqrt(50),ZW=Math.sqrt(10),e7=Math.sqrt(2);function vf(e,t,r){const n=(t-e)/Math.max(0,r),i=Math.floor(Math.log10(n)),a=n/Math.pow(10,i),o=a>=JW?10:a>=ZW?5:a>=e7?2:1;let s,l,u;return i<0?(u=Math.pow(10,-i)/o,s=Math.round(e*u),l=Math.round(t*u),s/ut&&--l,u=-u):(u=Math.pow(10,i)*o,s=Math.round(e/u),l=Math.round(t/u),s*ut&&--l),l0))return[];if(e===t)return[e];const n=t=i))return[];const s=a-i+1,l=new Array(s);if(n)if(o<0)for(let u=0;u=n)&&(r=n);return r}function F1(e,t){let r;for(const n of e)n!=null&&(r>n||r===void 0&&n>=n)&&(r=n);return r}function UE(e,t,r=0,n=1/0,i){if(t=Math.floor(t),r=Math.floor(Math.max(0,r)),n=Math.floor(Math.min(e.length-1,n)),!(r<=t&&t<=n))return e;for(i=i===void 0?FE:YW(i);n>r;){if(n-r>600){const l=n-r+1,u=t-r+1,f=Math.log(l),c=.5*Math.exp(2*f/3),d=.5*Math.sqrt(f*c*(l-c)/l)*(u-l/2<0?-1:1),h=Math.max(r,Math.floor(t-u*c/l+d)),p=Math.min(n,Math.floor(t+(l-u)*c/l+d));UE(e,t,h,p,i)}const a=e[t];let o=r,s=n;for(Os(e,r,t),i(e[n],a)>0&&Os(e,r,n);o0;)--s}i(e[r],a)===0?Os(e,r,s):(++s,Os(e,s,n)),s<=t&&(r=s+1),t<=s&&(n=s-1)}return e}function Os(e,t,r){const n=e[t];e[t]=e[r],e[r]=n}function t7(e,t,r){if(e=Float64Array.from(KW(e)),!(!(n=e.length)||isNaN(t=+t))){if(t<=0||n<2)return F1(e);if(t>=1)return B1(e);var n,i=(n-1)*t,a=Math.floor(i),o=B1(UE(e,a).subarray(0,a+1)),s=F1(e.subarray(a+1));return o+(s-o)*(i-a)}}function r7(e,t,r=BE){if(!(!(n=e.length)||isNaN(t=+t))){if(t<=0||n<2)return+r(e[0],0,e);if(t>=1)return+r(e[n-1],n-1,e);var n,i=(n-1)*t,a=Math.floor(i),o=+r(e[a],a,e),s=+r(e[a+1],a+1,e);return o+(s-o)*(i-a)}}function n7(e,t,r){e=+e,t=+t,r=(i=arguments.length)<2?(t=e,e=0,1):i<3?1:+r;for(var n=-1,i=Math.max(0,Math.ceil((t-e)/r))|0,a=new Array(i);++n>8&15|t>>4&240,t>>4&15|t&240,(t&15)<<4|t&15,1):r===8?Yu(t>>24&255,t>>16&255,t>>8&255,(t&255)/255):r===4?Yu(t>>12&15|t>>8&240,t>>8&15|t>>4&240,t>>4&15|t&240,((t&15)<<4|t&15)/255):null):(t=a7.exec(e))?new Lt(t[1],t[2],t[3],1):(t=o7.exec(e))?new Lt(t[1]*255/100,t[2]*255/100,t[3]*255/100,1):(t=s7.exec(e))?Yu(t[1],t[2],t[3],t[4]):(t=l7.exec(e))?Yu(t[1]*255/100,t[2]*255/100,t[3]*255/100,t[4]):(t=u7.exec(e))?V1(t[1],t[2]/100,t[3]/100,1):(t=c7.exec(e))?V1(t[1],t[2]/100,t[3]/100,t[4]):U1.hasOwnProperty(e)?H1(U1[e]):e==="transparent"?new Lt(NaN,NaN,NaN,0):null}function H1(e){return new Lt(e>>16&255,e>>8&255,e&255,1)}function Yu(e,t,r,n){return n<=0&&(e=t=r=NaN),new Lt(e,t,r,n)}function h7(e){return e instanceof gu||(e=$l(e)),e?(e=e.rgb(),new Lt(e.r,e.g,e.b,e.opacity)):new Lt}function wy(e,t,r,n){return arguments.length===1?h7(e):new Lt(e,t,r,n??1)}function Lt(e,t,r,n){this.r=+e,this.g=+t,this.b=+r,this.opacity=+n}e0(Lt,wy,WE(gu,{brighter(e){return e=e==null?gf:Math.pow(gf,e),new Lt(this.r*e,this.g*e,this.b*e,this.opacity)},darker(e){return e=e==null?Tl:Math.pow(Tl,e),new Lt(this.r*e,this.g*e,this.b*e,this.opacity)},rgb(){return this},clamp(){return new Lt(ea(this.r),ea(this.g),ea(this.b),bf(this.opacity))},displayable(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:q1,formatHex:q1,formatHex8:p7,formatRgb:K1,toString:K1}));function q1(){return`#${Bi(this.r)}${Bi(this.g)}${Bi(this.b)}`}function p7(){return`#${Bi(this.r)}${Bi(this.g)}${Bi(this.b)}${Bi((isNaN(this.opacity)?1:this.opacity)*255)}`}function K1(){const e=bf(this.opacity);return`${e===1?"rgb(":"rgba("}${ea(this.r)}, ${ea(this.g)}, ${ea(this.b)}${e===1?")":`, ${e})`}`}function bf(e){return isNaN(e)?1:Math.max(0,Math.min(1,e))}function ea(e){return Math.max(0,Math.min(255,Math.round(e)||0))}function Bi(e){return e=ea(e),(e<16?"0":"")+e.toString(16)}function V1(e,t,r,n){return n<=0?e=t=r=NaN:r<=0||r>=1?e=t=NaN:t<=0&&(e=NaN),new Tr(e,t,r,n)}function HE(e){if(e instanceof Tr)return new Tr(e.h,e.s,e.l,e.opacity);if(e instanceof gu||(e=$l(e)),!e)return new Tr;if(e instanceof Tr)return e;e=e.rgb();var t=e.r/255,r=e.g/255,n=e.b/255,i=Math.min(t,r,n),a=Math.max(t,r,n),o=NaN,s=a-i,l=(a+i)/2;return s?(t===a?o=(r-n)/s+(r0&&l<1?0:o,new Tr(o,s,l,e.opacity)}function m7(e,t,r,n){return arguments.length===1?HE(e):new Tr(e,t,r,n??1)}function Tr(e,t,r,n){this.h=+e,this.s=+t,this.l=+r,this.opacity=+n}e0(Tr,m7,WE(gu,{brighter(e){return e=e==null?gf:Math.pow(gf,e),new Tr(this.h,this.s,this.l*e,this.opacity)},darker(e){return e=e==null?Tl:Math.pow(Tl,e),new Tr(this.h,this.s,this.l*e,this.opacity)},rgb(){var e=this.h%360+(this.h<0)*360,t=isNaN(e)||isNaN(this.s)?0:this.s,r=this.l,n=r+(r<.5?r:1-r)*t,i=2*r-n;return new Lt(hp(e>=240?e-240:e+120,i,n),hp(e,i,n),hp(e<120?e+240:e-120,i,n),this.opacity)},clamp(){return new Tr(G1(this.h),Ju(this.s),Ju(this.l),bf(this.opacity))},displayable(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl(){const e=bf(this.opacity);return`${e===1?"hsl(":"hsla("}${G1(this.h)}, ${Ju(this.s)*100}%, ${Ju(this.l)*100}%${e===1?")":`, ${e})`}`}}));function G1(e){return e=(e||0)%360,e<0?e+360:e}function Ju(e){return Math.max(0,Math.min(1,e||0))}function hp(e,t,r){return(e<60?t+(r-t)*e/60:e<180?r:e<240?t+(r-t)*(240-e)/60:t)*255}const t0=e=>()=>e;function y7(e,t){return function(r){return e+r*t}}function v7(e,t,r){return e=Math.pow(e,r),t=Math.pow(t,r)-e,r=1/r,function(n){return Math.pow(e+n*t,r)}}function g7(e){return(e=+e)==1?qE:function(t,r){return r-t?v7(t,r,e):t0(isNaN(t)?r:t)}}function qE(e,t){var r=t-e;return r?y7(e,r):t0(isNaN(e)?t:e)}const X1=function e(t){var r=g7(t);function n(i,a){var o=r((i=wy(i)).r,(a=wy(a)).r),s=r(i.g,a.g),l=r(i.b,a.b),u=qE(i.opacity,a.opacity);return function(f){return i.r=o(f),i.g=s(f),i.b=l(f),i.opacity=u(f),i+""}}return n.gamma=e,n}(1);function b7(e,t){t||(t=[]);var r=e?Math.min(t.length,e.length):0,n=t.slice(),i;return function(a){for(i=0;ir&&(a=t.slice(r,a),s[o]?s[o]+=a:s[++o]=a),(n=n[0])===(i=i[0])?s[o]?s[o]+=i:s[++o]=i:(s[++o]=null,l.push({i:o,x:xf(n,i)})),r=pp.lastIndex;return rt&&(r=e,e=t,t=r),function(n){return Math.max(e,Math.min(t,n))}}function C7(e,t,r){var n=e[0],i=e[1],a=t[0],o=t[1];return i2?$7:C7,l=u=null,c}function c(d){return d==null||isNaN(d=+d)?a:(l||(l=s(e.map(n),t,r)))(n(o(d)))}return c.invert=function(d){return o(i((u||(u=s(t,e.map(n),xf)))(d)))},c.domain=function(d){return arguments.length?(e=Array.from(d,wf),f()):e.slice()},c.range=function(d){return arguments.length?(t=Array.from(d),f()):t.slice()},c.rangeRound=function(d){return t=Array.from(d),r=r0,f()},c.clamp=function(d){return arguments.length?(o=d?!0:jt,f()):o!==jt},c.interpolate=function(d){return arguments.length?(r=d,f()):r},c.unknown=function(d){return arguments.length?(a=d,c):a},function(d,h){return n=d,i=h,f()}}function n0(){return Vd()(jt,jt)}function k7(e){return Math.abs(e=Math.round(e))>=1e21?e.toLocaleString("en").replace(/,/g,""):e.toString(10)}function Sf(e,t){if(!isFinite(e)||e===0)return null;var r=(e=t?e.toExponential(t-1):e.toExponential()).indexOf("e"),n=e.slice(0,r);return[n.length>1?n[0]+n.slice(2):n,+e.slice(r+1)]}function jo(e){return e=Sf(Math.abs(e)),e?e[1]:NaN}function N7(e,t){return function(r,n){for(var i=r.length,a=[],o=0,s=e[0],l=0;i>0&&s>0&&(l+s+1>n&&(s=Math.max(1,n-l)),a.push(r.substring(i-=s,i+s)),!((l+=s+1)>n));)s=e[o=(o+1)%e.length];return a.reverse().join(t)}}function M7(e){return function(t){return t.replace(/[0-9]/g,function(r){return e[+r]})}}var I7=/^(?:(.)?([<>=^]))?([+\-( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?(~)?([a-z%])?$/i;function kl(e){if(!(t=I7.exec(e)))throw new Error("invalid format: "+e);var t;return new i0({fill:t[1],align:t[2],sign:t[3],symbol:t[4],zero:t[5],width:t[6],comma:t[7],precision:t[8]&&t[8].slice(1),trim:t[9],type:t[10]})}kl.prototype=i0.prototype;function i0(e){this.fill=e.fill===void 0?" ":e.fill+"",this.align=e.align===void 0?">":e.align+"",this.sign=e.sign===void 0?"-":e.sign+"",this.symbol=e.symbol===void 0?"":e.symbol+"",this.zero=!!e.zero,this.width=e.width===void 0?void 0:+e.width,this.comma=!!e.comma,this.precision=e.precision===void 0?void 0:+e.precision,this.trim=!!e.trim,this.type=e.type===void 0?"":e.type+""}i0.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(this.width===void 0?"":Math.max(1,this.width|0))+(this.comma?",":"")+(this.precision===void 0?"":"."+Math.max(0,this.precision|0))+(this.trim?"~":"")+this.type};function R7(e){e:for(var t=e.length,r=1,n=-1,i;r0&&(n=0);break}return n>0?e.slice(0,n)+e.slice(i+1):e}var Of;function D7(e,t){var r=Sf(e,t);if(!r)return Of=void 0,e.toPrecision(t);var n=r[0],i=r[1],a=i-(Of=Math.max(-8,Math.min(8,Math.floor(i/3)))*3)+1,o=n.length;return a===o?n:a>o?n+new Array(a-o+1).join("0"):a>0?n.slice(0,a)+"."+n.slice(a):"0."+new Array(1-a).join("0")+Sf(e,Math.max(0,t+a-1))[0]}function Y1(e,t){var r=Sf(e,t);if(!r)return e+"";var n=r[0],i=r[1];return i<0?"0."+new Array(-i).join("0")+n:n.length>i+1?n.slice(0,i+1)+"."+n.slice(i+1):n+new Array(i-n.length+2).join("0")}const J1={"%":(e,t)=>(e*100).toFixed(t),b:e=>Math.round(e).toString(2),c:e=>e+"",d:k7,e:(e,t)=>e.toExponential(t),f:(e,t)=>e.toFixed(t),g:(e,t)=>e.toPrecision(t),o:e=>Math.round(e).toString(8),p:(e,t)=>Y1(e*100,t),r:Y1,s:D7,X:e=>Math.round(e).toString(16).toUpperCase(),x:e=>Math.round(e).toString(16)};function Z1(e){return e}var ew=Array.prototype.map,tw=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"];function L7(e){var t=e.grouping===void 0||e.thousands===void 0?Z1:N7(ew.call(e.grouping,Number),e.thousands+""),r=e.currency===void 0?"":e.currency[0]+"",n=e.currency===void 0?"":e.currency[1]+"",i=e.decimal===void 0?".":e.decimal+"",a=e.numerals===void 0?Z1:M7(ew.call(e.numerals,String)),o=e.percent===void 0?"%":e.percent+"",s=e.minus===void 0?"−":e.minus+"",l=e.nan===void 0?"NaN":e.nan+"";function u(c,d){c=kl(c);var h=c.fill,p=c.align,m=c.sign,y=c.symbol,v=c.zero,g=c.width,b=c.comma,w=c.precision,x=c.trim,S=c.type;S==="n"?(b=!0,S="g"):J1[S]||(w===void 0&&(w=12),x=!0,S="g"),(v||h==="0"&&p==="=")&&(v=!0,h="0",p="=");var _=(d&&d.prefix!==void 0?d.prefix:"")+(y==="$"?r:y==="#"&&/[boxX]/.test(S)?"0"+S.toLowerCase():""),P=(y==="$"?n:/[%p]/.test(S)?o:"")+(d&&d.suffix!==void 0?d.suffix:""),A=J1[S],C=/[defgprs%]/.test(S);w=w===void 0?6:/[gprs]/.test(S)?Math.max(1,Math.min(21,w)):Math.max(0,Math.min(20,w));function N($){var L=_,I=P,R,B,z;if(S==="c")I=A($)+I,$="";else{$=+$;var k=$<0||1/$<0;if($=isNaN($)?l:A(Math.abs($),w),x&&($=R7($)),k&&+$==0&&m!=="+"&&(k=!1),L=(k?m==="("?m:s:m==="-"||m==="("?"":m)+L,I=(S==="s"&&!isNaN($)&&Of!==void 0?tw[8+Of/3]:"")+I+(k&&m==="("?")":""),C){for(R=-1,B=$.length;++Rz||z>57){I=(z===46?i+$.slice(R+1):$.slice(R))+I,$=$.slice(0,R);break}}}b&&!v&&($=t($,1/0));var F=L.length+$.length+I.length,U=F>1)+L+$+I+U.slice(F);break;default:$=U+L+$+I;break}return a($)}return N.toString=function(){return c+""},N}function f(c,d){var h=Math.max(-8,Math.min(8,Math.floor(jo(d)/3)))*3,p=Math.pow(10,-h),m=u((c=kl(c),c.type="f",c),{suffix:tw[8+h/3]});return function(y){return m(p*y)}}return{format:u,formatPrefix:f}}var Zu,a0,KE;B7({thousands:",",grouping:[3],currency:["$",""]});function B7(e){return Zu=L7(e),a0=Zu.format,KE=Zu.formatPrefix,Zu}function F7(e){return Math.max(0,-jo(Math.abs(e)))}function U7(e,t){return Math.max(0,Math.max(-8,Math.min(8,Math.floor(jo(t)/3)))*3-jo(Math.abs(e)))}function z7(e,t){return e=Math.abs(e),t=Math.abs(t)-e,Math.max(0,jo(t)-jo(e))+1}function VE(e,t,r,n){var i=by(e,t,r),a;switch(n=kl(n??",f"),n.type){case"s":{var o=Math.max(Math.abs(e),Math.abs(t));return n.precision==null&&!isNaN(a=U7(i,o))&&(n.precision=a),KE(n,o)}case"":case"e":case"g":case"p":case"r":{n.precision==null&&!isNaN(a=z7(i,Math.max(Math.abs(e),Math.abs(t))))&&(n.precision=a-(n.type==="e"));break}case"f":case"%":{n.precision==null&&!isNaN(a=F7(i))&&(n.precision=a-(n.type==="%")*2);break}}return a0(n)}function gi(e){var t=e.domain;return e.ticks=function(r){var n=t();return vy(n[0],n[n.length-1],r??10)},e.tickFormat=function(r,n){var i=t();return VE(i[0],i[i.length-1],r??10,n)},e.nice=function(r){r==null&&(r=10);var n=t(),i=0,a=n.length-1,o=n[i],s=n[a],l,u,f=10;for(s0;){if(u=gy(o,s,r),u===l)return n[i]=o,n[a]=s,t(n);if(u>0)o=Math.floor(o/u)*u,s=Math.ceil(s/u)*u;else if(u<0)o=Math.ceil(o*u)/u,s=Math.floor(s*u)/u;else break;l=u}return e},e}function _f(){var e=n0();return e.copy=function(){return bu(e,_f())},br.apply(e,arguments),gi(e)}function GE(e){var t;function r(n){return n==null||isNaN(n=+n)?t:n}return r.invert=r,r.domain=r.range=function(n){return arguments.length?(e=Array.from(n,wf),r):e.slice()},r.unknown=function(n){return arguments.length?(t=n,r):t},r.copy=function(){return GE(e).unknown(t)},e=arguments.length?Array.from(e,wf):[0,1],gi(r)}function XE(e,t){e=e.slice();var r=0,n=e.length-1,i=e[r],a=e[n],o;return aMath.pow(e,t)}function V7(e){return e===Math.E?Math.log:e===10&&Math.log10||e===2&&Math.log2||(e=Math.log(e),t=>Math.log(t)/e)}function iw(e){return(t,r)=>-e(-t,r)}function o0(e){const t=e(rw,nw),r=t.domain;let n=10,i,a;function o(){return i=V7(n),a=K7(n),r()[0]<0?(i=iw(i),a=iw(a),e(W7,H7)):e(rw,nw),t}return t.base=function(s){return arguments.length?(n=+s,o()):n},t.domain=function(s){return arguments.length?(r(s),o()):r()},t.ticks=s=>{const l=r();let u=l[0],f=l[l.length-1];const c=f0){for(;d<=h;++d)for(p=1;pf)break;v.push(m)}}else for(;d<=h;++d)for(p=n-1;p>=1;--p)if(m=d>0?p/a(-d):p*a(d),!(mf)break;v.push(m)}v.length*2{if(s==null&&(s=10),l==null&&(l=n===10?"s":","),typeof l!="function"&&(!(n%1)&&(l=kl(l)).precision==null&&(l.trim=!0),l=a0(l)),s===1/0)return l;const u=Math.max(1,n*s/t.ticks().length);return f=>{let c=f/a(Math.round(i(f)));return c*nr(XE(r(),{floor:s=>a(Math.floor(i(s))),ceil:s=>a(Math.ceil(i(s)))})),t}function QE(){const e=o0(Vd()).domain([1,10]);return e.copy=()=>bu(e,QE()).base(e.base()),br.apply(e,arguments),e}function aw(e){return function(t){return Math.sign(t)*Math.log1p(Math.abs(t/e))}}function ow(e){return function(t){return Math.sign(t)*Math.expm1(Math.abs(t))*e}}function s0(e){var t=1,r=e(aw(t),ow(t));return r.constant=function(n){return arguments.length?e(aw(t=+n),ow(t)):t},gi(r)}function YE(){var e=s0(Vd());return e.copy=function(){return bu(e,YE()).constant(e.constant())},br.apply(e,arguments)}function sw(e){return function(t){return t<0?-Math.pow(-t,e):Math.pow(t,e)}}function G7(e){return e<0?-Math.sqrt(-e):Math.sqrt(e)}function X7(e){return e<0?-e*e:e*e}function l0(e){var t=e(jt,jt),r=1;function n(){return r===1?e(jt,jt):r===.5?e(G7,X7):e(sw(r),sw(1/r))}return t.exponent=function(i){return arguments.length?(r=+i,n()):r},gi(t)}function u0(){var e=l0(Vd());return e.copy=function(){return bu(e,u0()).exponent(e.exponent())},br.apply(e,arguments),e}function Q7(){return u0.apply(null,arguments).exponent(.5)}function lw(e){return Math.sign(e)*e*e}function Y7(e){return Math.sign(e)*Math.sqrt(Math.abs(e))}function JE(){var e=n0(),t=[0,1],r=!1,n;function i(a){var o=Y7(e(a));return isNaN(o)?n:r?Math.round(o):o}return i.invert=function(a){return e.invert(lw(a))},i.domain=function(a){return arguments.length?(e.domain(a),i):e.domain()},i.range=function(a){return arguments.length?(e.range((t=Array.from(a,wf)).map(lw)),i):t.slice()},i.rangeRound=function(a){return i.range(a).round(!0)},i.round=function(a){return arguments.length?(r=!!a,i):r},i.clamp=function(a){return arguments.length?(e.clamp(a),i):e.clamp()},i.unknown=function(a){return arguments.length?(n=a,i):n},i.copy=function(){return JE(e.domain(),t).round(r).clamp(e.clamp()).unknown(n)},br.apply(i,arguments),gi(i)}function ZE(){var e=[],t=[],r=[],n;function i(){var o=0,s=Math.max(1,t.length);for(r=new Array(s-1);++o0?r[s-1]:e[0],s=r?[n[r-1],t]:[n[u-1],n[u]]},o.unknown=function(l){return arguments.length&&(a=l),o},o.thresholds=function(){return n.slice()},o.copy=function(){return ej().domain([e,t]).range(i).unknown(a)},br.apply(gi(o),arguments)}function tj(){var e=[.5],t=[0,1],r,n=1;function i(a){return a!=null&&a<=a?t[vu(e,a,0,n)]:r}return i.domain=function(a){return arguments.length?(e=Array.from(a),n=Math.min(e.length,t.length-1),i):e.slice()},i.range=function(a){return arguments.length?(t=Array.from(a),n=Math.min(e.length,t.length-1),i):t.slice()},i.invertExtent=function(a){var o=t.indexOf(a);return[e[o-1],e[o]]},i.unknown=function(a){return arguments.length?(r=a,i):r},i.copy=function(){return tj().domain(e).range(t).unknown(r)},br.apply(i,arguments)}const mp=new Date,yp=new Date;function it(e,t,r,n){function i(a){return e(a=arguments.length===0?new Date:new Date(+a)),a}return i.floor=a=>(e(a=new Date(+a)),a),i.ceil=a=>(e(a=new Date(a-1)),t(a,1),e(a),a),i.round=a=>{const o=i(a),s=i.ceil(a);return a-o(t(a=new Date(+a),o==null?1:Math.floor(o)),a),i.range=(a,o,s)=>{const l=[];if(a=i.ceil(a),s=s==null?1:Math.floor(s),!(a0))return l;let u;do l.push(u=new Date(+a)),t(a,s),e(a);while(uit(o=>{if(o>=o)for(;e(o),!a(o);)o.setTime(o-1)},(o,s)=>{if(o>=o)if(s<0)for(;++s<=0;)for(;t(o,-1),!a(o););else for(;--s>=0;)for(;t(o,1),!a(o););}),r&&(i.count=(a,o)=>(mp.setTime(+a),yp.setTime(+o),e(mp),e(yp),Math.floor(r(mp,yp))),i.every=a=>(a=Math.floor(a),!isFinite(a)||!(a>0)?null:a>1?i.filter(n?o=>n(o)%a===0:o=>i.count(0,o)%a===0):i)),i}const Pf=it(()=>{},(e,t)=>{e.setTime(+e+t)},(e,t)=>t-e);Pf.every=e=>(e=Math.floor(e),!isFinite(e)||!(e>0)?null:e>1?it(t=>{t.setTime(Math.floor(t/e)*e)},(t,r)=>{t.setTime(+t+r*e)},(t,r)=>(r-t)/e):Pf);Pf.range;const ln=1e3,dr=ln*60,un=dr*60,xn=un*24,c0=xn*7,uw=xn*30,vp=xn*365,Fi=it(e=>{e.setTime(e-e.getMilliseconds())},(e,t)=>{e.setTime(+e+t*ln)},(e,t)=>(t-e)/ln,e=>e.getUTCSeconds());Fi.range;const f0=it(e=>{e.setTime(e-e.getMilliseconds()-e.getSeconds()*ln)},(e,t)=>{e.setTime(+e+t*dr)},(e,t)=>(t-e)/dr,e=>e.getMinutes());f0.range;const d0=it(e=>{e.setUTCSeconds(0,0)},(e,t)=>{e.setTime(+e+t*dr)},(e,t)=>(t-e)/dr,e=>e.getUTCMinutes());d0.range;const h0=it(e=>{e.setTime(e-e.getMilliseconds()-e.getSeconds()*ln-e.getMinutes()*dr)},(e,t)=>{e.setTime(+e+t*un)},(e,t)=>(t-e)/un,e=>e.getHours());h0.range;const p0=it(e=>{e.setUTCMinutes(0,0,0)},(e,t)=>{e.setTime(+e+t*un)},(e,t)=>(t-e)/un,e=>e.getUTCHours());p0.range;const xu=it(e=>e.setHours(0,0,0,0),(e,t)=>e.setDate(e.getDate()+t),(e,t)=>(t-e-(t.getTimezoneOffset()-e.getTimezoneOffset())*dr)/xn,e=>e.getDate()-1);xu.range;const Gd=it(e=>{e.setUTCHours(0,0,0,0)},(e,t)=>{e.setUTCDate(e.getUTCDate()+t)},(e,t)=>(t-e)/xn,e=>e.getUTCDate()-1);Gd.range;const rj=it(e=>{e.setUTCHours(0,0,0,0)},(e,t)=>{e.setUTCDate(e.getUTCDate()+t)},(e,t)=>(t-e)/xn,e=>Math.floor(e/xn));rj.range;function xa(e){return it(t=>{t.setDate(t.getDate()-(t.getDay()+7-e)%7),t.setHours(0,0,0,0)},(t,r)=>{t.setDate(t.getDate()+r*7)},(t,r)=>(r-t-(r.getTimezoneOffset()-t.getTimezoneOffset())*dr)/c0)}const Xd=xa(0),Af=xa(1),J7=xa(2),Z7=xa(3),To=xa(4),eH=xa(5),tH=xa(6);Xd.range;Af.range;J7.range;Z7.range;To.range;eH.range;tH.range;function wa(e){return it(t=>{t.setUTCDate(t.getUTCDate()-(t.getUTCDay()+7-e)%7),t.setUTCHours(0,0,0,0)},(t,r)=>{t.setUTCDate(t.getUTCDate()+r*7)},(t,r)=>(r-t)/c0)}const Qd=wa(0),Ef=wa(1),rH=wa(2),nH=wa(3),Co=wa(4),iH=wa(5),aH=wa(6);Qd.range;Ef.range;rH.range;nH.range;Co.range;iH.range;aH.range;const m0=it(e=>{e.setDate(1),e.setHours(0,0,0,0)},(e,t)=>{e.setMonth(e.getMonth()+t)},(e,t)=>t.getMonth()-e.getMonth()+(t.getFullYear()-e.getFullYear())*12,e=>e.getMonth());m0.range;const y0=it(e=>{e.setUTCDate(1),e.setUTCHours(0,0,0,0)},(e,t)=>{e.setUTCMonth(e.getUTCMonth()+t)},(e,t)=>t.getUTCMonth()-e.getUTCMonth()+(t.getUTCFullYear()-e.getUTCFullYear())*12,e=>e.getUTCMonth());y0.range;const wn=it(e=>{e.setMonth(0,1),e.setHours(0,0,0,0)},(e,t)=>{e.setFullYear(e.getFullYear()+t)},(e,t)=>t.getFullYear()-e.getFullYear(),e=>e.getFullYear());wn.every=e=>!isFinite(e=Math.floor(e))||!(e>0)?null:it(t=>{t.setFullYear(Math.floor(t.getFullYear()/e)*e),t.setMonth(0,1),t.setHours(0,0,0,0)},(t,r)=>{t.setFullYear(t.getFullYear()+r*e)});wn.range;const Sn=it(e=>{e.setUTCMonth(0,1),e.setUTCHours(0,0,0,0)},(e,t)=>{e.setUTCFullYear(e.getUTCFullYear()+t)},(e,t)=>t.getUTCFullYear()-e.getUTCFullYear(),e=>e.getUTCFullYear());Sn.every=e=>!isFinite(e=Math.floor(e))||!(e>0)?null:it(t=>{t.setUTCFullYear(Math.floor(t.getUTCFullYear()/e)*e),t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)},(t,r)=>{t.setUTCFullYear(t.getUTCFullYear()+r*e)});Sn.range;function nj(e,t,r,n,i,a){const o=[[Fi,1,ln],[Fi,5,5*ln],[Fi,15,15*ln],[Fi,30,30*ln],[a,1,dr],[a,5,5*dr],[a,15,15*dr],[a,30,30*dr],[i,1,un],[i,3,3*un],[i,6,6*un],[i,12,12*un],[n,1,xn],[n,2,2*xn],[r,1,c0],[t,1,uw],[t,3,3*uw],[e,1,vp]];function s(u,f,c){const d=fy).right(o,d);if(h===o.length)return e.every(by(u/vp,f/vp,c));if(h===0)return Pf.every(Math.max(by(u,f,c),1));const[p,m]=o[d/o[h-1][2]53)return null;"w"in W||(W.w=1),"Z"in W?(he=bp(_s(W.y,0,1)),Qe=he.getUTCDay(),he=Qe>4||Qe===0?Ef.ceil(he):Ef(he),he=Gd.offset(he,(W.V-1)*7),W.y=he.getUTCFullYear(),W.m=he.getUTCMonth(),W.d=he.getUTCDate()+(W.w+6)%7):(he=gp(_s(W.y,0,1)),Qe=he.getDay(),he=Qe>4||Qe===0?Af.ceil(he):Af(he),he=xu.offset(he,(W.V-1)*7),W.y=he.getFullYear(),W.m=he.getMonth(),W.d=he.getDate()+(W.w+6)%7)}else("W"in W||"U"in W)&&("w"in W||(W.w="u"in W?W.u%7:"W"in W?1:0),Qe="Z"in W?bp(_s(W.y,0,1)).getUTCDay():gp(_s(W.y,0,1)).getDay(),W.m=0,W.d="W"in W?(W.w+6)%7+W.W*7-(Qe+5)%7:W.w+W.U*7-(Qe+6)%7);return"Z"in W?(W.H+=W.Z/100|0,W.M+=W.Z%100,bp(W)):gp(W)}}function P(X,ie,se,W){for(var Be=0,he=ie.length,Qe=se.length,Ye,Nt;Be=Qe)return-1;if(Ye=ie.charCodeAt(Be++),Ye===37){if(Ye=ie.charAt(Be++),Nt=x[Ye in cw?ie.charAt(Be++):Ye],!Nt||(W=Nt(X,se,W))<0)return-1}else if(Ye!=se.charCodeAt(W++))return-1}return W}function A(X,ie,se){var W=u.exec(ie.slice(se));return W?(X.p=f.get(W[0].toLowerCase()),se+W[0].length):-1}function C(X,ie,se){var W=h.exec(ie.slice(se));return W?(X.w=p.get(W[0].toLowerCase()),se+W[0].length):-1}function N(X,ie,se){var W=c.exec(ie.slice(se));return W?(X.w=d.get(W[0].toLowerCase()),se+W[0].length):-1}function $(X,ie,se){var W=v.exec(ie.slice(se));return W?(X.m=g.get(W[0].toLowerCase()),se+W[0].length):-1}function L(X,ie,se){var W=m.exec(ie.slice(se));return W?(X.m=y.get(W[0].toLowerCase()),se+W[0].length):-1}function I(X,ie,se){return P(X,t,ie,se)}function R(X,ie,se){return P(X,r,ie,se)}function B(X,ie,se){return P(X,n,ie,se)}function z(X){return o[X.getDay()]}function k(X){return a[X.getDay()]}function F(X){return l[X.getMonth()]}function U(X){return s[X.getMonth()]}function K(X){return i[+(X.getHours()>=12)]}function H(X){return 1+~~(X.getMonth()/3)}function J(X){return o[X.getUTCDay()]}function le(X){return a[X.getUTCDay()]}function Oe(X){return l[X.getUTCMonth()]}function He(X){return s[X.getUTCMonth()]}function rr(X){return i[+(X.getUTCHours()>=12)]}function kt(X){return 1+~~(X.getUTCMonth()/3)}return{format:function(X){var ie=S(X+="",b);return ie.toString=function(){return X},ie},parse:function(X){var ie=_(X+="",!1);return ie.toString=function(){return X},ie},utcFormat:function(X){var ie=S(X+="",w);return ie.toString=function(){return X},ie},utcParse:function(X){var ie=_(X+="",!0);return ie.toString=function(){return X},ie}}}var cw={"-":"",_:" ",0:"0"},ct=/^\s*\d+/,fH=/^%/,dH=/[\\^$*+?|[\]().{}]/g;function me(e,t,r){var n=e<0?"-":"",i=(n?-e:e)+"",a=i.length;return n+(a[t.toLowerCase(),r]))}function pH(e,t,r){var n=ct.exec(t.slice(r,r+1));return n?(e.w=+n[0],r+n[0].length):-1}function mH(e,t,r){var n=ct.exec(t.slice(r,r+1));return n?(e.u=+n[0],r+n[0].length):-1}function yH(e,t,r){var n=ct.exec(t.slice(r,r+2));return n?(e.U=+n[0],r+n[0].length):-1}function vH(e,t,r){var n=ct.exec(t.slice(r,r+2));return n?(e.V=+n[0],r+n[0].length):-1}function gH(e,t,r){var n=ct.exec(t.slice(r,r+2));return n?(e.W=+n[0],r+n[0].length):-1}function fw(e,t,r){var n=ct.exec(t.slice(r,r+4));return n?(e.y=+n[0],r+n[0].length):-1}function dw(e,t,r){var n=ct.exec(t.slice(r,r+2));return n?(e.y=+n[0]+(+n[0]>68?1900:2e3),r+n[0].length):-1}function bH(e,t,r){var n=/^(Z)|([+-]\d\d)(?::?(\d\d))?/.exec(t.slice(r,r+6));return n?(e.Z=n[1]?0:-(n[2]+(n[3]||"00")),r+n[0].length):-1}function xH(e,t,r){var n=ct.exec(t.slice(r,r+1));return n?(e.q=n[0]*3-3,r+n[0].length):-1}function wH(e,t,r){var n=ct.exec(t.slice(r,r+2));return n?(e.m=n[0]-1,r+n[0].length):-1}function hw(e,t,r){var n=ct.exec(t.slice(r,r+2));return n?(e.d=+n[0],r+n[0].length):-1}function SH(e,t,r){var n=ct.exec(t.slice(r,r+3));return n?(e.m=0,e.d=+n[0],r+n[0].length):-1}function pw(e,t,r){var n=ct.exec(t.slice(r,r+2));return n?(e.H=+n[0],r+n[0].length):-1}function OH(e,t,r){var n=ct.exec(t.slice(r,r+2));return n?(e.M=+n[0],r+n[0].length):-1}function _H(e,t,r){var n=ct.exec(t.slice(r,r+2));return n?(e.S=+n[0],r+n[0].length):-1}function PH(e,t,r){var n=ct.exec(t.slice(r,r+3));return n?(e.L=+n[0],r+n[0].length):-1}function AH(e,t,r){var n=ct.exec(t.slice(r,r+6));return n?(e.L=Math.floor(n[0]/1e3),r+n[0].length):-1}function EH(e,t,r){var n=fH.exec(t.slice(r,r+1));return n?r+n[0].length:-1}function jH(e,t,r){var n=ct.exec(t.slice(r));return n?(e.Q=+n[0],r+n[0].length):-1}function TH(e,t,r){var n=ct.exec(t.slice(r));return n?(e.s=+n[0],r+n[0].length):-1}function mw(e,t){return me(e.getDate(),t,2)}function CH(e,t){return me(e.getHours(),t,2)}function $H(e,t){return me(e.getHours()%12||12,t,2)}function kH(e,t){return me(1+xu.count(wn(e),e),t,3)}function ij(e,t){return me(e.getMilliseconds(),t,3)}function NH(e,t){return ij(e,t)+"000"}function MH(e,t){return me(e.getMonth()+1,t,2)}function IH(e,t){return me(e.getMinutes(),t,2)}function RH(e,t){return me(e.getSeconds(),t,2)}function DH(e){var t=e.getDay();return t===0?7:t}function LH(e,t){return me(Xd.count(wn(e)-1,e),t,2)}function aj(e){var t=e.getDay();return t>=4||t===0?To(e):To.ceil(e)}function BH(e,t){return e=aj(e),me(To.count(wn(e),e)+(wn(e).getDay()===4),t,2)}function FH(e){return e.getDay()}function UH(e,t){return me(Af.count(wn(e)-1,e),t,2)}function zH(e,t){return me(e.getFullYear()%100,t,2)}function WH(e,t){return e=aj(e),me(e.getFullYear()%100,t,2)}function HH(e,t){return me(e.getFullYear()%1e4,t,4)}function qH(e,t){var r=e.getDay();return e=r>=4||r===0?To(e):To.ceil(e),me(e.getFullYear()%1e4,t,4)}function KH(e){var t=e.getTimezoneOffset();return(t>0?"-":(t*=-1,"+"))+me(t/60|0,"0",2)+me(t%60,"0",2)}function yw(e,t){return me(e.getUTCDate(),t,2)}function VH(e,t){return me(e.getUTCHours(),t,2)}function GH(e,t){return me(e.getUTCHours()%12||12,t,2)}function XH(e,t){return me(1+Gd.count(Sn(e),e),t,3)}function oj(e,t){return me(e.getUTCMilliseconds(),t,3)}function QH(e,t){return oj(e,t)+"000"}function YH(e,t){return me(e.getUTCMonth()+1,t,2)}function JH(e,t){return me(e.getUTCMinutes(),t,2)}function ZH(e,t){return me(e.getUTCSeconds(),t,2)}function e9(e){var t=e.getUTCDay();return t===0?7:t}function t9(e,t){return me(Qd.count(Sn(e)-1,e),t,2)}function sj(e){var t=e.getUTCDay();return t>=4||t===0?Co(e):Co.ceil(e)}function r9(e,t){return e=sj(e),me(Co.count(Sn(e),e)+(Sn(e).getUTCDay()===4),t,2)}function n9(e){return e.getUTCDay()}function i9(e,t){return me(Ef.count(Sn(e)-1,e),t,2)}function a9(e,t){return me(e.getUTCFullYear()%100,t,2)}function o9(e,t){return e=sj(e),me(e.getUTCFullYear()%100,t,2)}function s9(e,t){return me(e.getUTCFullYear()%1e4,t,4)}function l9(e,t){var r=e.getUTCDay();return e=r>=4||r===0?Co(e):Co.ceil(e),me(e.getUTCFullYear()%1e4,t,4)}function u9(){return"+0000"}function vw(){return"%"}function gw(e){return+e}function bw(e){return Math.floor(+e/1e3)}var Aa,lj,uj;c9({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});function c9(e){return Aa=cH(e),lj=Aa.format,Aa.parse,uj=Aa.utcFormat,Aa.utcParse,Aa}function f9(e){return new Date(e)}function d9(e){return e instanceof Date?+e:+new Date(+e)}function v0(e,t,r,n,i,a,o,s,l,u){var f=n0(),c=f.invert,d=f.domain,h=u(".%L"),p=u(":%S"),m=u("%I:%M"),y=u("%I %p"),v=u("%a %d"),g=u("%b %d"),b=u("%B"),w=u("%Y");function x(S){return(l(S)t(i/(e.length-1)))},r.quantiles=function(n){return Array.from({length:n+1},(i,a)=>t7(e,a/n))},r.copy=function(){return hj(t).domain(e)},jn.apply(r,arguments)}function Jd(){var e=0,t=.5,r=1,n=1,i,a,o,s,l,u=jt,f,c=!1,d;function h(m){return isNaN(m=+m)?d:(m=.5+((m=+f(m))-a)*(n*mt}var vj=b9,x9=Zd,w9=vj,S9=os;function O9(e){return e&&e.length?x9(e,S9,w9):void 0}var _9=O9;const eh=Se(_9);function P9(e,t){return ee.e^a.s<0?1:-1;for(n=a.d.length,i=e.d.length,t=0,r=ne.d[t]^a.s<0?1:-1;return n===i?0:n>i^a.s<0?1:-1};Y.decimalPlaces=Y.dp=function(){var e=this,t=e.d.length-1,r=(t-e.e)*ke;if(t=e.d[t],t)for(;t%10==0;t/=10)r--;return r<0?0:r};Y.dividedBy=Y.div=function(e){return hn(this,new this.constructor(e))};Y.dividedToIntegerBy=Y.idiv=function(e){var t=this,r=t.constructor;return _e(hn(t,new r(e),0,1),r.precision)};Y.equals=Y.eq=function(e){return!this.cmp(e)};Y.exponent=function(){return Xe(this)};Y.greaterThan=Y.gt=function(e){return this.cmp(e)>0};Y.greaterThanOrEqualTo=Y.gte=function(e){return this.cmp(e)>=0};Y.isInteger=Y.isint=function(){return this.e>this.d.length-2};Y.isNegative=Y.isneg=function(){return this.s<0};Y.isPositive=Y.ispos=function(){return this.s>0};Y.isZero=function(){return this.s===0};Y.lessThan=Y.lt=function(e){return this.cmp(e)<0};Y.lessThanOrEqualTo=Y.lte=function(e){return this.cmp(e)<1};Y.logarithm=Y.log=function(e){var t,r=this,n=r.constructor,i=n.precision,a=i+5;if(e===void 0)e=new n(10);else if(e=new n(e),e.s<1||e.eq(Xt))throw Error(vr+"NaN");if(r.s<1)throw Error(vr+(r.s?"NaN":"-Infinity"));return r.eq(Xt)?new n(0):(Ie=!1,t=hn(Nl(r,a),Nl(e,a),a),Ie=!0,_e(t,i))};Y.minus=Y.sub=function(e){var t=this;return e=new t.constructor(e),t.s==e.s?Sj(t,e):xj(t,(e.s=-e.s,e))};Y.modulo=Y.mod=function(e){var t,r=this,n=r.constructor,i=n.precision;if(e=new n(e),!e.s)throw Error(vr+"NaN");return r.s?(Ie=!1,t=hn(r,e,0,1).times(e),Ie=!0,r.minus(t)):_e(new n(r),i)};Y.naturalExponential=Y.exp=function(){return wj(this)};Y.naturalLogarithm=Y.ln=function(){return Nl(this)};Y.negated=Y.neg=function(){var e=new this.constructor(this);return e.s=-e.s||0,e};Y.plus=Y.add=function(e){var t=this;return e=new t.constructor(e),t.s==e.s?xj(t,e):Sj(t,(e.s=-e.s,e))};Y.precision=Y.sd=function(e){var t,r,n,i=this;if(e!==void 0&&e!==!!e&&e!==1&&e!==0)throw Error(ta+e);if(t=Xe(i)+1,n=i.d.length-1,r=n*ke+1,n=i.d[n],n){for(;n%10==0;n/=10)r--;for(n=i.d[0];n>=10;n/=10)r++}return e&&t>r?t:r};Y.squareRoot=Y.sqrt=function(){var e,t,r,n,i,a,o,s=this,l=s.constructor;if(s.s<1){if(!s.s)return new l(0);throw Error(vr+"NaN")}for(e=Xe(s),Ie=!1,i=Math.sqrt(+s),i==0||i==1/0?(t=Wr(s.d),(t.length+e)%2==0&&(t+="0"),i=Math.sqrt(t),e=cs((e+1)/2)-(e<0||e%2),i==1/0?t="5e"+e:(t=i.toExponential(),t=t.slice(0,t.indexOf("e")+1)+e),n=new l(t)):n=new l(i.toString()),r=l.precision,i=o=r+3;;)if(a=n,n=a.plus(hn(s,a,o+2)).times(.5),Wr(a.d).slice(0,o)===(t=Wr(n.d)).slice(0,o)){if(t=t.slice(o-3,o+1),i==o&&t=="4999"){if(_e(a,r+1,0),a.times(a).eq(s)){n=a;break}}else if(t!="9999")break;o+=4}return Ie=!0,_e(n,r)};Y.times=Y.mul=function(e){var t,r,n,i,a,o,s,l,u,f=this,c=f.constructor,d=f.d,h=(e=new c(e)).d;if(!f.s||!e.s)return new c(0);for(e.s*=f.s,r=f.e+e.e,l=d.length,u=h.length,l=0;){for(t=0,i=l+n;i>n;)s=a[i]+h[n]*d[i-n-1]+t,a[i--]=s%ot|0,t=s/ot|0;a[i]=(a[i]+t)%ot|0}for(;!a[--o];)a.pop();return t?++r:a.shift(),e.d=a,e.e=r,Ie?_e(e,c.precision):e};Y.toDecimalPlaces=Y.todp=function(e,t){var r=this,n=r.constructor;return r=new n(r),e===void 0?r:(Gr(e,0,us),t===void 0?t=n.rounding:Gr(t,0,8),_e(r,e+Xe(r)+1,t))};Y.toExponential=function(e,t){var r,n=this,i=n.constructor;return e===void 0?r=ha(n,!0):(Gr(e,0,us),t===void 0?t=i.rounding:Gr(t,0,8),n=_e(new i(n),e+1,t),r=ha(n,!0,e+1)),r};Y.toFixed=function(e,t){var r,n,i=this,a=i.constructor;return e===void 0?ha(i):(Gr(e,0,us),t===void 0?t=a.rounding:Gr(t,0,8),n=_e(new a(i),e+Xe(i)+1,t),r=ha(n.abs(),!1,e+Xe(n)+1),i.isneg()&&!i.isZero()?"-"+r:r)};Y.toInteger=Y.toint=function(){var e=this,t=e.constructor;return _e(new t(e),Xe(e)+1,t.rounding)};Y.toNumber=function(){return+this};Y.toPower=Y.pow=function(e){var t,r,n,i,a,o,s=this,l=s.constructor,u=12,f=+(e=new l(e));if(!e.s)return new l(Xt);if(s=new l(s),!s.s){if(e.s<1)throw Error(vr+"Infinity");return s}if(s.eq(Xt))return s;if(n=l.precision,e.eq(Xt))return _e(s,n);if(t=e.e,r=e.d.length-1,o=t>=r,a=s.s,o){if((r=f<0?-f:f)<=bj){for(i=new l(Xt),t=Math.ceil(n/ke+4),Ie=!1;r%2&&(i=i.times(s),Sw(i.d,t)),r=cs(r/2),r!==0;)s=s.times(s),Sw(s.d,t);return Ie=!0,e.s<0?new l(Xt).div(i):_e(i,n)}}else if(a<0)throw Error(vr+"NaN");return a=a<0&&e.d[Math.max(t,r)]&1?-1:1,s.s=1,Ie=!1,i=e.times(Nl(s,n+u)),Ie=!0,i=wj(i),i.s=a,i};Y.toPrecision=function(e,t){var r,n,i=this,a=i.constructor;return e===void 0?(r=Xe(i),n=ha(i,r<=a.toExpNeg||r>=a.toExpPos)):(Gr(e,1,us),t===void 0?t=a.rounding:Gr(t,0,8),i=_e(new a(i),e,t),r=Xe(i),n=ha(i,e<=r||r<=a.toExpNeg,e)),n};Y.toSignificantDigits=Y.tosd=function(e,t){var r=this,n=r.constructor;return e===void 0?(e=n.precision,t=n.rounding):(Gr(e,1,us),t===void 0?t=n.rounding:Gr(t,0,8)),_e(new n(r),e,t)};Y.toString=Y.valueOf=Y.val=Y.toJSON=Y[Symbol.for("nodejs.util.inspect.custom")]=function(){var e=this,t=Xe(e),r=e.constructor;return ha(e,t<=r.toExpNeg||t>=r.toExpPos)};function xj(e,t){var r,n,i,a,o,s,l,u,f=e.constructor,c=f.precision;if(!e.s||!t.s)return t.s||(t=new f(e)),Ie?_e(t,c):t;if(l=e.d,u=t.d,o=e.e,i=t.e,l=l.slice(),a=o-i,a){for(a<0?(n=l,a=-a,s=u.length):(n=u,i=o,s=l.length),o=Math.ceil(c/ke),s=o>s?o+1:s+1,a>s&&(a=s,n.length=1),n.reverse();a--;)n.push(0);n.reverse()}for(s=l.length,a=u.length,s-a<0&&(a=s,n=u,u=l,l=n),r=0;a;)r=(l[--a]=l[a]+u[a]+r)/ot|0,l[a]%=ot;for(r&&(l.unshift(r),++i),s=l.length;l[--s]==0;)l.pop();return t.d=l,t.e=i,Ie?_e(t,c):t}function Gr(e,t,r){if(e!==~~e||er)throw Error(ta+e)}function Wr(e){var t,r,n,i=e.length-1,a="",o=e[0];if(i>0){for(a+=o,t=1;to?1:-1;else for(s=l=0;si[s]?1:-1;break}return l}function r(n,i,a){for(var o=0;a--;)n[a]-=o,o=n[a]1;)n.shift()}return function(n,i,a,o){var s,l,u,f,c,d,h,p,m,y,v,g,b,w,x,S,_,P,A=n.constructor,C=n.s==i.s?1:-1,N=n.d,$=i.d;if(!n.s)return new A(n);if(!i.s)throw Error(vr+"Division by zero");for(l=n.e-i.e,_=$.length,x=N.length,h=new A(C),p=h.d=[],u=0;$[u]==(N[u]||0);)++u;if($[u]>(N[u]||0)&&--l,a==null?g=a=A.precision:o?g=a+(Xe(n)-Xe(i))+1:g=a,g<0)return new A(0);if(g=g/ke+2|0,u=0,_==1)for(f=0,$=$[0],g++;(u1&&($=e($,f),N=e(N,f),_=$.length,x=N.length),w=_,m=N.slice(0,_),y=m.length;y<_;)m[y++]=0;P=$.slice(),P.unshift(0),S=$[0],$[1]>=ot/2&&++S;do f=0,s=t($,m,_,y),s<0?(v=m[0],_!=y&&(v=v*ot+(m[1]||0)),f=v/S|0,f>1?(f>=ot&&(f=ot-1),c=e($,f),d=c.length,y=m.length,s=t(c,m,d,y),s==1&&(f--,r(c,_16)throw Error(x0+Xe(e));if(!e.s)return new f(Xt);for(Ie=!1,s=c,o=new f(.03125);e.abs().gte(.1);)e=e.times(o),u+=5;for(n=Math.log($i(2,u))/Math.LN10*2+5|0,s+=n,r=i=a=new f(Xt),f.precision=s;;){if(i=_e(i.times(e),s),r=r.times(++l),o=a.plus(hn(i,r,s)),Wr(o.d).slice(0,s)===Wr(a.d).slice(0,s)){for(;u--;)a=_e(a.times(a),s);return f.precision=c,t==null?(Ie=!0,_e(a,c)):a}a=o}}function Xe(e){for(var t=e.e*ke,r=e.d[0];r>=10;r/=10)t++;return t}function xp(e,t,r){if(t>e.LN10.sd())throw Ie=!0,r&&(e.precision=r),Error(vr+"LN10 precision limit exceeded");return _e(new e(e.LN10),t)}function Rn(e){for(var t="";e--;)t+="0";return t}function Nl(e,t){var r,n,i,a,o,s,l,u,f,c=1,d=10,h=e,p=h.d,m=h.constructor,y=m.precision;if(h.s<1)throw Error(vr+(h.s?"NaN":"-Infinity"));if(h.eq(Xt))return new m(0);if(t==null?(Ie=!1,u=y):u=t,h.eq(10))return t==null&&(Ie=!0),xp(m,u);if(u+=d,m.precision=u,r=Wr(p),n=r.charAt(0),a=Xe(h),Math.abs(a)<15e14){for(;n<7&&n!=1||n==1&&r.charAt(1)>3;)h=h.times(e),r=Wr(h.d),n=r.charAt(0),c++;a=Xe(h),n>1?(h=new m("0."+r),a++):h=new m(n+"."+r.slice(1))}else return l=xp(m,u+2,y).times(a+""),h=Nl(new m(n+"."+r.slice(1)),u-d).plus(l),m.precision=y,t==null?(Ie=!0,_e(h,y)):h;for(s=o=h=hn(h.minus(Xt),h.plus(Xt),u),f=_e(h.times(h),u),i=3;;){if(o=_e(o.times(f),u),l=s.plus(hn(o,new m(i),u)),Wr(l.d).slice(0,u)===Wr(s.d).slice(0,u))return s=s.times(2),a!==0&&(s=s.plus(xp(m,u+2,y).times(a+""))),s=hn(s,new m(c),u),m.precision=y,t==null?(Ie=!0,_e(s,y)):s;s=l,i+=2}}function ww(e,t){var r,n,i;for((r=t.indexOf("."))>-1&&(t=t.replace(".","")),(n=t.search(/e/i))>0?(r<0&&(r=n),r+=+t.slice(n+1),t=t.substring(0,n)):r<0&&(r=t.length),n=0;t.charCodeAt(n)===48;)++n;for(i=t.length;t.charCodeAt(i-1)===48;)--i;if(t=t.slice(n,i),t){if(i-=n,r=r-n-1,e.e=cs(r/ke),e.d=[],n=(r+1)%ke,r<0&&(n+=ke),njf||e.e<-jf))throw Error(x0+r)}else e.s=0,e.e=0,e.d=[0];return e}function _e(e,t,r){var n,i,a,o,s,l,u,f,c=e.d;for(o=1,a=c[0];a>=10;a/=10)o++;if(n=t-o,n<0)n+=ke,i=t,u=c[f=0];else{if(f=Math.ceil((n+1)/ke),a=c.length,f>=a)return e;for(u=a=c[f],o=1;a>=10;a/=10)o++;n%=ke,i=n-ke+o}if(r!==void 0&&(a=$i(10,o-i-1),s=u/a%10|0,l=t<0||c[f+1]!==void 0||u%a,l=r<4?(s||l)&&(r==0||r==(e.s<0?3:2)):s>5||s==5&&(r==4||l||r==6&&(n>0?i>0?u/$i(10,o-i):0:c[f-1])%10&1||r==(e.s<0?8:7))),t<1||!c[0])return l?(a=Xe(e),c.length=1,t=t-a-1,c[0]=$i(10,(ke-t%ke)%ke),e.e=cs(-t/ke)||0):(c.length=1,c[0]=e.e=e.s=0),e;if(n==0?(c.length=f,a=1,f--):(c.length=f+1,a=$i(10,ke-n),c[f]=i>0?(u/$i(10,o-i)%$i(10,i)|0)*a:0),l)for(;;)if(f==0){(c[0]+=a)==ot&&(c[0]=1,++e.e);break}else{if(c[f]+=a,c[f]!=ot)break;c[f--]=0,a=1}for(n=c.length;c[--n]===0;)c.pop();if(Ie&&(e.e>jf||e.e<-jf))throw Error(x0+Xe(e));return e}function Sj(e,t){var r,n,i,a,o,s,l,u,f,c,d=e.constructor,h=d.precision;if(!e.s||!t.s)return t.s?t.s=-t.s:t=new d(e),Ie?_e(t,h):t;if(l=e.d,c=t.d,n=t.e,u=e.e,l=l.slice(),o=u-n,o){for(f=o<0,f?(r=l,o=-o,s=c.length):(r=c,n=u,s=l.length),i=Math.max(Math.ceil(h/ke),s)+2,o>i&&(o=i,r.length=1),r.reverse(),i=o;i--;)r.push(0);r.reverse()}else{for(i=l.length,s=c.length,f=i0;--i)l[s++]=0;for(i=c.length;i>o;){if(l[--i]0?a=a.charAt(0)+"."+a.slice(1)+Rn(n):o>1&&(a=a.charAt(0)+"."+a.slice(1)),a=a+(i<0?"e":"e+")+i):i<0?(a="0."+Rn(-i-1)+a,r&&(n=r-o)>0&&(a+=Rn(n))):i>=o?(a+=Rn(i+1-o),r&&(n=r-i-1)>0&&(a=a+"."+Rn(n))):((n=i+1)0&&(i+1===o&&(a+="."),a+=Rn(n))),e.s<0?"-"+a:a}function Sw(e,t){if(e.length>t)return e.length=t,!0}function Oj(e){var t,r,n;function i(a){var o=this;if(!(o instanceof i))return new i(a);if(o.constructor=i,a instanceof i){o.s=a.s,o.e=a.e,o.d=(a=a.d)?a.slice():a;return}if(typeof a=="number"){if(a*0!==0)throw Error(ta+a);if(a>0)o.s=1;else if(a<0)a=-a,o.s=-1;else{o.s=0,o.e=0,o.d=[0];return}if(a===~~a&&a<1e7){o.e=0,o.d=[a];return}return ww(o,a.toString())}else if(typeof a!="string")throw Error(ta+a);if(a.charCodeAt(0)===45?(a=a.slice(1),o.s=-1):o.s=1,K9.test(a))ww(o,a);else throw Error(ta+a)}if(i.prototype=Y,i.ROUND_UP=0,i.ROUND_DOWN=1,i.ROUND_CEIL=2,i.ROUND_FLOOR=3,i.ROUND_HALF_UP=4,i.ROUND_HALF_DOWN=5,i.ROUND_HALF_EVEN=6,i.ROUND_HALF_CEIL=7,i.ROUND_HALF_FLOOR=8,i.clone=Oj,i.config=i.set=V9,e===void 0&&(e={}),e)for(n=["precision","rounding","toExpNeg","toExpPos","LN10"],t=0;t=i[t+1]&&n<=i[t+2])this[r]=n;else throw Error(ta+r+": "+n);if((n=e[r="LN10"])!==void 0)if(n==Math.LN10)this[r]=new this(n);else throw Error(ta+r+": "+n);return this}var w0=Oj(q9);Xt=new w0(1);const we=w0;function G9(e){return J9(e)||Y9(e)||Q9(e)||X9()}function X9(){throw new TypeError(`Invalid attempt to spread non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function Q9(e,t){if(e){if(typeof e=="string")return _y(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);if(r==="Object"&&e.constructor&&(r=e.constructor.name),r==="Map"||r==="Set")return Array.from(e);if(r==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return _y(e,t)}}function Y9(e){if(typeof Symbol<"u"&&Symbol.iterator in Object(e))return Array.from(e)}function J9(e){if(Array.isArray(e))return _y(e)}function _y(e,t){(t==null||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r=t?r.apply(void 0,i):e(t-o,Ow(function(){for(var s=arguments.length,l=new Array(s),u=0;ue.length)&&(t=e.length);for(var r=0,n=new Array(t);r"u"||!(Symbol.iterator in Object(e)))){var r=[],n=!0,i=!1,a=void 0;try{for(var o=e[Symbol.iterator](),s;!(n=(s=o.next()).done)&&(r.push(s.value),!(t&&r.length===t));n=!0);}catch(l){i=!0,a=l}finally{try{!n&&o.return!=null&&o.return()}finally{if(i)throw a}}return r}}function hq(e){if(Array.isArray(e))return e}function jj(e){var t=Ml(e,2),r=t[0],n=t[1],i=r,a=n;return r>n&&(i=n,a=r),[i,a]}function Tj(e,t,r){if(e.lte(0))return new we(0);var n=ih.getDigitCount(e.toNumber()),i=new we(10).pow(n),a=e.div(i),o=n!==1?.05:.1,s=new we(Math.ceil(a.div(o).toNumber())).add(r).mul(o),l=s.mul(i);return t?l:new we(Math.ceil(l))}function pq(e,t,r){var n=1,i=new we(e);if(!i.isint()&&r){var a=Math.abs(e);a<1?(n=new we(10).pow(ih.getDigitCount(e)-1),i=new we(Math.floor(i.div(n).toNumber())).mul(n)):a>1&&(i=new we(Math.floor(e)))}else e===0?i=new we(Math.floor((t-1)/2)):r||(i=new we(Math.floor(e)));var o=Math.floor((t-1)/2),s=rq(tq(function(l){return i.add(new we(l-o).mul(n)).toNumber()}),Py);return s(0,t)}function Cj(e,t,r,n){var i=arguments.length>4&&arguments[4]!==void 0?arguments[4]:0;if(!Number.isFinite((t-e)/(r-1)))return{step:new we(0),tickMin:new we(0),tickMax:new we(0)};var a=Tj(new we(t).sub(e).div(r-1),n,i),o;e<=0&&t>=0?o=new we(0):(o=new we(e).add(t).div(2),o=o.sub(new we(o).mod(a)));var s=Math.ceil(o.sub(e).div(a).toNumber()),l=Math.ceil(new we(t).sub(o).div(a).toNumber()),u=s+l+1;return u>r?Cj(e,t,r,n,i+1):(u0?l+(r-u):l,s=t>0?s:s+(r-u)),{step:a,tickMin:o.sub(new we(s).mul(a)),tickMax:o.add(new we(l).mul(a))})}function mq(e){var t=Ml(e,2),r=t[0],n=t[1],i=arguments.length>1&&arguments[1]!==void 0?arguments[1]:6,a=arguments.length>2&&arguments[2]!==void 0?arguments[2]:!0,o=Math.max(i,2),s=jj([r,n]),l=Ml(s,2),u=l[0],f=l[1];if(u===-1/0||f===1/0){var c=f===1/0?[u].concat(Ey(Py(0,i-1).map(function(){return 1/0}))):[].concat(Ey(Py(0,i-1).map(function(){return-1/0})),[f]);return r>n?Ay(c):c}if(u===f)return pq(u,i,a);var d=Cj(u,f,o,a),h=d.step,p=d.tickMin,m=d.tickMax,y=ih.rangeStep(p,m.add(new we(.1).mul(h)),h);return r>n?Ay(y):y}function yq(e,t){var r=Ml(e,2),n=r[0],i=r[1],a=arguments.length>2&&arguments[2]!==void 0?arguments[2]:!0,o=jj([n,i]),s=Ml(o,2),l=s[0],u=s[1];if(l===-1/0||u===1/0)return[n,i];if(l===u)return[l];var f=Math.max(t,2),c=Tj(new we(u).sub(l).div(f-1),a,0),d=[].concat(Ey(ih.rangeStep(new we(l),new we(u).sub(new we(.99).mul(c)),c)),[u]);return n>i?Ay(d):d}var vq=Aj(mq),gq=Aj(yq),bq="Invariant failed";function pa(e,t){throw new Error(bq)}var xq=["offset","layout","width","dataKey","data","dataPointFormatter","xAxis","yAxis"];function $o(e){"@babel/helpers - typeof";return $o=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},$o(e)}function Tf(){return Tf=Object.assign?Object.assign.bind():function(e){for(var t=1;te.length)&&(t=e.length);for(var r=0,n=new Array(t);r=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function Eq(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}function jq(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function Tq(e,t){for(var r=0;re.length)&&(t=e.length);for(var r=0,n=new Array(t);r1&&arguments[1]!==void 0?arguments[1]:[],i=arguments.length>2?arguments[2]:void 0,a=arguments.length>3?arguments[3]:void 0,o=-1,s=(r=n==null?void 0:n.length)!==null&&r!==void 0?r:0;if(s<=1)return 0;if(a&&a.axisType==="angleAxis"&&Math.abs(Math.abs(a.range[1]-a.range[0])-360)<=1e-6)for(var l=a.range,u=0;u0?i[u-1].coordinate:i[s-1].coordinate,c=i[u].coordinate,d=u>=s-1?i[0].coordinate:i[u+1].coordinate,h=void 0;if(At(c-f)!==At(d-c)){var p=[];if(At(d-c)===At(l[1]-l[0])){h=d;var m=c+l[1]-l[0];p[0]=Math.min(m,(m+f)/2),p[1]=Math.max(m,(m+f)/2)}else{h=f;var y=d+l[1]-l[0];p[0]=Math.min(c,(y+c)/2),p[1]=Math.max(c,(y+c)/2)}var v=[Math.min(c,(h+c)/2),Math.max(c,(h+c)/2)];if(t>v[0]&&t<=v[1]||t>=p[0]&&t<=p[1]){o=i[u].index;break}}else{var g=Math.min(f,d),b=Math.max(f,d);if(t>(g+c)/2&&t<=(b+c)/2){o=i[u].index;break}}}else for(var w=0;w0&&w(n[w].coordinate+n[w-1].coordinate)/2&&t<=(n[w].coordinate+n[w+1].coordinate)/2||w===s-1&&t>(n[w].coordinate+n[w-1].coordinate)/2){o=n[w].index;break}return o},S0=function(t){var r,n=t,i=n.type.displayName,a=(r=t.type)!==null&&r!==void 0&&r.defaultProps?ze(ze({},t.type.defaultProps),t.props):t.props,o=a.stroke,s=a.fill,l;switch(i){case"Line":l=o;break;case"Area":case"Radar":l=o&&o!=="none"?o:s;break;default:l=s;break}return l},Kq=function(t){var r=t.barSize,n=t.totalSize,i=t.stackGroups,a=i===void 0?{}:i;if(!a)return{};for(var o={},s=Object.keys(a),l=0,u=s.length;l=0});if(v&&v.length){var g=v[0].type.defaultProps,b=g!==void 0?ze(ze({},g),v[0].props):v[0].props,w=b.barSize,x=b[y];o[x]||(o[x]=[]);var S=ue(w)?r:w;o[x].push({item:v[0],stackList:v.slice(1),barSize:ue(S)?void 0:Et(S,n,0)})}}return o},Vq=function(t){var r=t.barGap,n=t.barCategoryGap,i=t.bandSize,a=t.sizeList,o=a===void 0?[]:a,s=t.maxBarSize,l=o.length;if(l<1)return null;var u=Et(r,i,0,!0),f,c=[];if(o[0].barSize===+o[0].barSize){var d=!1,h=i/l,p=o.reduce(function(w,x){return w+x.barSize||0},0);p+=(l-1)*u,p>=i&&(p-=(l-1)*u,u=0),p>=i&&h>0&&(d=!0,h*=.9,p=l*h);var m=(i-p)/2>>0,y={offset:m-u,size:0};f=o.reduce(function(w,x){var S={item:x.item,position:{offset:y.offset+y.size+u,size:d?h:x.barSize}},_=[].concat(Aw(w),[S]);return y=_[_.length-1].position,x.stackList&&x.stackList.length&&x.stackList.forEach(function(P){_.push({item:P,position:y})}),_},c)}else{var v=Et(n,i,0,!0);i-2*v-(l-1)*u<=0&&(u=0);var g=(i-2*v-(l-1)*u)/l;g>1&&(g>>=0);var b=s===+s?Math.min(g,s):g;f=o.reduce(function(w,x,S){var _=[].concat(Aw(w),[{item:x.item,position:{offset:v+(g+u)*S+(g-b)/2,size:b}}]);return x.stackList&&x.stackList.length&&x.stackList.forEach(function(P){_.push({item:P,position:_[_.length-1].position})}),_},c)}return f},Gq=function(t,r,n,i){var a=n.children,o=n.width,s=n.margin,l=o-(s.left||0)-(s.right||0),u=Mj({children:a,legendWidth:l});if(u){var f=i||{},c=f.width,d=f.height,h=u.align,p=u.verticalAlign,m=u.layout;if((m==="vertical"||m==="horizontal"&&p==="middle")&&h!=="center"&&V(t[h]))return ze(ze({},t),{},eo({},h,t[h]+(c||0)));if((m==="horizontal"||m==="vertical"&&h==="center")&&p!=="middle"&&V(t[p]))return ze(ze({},t),{},eo({},p,t[p]+(d||0)))}return t},Xq=function(t,r,n){return ue(r)?!0:t==="horizontal"?r==="yAxis":t==="vertical"||n==="x"?r==="xAxis":n==="y"?r==="yAxis":!0},Ij=function(t,r,n,i,a){var o=r.props.children,s=pr(o,ah).filter(function(u){return Xq(i,a,u.props.direction)});if(s&&s.length){var l=s.map(function(u){return u.props.dataKey});return t.reduce(function(u,f){var c=xt(f,n);if(ue(c))return u;var d=Array.isArray(c)?[th(c),eh(c)]:[c,c],h=l.reduce(function(p,m){var y=xt(f,m,0),v=d[0]-Math.abs(Array.isArray(y)?y[0]:y),g=d[1]+Math.abs(Array.isArray(y)?y[1]:y);return[Math.min(v,p[0]),Math.max(g,p[1])]},[1/0,-1/0]);return[Math.min(h[0],u[0]),Math.max(h[1],u[1])]},[1/0,-1/0])}return null},Qq=function(t,r,n,i,a){var o=r.map(function(s){return Ij(t,s,n,a,i)}).filter(function(s){return!ue(s)});return o&&o.length?o.reduce(function(s,l){return[Math.min(s[0],l[0]),Math.max(s[1],l[1])]},[1/0,-1/0]):null},Rj=function(t,r,n,i,a){var o=r.map(function(l){var u=l.props.dataKey;return n==="number"&&u&&Ij(t,l,u,i)||Qs(t,u,n,a)});if(n==="number")return o.reduce(function(l,u){return[Math.min(l[0],u[0]),Math.max(l[1],u[1])]},[1/0,-1/0]);var s={};return o.reduce(function(l,u){for(var f=0,c=u.length;f=2?At(s[0]-s[1])*2*u:u,r&&(t.ticks||t.niceTicks)){var f=(t.ticks||t.niceTicks).map(function(c){var d=a?a.indexOf(c):c;return{coordinate:i(d)+u,value:c,offset:u}});return f.filter(function(c){return!mu(c.coordinate)})}return t.isCategorical&&t.categoricalDomain?t.categoricalDomain.map(function(c,d){return{coordinate:i(c)+u,value:c,index:d,offset:u}}):i.ticks&&!n?i.ticks(t.tickCount).map(function(c){return{coordinate:i(c)+u,value:c,offset:u}}):i.domain().map(function(c,d){return{coordinate:i(c)+u,value:a?a[c]:c,index:d,offset:u}})},wp=new WeakMap,ec=function(t,r){if(typeof r!="function")return t;wp.has(t)||wp.set(t,new WeakMap);var n=wp.get(t);if(n.has(r))return n.get(r);var i=function(){t.apply(void 0,arguments),r.apply(void 0,arguments)};return n.set(r,i),i},Lj=function(t,r,n){var i=t.scale,a=t.type,o=t.layout,s=t.axisType;if(i==="auto")return o==="radial"&&s==="radiusAxis"?{scale:jl(),realScaleType:"band"}:o==="radial"&&s==="angleAxis"?{scale:_f(),realScaleType:"linear"}:a==="category"&&r&&(r.indexOf("LineChart")>=0||r.indexOf("AreaChart")>=0||r.indexOf("ComposedChart")>=0&&!n)?{scale:Xs(),realScaleType:"point"}:a==="category"?{scale:jl(),realScaleType:"band"}:{scale:_f(),realScaleType:"linear"};if(ca(i)){var l="scale".concat(Bd(i));return{scale:(xw[l]||Xs)(),realScaleType:xw[l]?l:"point"}}return re(i)?{scale:i}:{scale:Xs(),realScaleType:"point"}},jw=1e-4,Bj=function(t){var r=t.domain();if(!(!r||r.length<=2)){var n=r.length,i=t.range(),a=Math.min(i[0],i[1])-jw,o=Math.max(i[0],i[1])+jw,s=t(r[0]),l=t(r[n-1]);(so||lo)&&t.domain([r[0],r[n-1]])}},Yq=function(t,r){if(!t)return null;for(var n=0,i=t.length;ni)&&(a[1]=i),a[0]>i&&(a[0]=i),a[1]=0?(t[s][n][0]=a,t[s][n][1]=a+l,a=t[s][n][1]):(t[s][n][0]=o,t[s][n][1]=o+l,o=t[s][n][1])}},eK=function(t){var r=t.length;if(!(r<=0))for(var n=0,i=t[0].length;n=0?(t[o][n][0]=a,t[o][n][1]=a+s,a=t[o][n][1]):(t[o][n][0]=0,t[o][n][1]=0)}},tK={sign:Zq,expand:b4,none:Oo,silhouette:x4,wiggle:w4,positive:eK},rK=function(t,r,n){var i=r.map(function(s){return s.props.dataKey}),a=tK[n],o=g4().keys(i).value(function(s,l){return+xt(s,l,0)}).order(ty).offset(a);return o(t)},nK=function(t,r,n,i,a,o){if(!t)return null;var s=o?r.reverse():r,l={},u=s.reduce(function(c,d){var h,p=(h=d.type)!==null&&h!==void 0&&h.defaultProps?ze(ze({},d.type.defaultProps),d.props):d.props,m=p.stackId,y=p.hide;if(y)return c;var v=p[n],g=c[v]||{hasStack:!1,stackGroups:{}};if(nt(m)){var b=g.stackGroups[m]||{numericAxisId:n,cateAxisId:i,items:[]};b.items.push(d),g.hasStack=!0,g.stackGroups[m]=b}else g.stackGroups[yu("_stackId_")]={numericAxisId:n,cateAxisId:i,items:[d]};return ze(ze({},c),{},eo({},v,g))},l),f={};return Object.keys(u).reduce(function(c,d){var h=u[d];if(h.hasStack){var p={};h.stackGroups=Object.keys(h.stackGroups).reduce(function(m,y){var v=h.stackGroups[y];return ze(ze({},m),{},eo({},y,{numericAxisId:n,cateAxisId:i,items:v.items,stackedData:rK(t,v.items,a)}))},p)}return ze(ze({},c),{},eo({},d,h))},f)},Fj=function(t,r){var n=r.realScaleType,i=r.type,a=r.tickCount,o=r.originalDomain,s=r.allowDecimals,l=n||r.scale;if(l!=="auto"&&l!=="linear")return null;if(a&&i==="number"&&o&&(o[0]==="auto"||o[1]==="auto")){var u=t.domain();if(!u.length)return null;var f=vq(u,a,s);return t.domain([th(f),eh(f)]),{niceTicks:f}}if(a&&i==="number"){var c=t.domain(),d=gq(c,a,s);return{niceTicks:d}}return null},Tw=function(t){var r=t.axis,n=t.ticks,i=t.offset,a=t.bandSize,o=t.entry,s=t.index;if(r.type==="category")return n[s]?n[s].coordinate+i:null;var l=xt(o,r.dataKey,r.domain[s]);return ue(l)?null:r.scale(l)-a/2+i},iK=function(t){var r=t.numericAxis,n=r.scale.domain();if(r.type==="number"){var i=Math.min(n[0],n[1]),a=Math.max(n[0],n[1]);return i<=0&&a>=0?0:a<0?a:i}return n[0]},aK=function(t,r){var n,i=(n=t.type)!==null&&n!==void 0&&n.defaultProps?ze(ze({},t.type.defaultProps),t.props):t.props,a=i.stackId;if(nt(a)){var o=r[a];if(o){var s=o.items.indexOf(t);return s>=0?o.stackedData[s]:null}}return null},oK=function(t){return t.reduce(function(r,n){return[th(n.concat([r[0]]).filter(V)),eh(n.concat([r[1]]).filter(V))]},[1/0,-1/0])},Uj=function(t,r,n){return Object.keys(t).reduce(function(i,a){var o=t[a],s=o.stackedData,l=s.reduce(function(u,f){var c=oK(f.slice(r,n+1));return[Math.min(u[0],c[0]),Math.max(u[1],c[1])]},[1/0,-1/0]);return[Math.min(l[0],i[0]),Math.max(l[1],i[1])]},[1/0,-1/0]).map(function(i){return i===1/0||i===-1/0?0:i})},Cw=/^dataMin[\s]*-[\s]*([0-9]+([.]{1}[0-9]+){0,1})$/,$w=/^dataMax[\s]*\+[\s]*([0-9]+([.]{1}[0-9]+){0,1})$/,$y=function(t,r,n){if(re(t))return t(r,n);if(!Array.isArray(t))return r;var i=[];if(V(t[0]))i[0]=n?t[0]:Math.min(t[0],r[0]);else if(Cw.test(t[0])){var a=+Cw.exec(t[0])[1];i[0]=r[0]-a}else re(t[0])?i[0]=t[0](r[0]):i[0]=r[0];if(V(t[1]))i[1]=n?t[1]:Math.max(t[1],r[1]);else if($w.test(t[1])){var o=+$w.exec(t[1])[1];i[1]=r[1]+o}else re(t[1])?i[1]=t[1](r[1]):i[1]=r[1];return i},$f=function(t,r,n){if(t&&t.scale&&t.scale.bandwidth){var i=t.scale.bandwidth();if(!n||i>0)return i}if(t&&r&&r.length>=2){for(var a=Qg(r,function(c){return c.coordinate}),o=1/0,s=1,l=a.length;se.length)&&(t=e.length);for(var r=0,n=new Array(t);r2&&arguments[2]!==void 0?arguments[2]:{top:0,right:0,bottom:0,left:0};return Math.min(Math.abs(t-(n.left||0)-(n.right||0)),Math.abs(r-(n.top||0)-(n.bottom||0)))/2},mK=function(t,r,n,i,a){var o=t.width,s=t.height,l=t.startAngle,u=t.endAngle,f=Et(t.cx,o,o/2),c=Et(t.cy,s,s/2),d=Hj(o,s,n),h=Et(t.innerRadius,d,0),p=Et(t.outerRadius,d,d*.8),m=Object.keys(r);return m.reduce(function(y,v){var g=r[v],b=g.domain,w=g.reversed,x;if(ue(g.range))i==="angleAxis"?x=[l,u]:i==="radiusAxis"&&(x=[h,p]),w&&(x=[x[1],x[0]]);else{x=g.range;var S=x,_=uK(S,2);l=_[0],u=_[1]}var P=Lj(g,a),A=P.realScaleType,C=P.scale;C.domain(b).range(x),Bj(C);var N=Fj(C,Zr(Zr({},g),{},{realScaleType:A})),$=Zr(Zr(Zr({},g),N),{},{range:x,radius:p,realScaleType:A,scale:C,cx:f,cy:c,innerRadius:h,outerRadius:p,startAngle:l,endAngle:u});return Zr(Zr({},y),{},Wj({},v,$))},{})},yK=function(t,r){var n=t.x,i=t.y,a=r.x,o=r.y;return Math.sqrt(Math.pow(n-a,2)+Math.pow(i-o,2))},vK=function(t,r){var n=t.x,i=t.y,a=r.cx,o=r.cy,s=yK({x:n,y:i},{x:a,y:o});if(s<=0)return{radius:s};var l=(n-a)/s,u=Math.acos(l);return i>o&&(u=2*Math.PI-u),{radius:s,angle:pK(u),angleInRadian:u}},gK=function(t){var r=t.startAngle,n=t.endAngle,i=Math.floor(r/360),a=Math.floor(n/360),o=Math.min(i,a);return{startAngle:r-o*360,endAngle:n-o*360}},bK=function(t,r){var n=r.startAngle,i=r.endAngle,a=Math.floor(n/360),o=Math.floor(i/360),s=Math.min(a,o);return t+s*360},Iw=function(t,r){var n=t.x,i=t.y,a=vK({x:n,y:i},r),o=a.radius,s=a.angle,l=r.innerRadius,u=r.outerRadius;if(ou)return!1;if(o===0)return!0;var f=gK(r),c=f.startAngle,d=f.endAngle,h=s,p;if(c<=d){for(;h>d;)h-=360;for(;h=c&&h<=d}else{for(;h>c;)h-=360;for(;h=d&&h<=c}return p?Zr(Zr({},r),{},{radius:o,angle:bK(h,r)}):null},qj=function(t){return!j.isValidElement(t)&&!re(t)&&typeof t!="boolean"?t.className:""};function Ll(e){"@babel/helpers - typeof";return Ll=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Ll(e)}var xK=["offset"];function wK(e){return PK(e)||_K(e)||OK(e)||SK()}function SK(){throw new TypeError(`Invalid attempt to spread non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function OK(e,t){if(e){if(typeof e=="string")return ky(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);if(r==="Object"&&e.constructor&&(r=e.constructor.name),r==="Map"||r==="Set")return Array.from(e);if(r==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return ky(e,t)}}function _K(e){if(typeof Symbol<"u"&&e[Symbol.iterator]!=null||e["@@iterator"]!=null)return Array.from(e)}function PK(e){if(Array.isArray(e))return ky(e)}function ky(e,t){(t==null||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function EK(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}function Rw(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function Ze(e){for(var t=1;t=0?1:-1,b,w;i==="insideStart"?(b=h+g*o,w=m):i==="insideEnd"?(b=p-g*o,w=!m):i==="end"&&(b=p+g*o,w=m),w=v<=0?w:!w;var x=je(u,f,y,b),S=je(u,f,y,b+(w?1:-1)*359),_="M".concat(x.x,",").concat(x.y,` + A`).concat(y,",").concat(y,",0,1,").concat(w?0:1,`, + `).concat(S.x,",").concat(S.y),P=ue(t.id)?yu("recharts-radial-line-"):t.id;return T.createElement("text",Bl({},n,{dominantBaseline:"central",className:oe("recharts-radial-bar-label",s)}),T.createElement("defs",null,T.createElement("path",{id:P,d:_})),T.createElement("textPath",{xlinkHref:"#".concat(P)},r))},MK=function(t){var r=t.viewBox,n=t.offset,i=t.position,a=r,o=a.cx,s=a.cy,l=a.innerRadius,u=a.outerRadius,f=a.startAngle,c=a.endAngle,d=(f+c)/2;if(i==="outside"){var h=je(o,s,u+n,d),p=h.x,m=h.y;return{x:p,y:m,textAnchor:p>=o?"start":"end",verticalAnchor:"middle"}}if(i==="center")return{x:o,y:s,textAnchor:"middle",verticalAnchor:"middle"};if(i==="centerTop")return{x:o,y:s,textAnchor:"middle",verticalAnchor:"start"};if(i==="centerBottom")return{x:o,y:s,textAnchor:"middle",verticalAnchor:"end"};var y=(l+u)/2,v=je(o,s,y,d),g=v.x,b=v.y;return{x:g,y:b,textAnchor:"middle",verticalAnchor:"middle"}},IK=function(t){var r=t.viewBox,n=t.parentViewBox,i=t.offset,a=t.position,o=r,s=o.x,l=o.y,u=o.width,f=o.height,c=f>=0?1:-1,d=c*i,h=c>0?"end":"start",p=c>0?"start":"end",m=u>=0?1:-1,y=m*i,v=m>0?"end":"start",g=m>0?"start":"end";if(a==="top"){var b={x:s+u/2,y:l-c*i,textAnchor:"middle",verticalAnchor:h};return Ze(Ze({},b),n?{height:Math.max(l-n.y,0),width:u}:{})}if(a==="bottom"){var w={x:s+u/2,y:l+f+d,textAnchor:"middle",verticalAnchor:p};return Ze(Ze({},w),n?{height:Math.max(n.y+n.height-(l+f),0),width:u}:{})}if(a==="left"){var x={x:s-y,y:l+f/2,textAnchor:v,verticalAnchor:"middle"};return Ze(Ze({},x),n?{width:Math.max(x.x-n.x,0),height:f}:{})}if(a==="right"){var S={x:s+u+y,y:l+f/2,textAnchor:g,verticalAnchor:"middle"};return Ze(Ze({},S),n?{width:Math.max(n.x+n.width-S.x,0),height:f}:{})}var _=n?{width:u,height:f}:{};return a==="insideLeft"?Ze({x:s+y,y:l+f/2,textAnchor:g,verticalAnchor:"middle"},_):a==="insideRight"?Ze({x:s+u-y,y:l+f/2,textAnchor:v,verticalAnchor:"middle"},_):a==="insideTop"?Ze({x:s+u/2,y:l+d,textAnchor:"middle",verticalAnchor:p},_):a==="insideBottom"?Ze({x:s+u/2,y:l+f-d,textAnchor:"middle",verticalAnchor:h},_):a==="insideTopLeft"?Ze({x:s+y,y:l+d,textAnchor:g,verticalAnchor:p},_):a==="insideTopRight"?Ze({x:s+u-y,y:l+d,textAnchor:v,verticalAnchor:p},_):a==="insideBottomLeft"?Ze({x:s+y,y:l+f-d,textAnchor:g,verticalAnchor:h},_):a==="insideBottomRight"?Ze({x:s+u-y,y:l+f-d,textAnchor:v,verticalAnchor:h},_):es(a)&&(V(a.x)||Li(a.x))&&(V(a.y)||Li(a.y))?Ze({x:s+Et(a.x,u),y:l+Et(a.y,f),textAnchor:"end",verticalAnchor:"end"},_):Ze({x:s+u/2,y:l+f/2,textAnchor:"middle",verticalAnchor:"middle"},_)},RK=function(t){return"cx"in t&&V(t.cx)};function lt(e){var t=e.offset,r=t===void 0?5:t,n=AK(e,xK),i=Ze({offset:r},n),a=i.viewBox,o=i.position,s=i.value,l=i.children,u=i.content,f=i.className,c=f===void 0?"":f,d=i.textBreakAll;if(!a||ue(s)&&ue(l)&&!j.isValidElement(u)&&!re(u))return null;if(j.isValidElement(u))return j.cloneElement(u,i);var h;if(re(u)){if(h=j.createElement(u,i),j.isValidElement(h))return h}else h=$K(i);var p=RK(a),m=te(i,!0);if(p&&(o==="insideStart"||o==="insideEnd"||o==="end"))return NK(i,h,m);var y=p?MK(i):IK(i);return T.createElement(da,Bl({className:oe("recharts-label",c)},m,y,{breakAll:d}),h)}lt.displayName="Label";var Kj=function(t){var r=t.cx,n=t.cy,i=t.angle,a=t.startAngle,o=t.endAngle,s=t.r,l=t.radius,u=t.innerRadius,f=t.outerRadius,c=t.x,d=t.y,h=t.top,p=t.left,m=t.width,y=t.height,v=t.clockWise,g=t.labelViewBox;if(g)return g;if(V(m)&&V(y)){if(V(c)&&V(d))return{x:c,y:d,width:m,height:y};if(V(h)&&V(p))return{x:h,y:p,width:m,height:y}}return V(c)&&V(d)?{x:c,y:d,width:0,height:0}:V(r)&&V(n)?{cx:r,cy:n,startAngle:a||i||0,endAngle:o||i||0,innerRadius:u||0,outerRadius:f||l||s||0,clockWise:v}:t.viewBox?t.viewBox:{}},DK=function(t,r){return t?t===!0?T.createElement(lt,{key:"label-implicit",viewBox:r}):nt(t)?T.createElement(lt,{key:"label-implicit",viewBox:r,value:t}):j.isValidElement(t)?t.type===lt?j.cloneElement(t,{key:"label-implicit",viewBox:r}):T.createElement(lt,{key:"label-implicit",content:t,viewBox:r}):re(t)?T.createElement(lt,{key:"label-implicit",content:t,viewBox:r}):es(t)?T.createElement(lt,Bl({viewBox:r},t,{key:"label-implicit"})):null:null},LK=function(t,r){var n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:!0;if(!t||!t.children&&n&&!t.label)return null;var i=t.children,a=Kj(t),o=pr(i,lt).map(function(l,u){return j.cloneElement(l,{viewBox:r||a,key:"label-".concat(u)})});if(!n)return o;var s=DK(t.label,r||a);return[s].concat(wK(o))};lt.parseViewBox=Kj;lt.renderCallByParent=LK;function BK(e){var t=e==null?0:e.length;return t?e[t-1]:void 0}var FK=BK;const UK=Se(FK);function Fl(e){"@babel/helpers - typeof";return Fl=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Fl(e)}var zK=["valueAccessor"],WK=["data","dataKey","clockWise","id","textBreakAll"];function HK(e){return GK(e)||VK(e)||KK(e)||qK()}function qK(){throw new TypeError(`Invalid attempt to spread non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function KK(e,t){if(e){if(typeof e=="string")return Ny(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);if(r==="Object"&&e.constructor&&(r=e.constructor.name),r==="Map"||r==="Set")return Array.from(e);if(r==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return Ny(e,t)}}function VK(e){if(typeof Symbol<"u"&&e[Symbol.iterator]!=null||e["@@iterator"]!=null)return Array.from(e)}function GK(e){if(Array.isArray(e))return Ny(e)}function Ny(e,t){(t==null||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function JK(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}var ZK=function(t){return Array.isArray(t.value)?UK(t.value):t.value};function ui(e){var t=e.valueAccessor,r=t===void 0?ZK:t,n=Bw(e,zK),i=n.data,a=n.dataKey,o=n.clockWise,s=n.id,l=n.textBreakAll,u=Bw(n,WK);return!i||!i.length?null:T.createElement(ge,{className:"recharts-label-list"},i.map(function(f,c){var d=ue(a)?r(f,c):xt(f&&f.payload,a),h=ue(s)?{}:{id:"".concat(s,"-").concat(c)};return T.createElement(lt,Nf({},te(f,!0),u,h,{parentViewBox:f.parentViewBox,value:d,textBreakAll:l,viewBox:lt.parseViewBox(ue(o)?f:Lw(Lw({},f),{},{clockWise:o})),key:"label-".concat(c),index:c}))}))}ui.displayName="LabelList";function eV(e,t){return e?e===!0?T.createElement(ui,{key:"labelList-implicit",data:t}):T.isValidElement(e)||re(e)?T.createElement(ui,{key:"labelList-implicit",data:t,content:e}):es(e)?T.createElement(ui,Nf({data:t},e,{key:"labelList-implicit"})):null:null}function tV(e,t){var r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:!0;if(!e||!e.children&&r&&!e.label)return null;var n=e.children,i=pr(n,ui).map(function(o,s){return j.cloneElement(o,{data:t,key:"labelList-".concat(s)})});if(!r)return i;var a=eV(e.label,t);return[a].concat(HK(i))}ui.renderCallByParent=tV;function Ul(e){"@babel/helpers - typeof";return Ul=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Ul(e)}function My(){return My=Object.assign?Object.assign.bind():function(e){for(var t=1;t180),",").concat(+(o>u),`, + `).concat(c.x,",").concat(c.y,` + `);if(i>0){var h=je(r,n,i,o),p=je(r,n,i,u);d+="L ".concat(p.x,",").concat(p.y,` + A `).concat(i,",").concat(i,`,0, + `).concat(+(Math.abs(l)>180),",").concat(+(o<=u),`, + `).concat(h.x,",").concat(h.y," Z")}else d+="L ".concat(r,",").concat(n," Z");return d},oV=function(t){var r=t.cx,n=t.cy,i=t.innerRadius,a=t.outerRadius,o=t.cornerRadius,s=t.forceCornerRadius,l=t.cornerIsExternal,u=t.startAngle,f=t.endAngle,c=At(f-u),d=tc({cx:r,cy:n,radius:a,angle:u,sign:c,cornerRadius:o,cornerIsExternal:l}),h=d.circleTangency,p=d.lineTangency,m=d.theta,y=tc({cx:r,cy:n,radius:a,angle:f,sign:-c,cornerRadius:o,cornerIsExternal:l}),v=y.circleTangency,g=y.lineTangency,b=y.theta,w=l?Math.abs(u-f):Math.abs(u-f)-m-b;if(w<0)return s?"M ".concat(p.x,",").concat(p.y,` + a`).concat(o,",").concat(o,",0,0,1,").concat(o*2,`,0 + a`).concat(o,",").concat(o,",0,0,1,").concat(-o*2,`,0 + `):Vj({cx:r,cy:n,innerRadius:i,outerRadius:a,startAngle:u,endAngle:f});var x="M ".concat(p.x,",").concat(p.y,` + A`).concat(o,",").concat(o,",0,0,").concat(+(c<0),",").concat(h.x,",").concat(h.y,` + A`).concat(a,",").concat(a,",0,").concat(+(w>180),",").concat(+(c<0),",").concat(v.x,",").concat(v.y,` + A`).concat(o,",").concat(o,",0,0,").concat(+(c<0),",").concat(g.x,",").concat(g.y,` + `);if(i>0){var S=tc({cx:r,cy:n,radius:i,angle:u,sign:c,isExternal:!0,cornerRadius:o,cornerIsExternal:l}),_=S.circleTangency,P=S.lineTangency,A=S.theta,C=tc({cx:r,cy:n,radius:i,angle:f,sign:-c,isExternal:!0,cornerRadius:o,cornerIsExternal:l}),N=C.circleTangency,$=C.lineTangency,L=C.theta,I=l?Math.abs(u-f):Math.abs(u-f)-A-L;if(I<0&&o===0)return"".concat(x,"L").concat(r,",").concat(n,"Z");x+="L".concat($.x,",").concat($.y,` + A`).concat(o,",").concat(o,",0,0,").concat(+(c<0),",").concat(N.x,",").concat(N.y,` + A`).concat(i,",").concat(i,",0,").concat(+(I>180),",").concat(+(c>0),",").concat(_.x,",").concat(_.y,` + A`).concat(o,",").concat(o,",0,0,").concat(+(c<0),",").concat(P.x,",").concat(P.y,"Z")}else x+="L".concat(r,",").concat(n,"Z");return x},sV={cx:0,cy:0,innerRadius:0,outerRadius:0,startAngle:0,endAngle:0,cornerRadius:0,forceCornerRadius:!1,cornerIsExternal:!1},Gj=function(t){var r=Uw(Uw({},sV),t),n=r.cx,i=r.cy,a=r.innerRadius,o=r.outerRadius,s=r.cornerRadius,l=r.forceCornerRadius,u=r.cornerIsExternal,f=r.startAngle,c=r.endAngle,d=r.className;if(o0&&Math.abs(f-c)<360?y=oV({cx:n,cy:i,innerRadius:a,outerRadius:o,cornerRadius:Math.min(m,p/2),forceCornerRadius:l,cornerIsExternal:u,startAngle:f,endAngle:c}):y=Vj({cx:n,cy:i,innerRadius:a,outerRadius:o,startAngle:f,endAngle:c}),T.createElement("path",My({},te(r,!0),{className:h,d:y,role:"img"}))};function zl(e){"@babel/helpers - typeof";return zl=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},zl(e)}function Iy(){return Iy=Object.assign?Object.assign.bind():function(e){for(var t=1;txV.call(e,t));function Sa(e,t){return e===t||!e&&!t&&e!==e&&t!==t}const OV="__v",_V="__o",PV="_owner",{getOwnPropertyDescriptor:Kw,keys:Vw}=Object;function AV(e,t){return e.byteLength===t.byteLength&&Mf(new Uint8Array(e),new Uint8Array(t))}function EV(e,t,r){let n=e.length;if(t.length!==n)return!1;for(;n-- >0;)if(!r.equals(e[n],t[n],n,n,e,t,r))return!1;return!0}function jV(e,t){return e.byteLength===t.byteLength&&Mf(new Uint8Array(e.buffer,e.byteOffset,e.byteLength),new Uint8Array(t.buffer,t.byteOffset,t.byteLength))}function TV(e,t){return Sa(e.getTime(),t.getTime())}function CV(e,t){return e.name===t.name&&e.message===t.message&&e.cause===t.cause&&e.stack===t.stack}function $V(e,t){return e===t}function Gw(e,t,r){const n=e.size;if(n!==t.size)return!1;if(!n)return!0;const i=new Array(n),a=e.entries();let o,s,l=0;for(;(o=a.next())&&!o.done;){const u=t.entries();let f=!1,c=0;for(;(s=u.next())&&!s.done;){if(i[c]){c++;continue}const d=o.value,h=s.value;if(r.equals(d[0],h[0],l,c,e,t,r)&&r.equals(d[1],h[1],d[0],h[0],e,t,r)){f=i[c]=!0;break}c++}if(!f)return!1;l++}return!0}const kV=Sa;function NV(e,t,r){const n=Vw(e);let i=n.length;if(Vw(t).length!==i)return!1;for(;i-- >0;)if(!Jj(e,t,r,n[i]))return!1;return!0}function Ts(e,t,r){const n=qw(e);let i=n.length;if(qw(t).length!==i)return!1;let a,o,s;for(;i-- >0;)if(a=n[i],!Jj(e,t,r,a)||(o=Kw(e,a),s=Kw(t,a),(o||s)&&(!o||!s||o.configurable!==s.configurable||o.enumerable!==s.enumerable||o.writable!==s.writable)))return!1;return!0}function MV(e,t){return Sa(e.valueOf(),t.valueOf())}function IV(e,t){return e.source===t.source&&e.flags===t.flags}function Xw(e,t,r){const n=e.size;if(n!==t.size)return!1;if(!n)return!0;const i=new Array(n),a=e.values();let o,s;for(;(o=a.next())&&!o.done;){const l=t.values();let u=!1,f=0;for(;(s=l.next())&&!s.done;){if(!i[f]&&r.equals(o.value,s.value,o.value,s.value,e,t,r)){u=i[f]=!0;break}f++}if(!u)return!1}return!0}function Mf(e,t){let r=e.byteLength;if(t.byteLength!==r||e.byteOffset!==t.byteOffset)return!1;for(;r-- >0;)if(e[r]!==t[r])return!1;return!0}function RV(e,t){return e.hostname===t.hostname&&e.pathname===t.pathname&&e.protocol===t.protocol&&e.port===t.port&&e.hash===t.hash&&e.username===t.username&&e.password===t.password}function Jj(e,t,r,n){return(n===PV||n===_V||n===OV)&&(e.$$typeof||t.$$typeof)?!0:SV(t,n)&&r.equals(e[n],t[n],n,n,e,t,r)}const DV="[object ArrayBuffer]",LV="[object Arguments]",BV="[object Boolean]",FV="[object DataView]",UV="[object Date]",zV="[object Error]",WV="[object Map]",HV="[object Number]",qV="[object Object]",KV="[object RegExp]",VV="[object Set]",GV="[object String]",XV={"[object Int8Array]":!0,"[object Uint8Array]":!0,"[object Uint8ClampedArray]":!0,"[object Int16Array]":!0,"[object Uint16Array]":!0,"[object Int32Array]":!0,"[object Uint32Array]":!0,"[object Float16Array]":!0,"[object Float32Array]":!0,"[object Float64Array]":!0,"[object BigInt64Array]":!0,"[object BigUint64Array]":!0},QV="[object URL]",YV=Object.prototype.toString;function JV({areArrayBuffersEqual:e,areArraysEqual:t,areDataViewsEqual:r,areDatesEqual:n,areErrorsEqual:i,areFunctionsEqual:a,areMapsEqual:o,areNumbersEqual:s,areObjectsEqual:l,arePrimitiveWrappersEqual:u,areRegExpsEqual:f,areSetsEqual:c,areTypedArraysEqual:d,areUrlsEqual:h,unknownTagComparators:p}){return function(y,v,g){if(y===v)return!0;if(y==null||v==null)return!1;const b=typeof y;if(b!==typeof v)return!1;if(b!=="object")return b==="number"?s(y,v,g):b==="function"?a(y,v,g):!1;const w=y.constructor;if(w!==v.constructor)return!1;if(w===Object)return l(y,v,g);if(Array.isArray(y))return t(y,v,g);if(w===Date)return n(y,v,g);if(w===RegExp)return f(y,v,g);if(w===Map)return o(y,v,g);if(w===Set)return c(y,v,g);const x=YV.call(y);if(x===UV)return n(y,v,g);if(x===KV)return f(y,v,g);if(x===WV)return o(y,v,g);if(x===VV)return c(y,v,g);if(x===qV)return typeof y.then!="function"&&typeof v.then!="function"&&l(y,v,g);if(x===QV)return h(y,v,g);if(x===zV)return i(y,v,g);if(x===LV)return l(y,v,g);if(XV[x])return d(y,v,g);if(x===DV)return e(y,v,g);if(x===FV)return r(y,v,g);if(x===BV||x===HV||x===GV)return u(y,v,g);if(p){let S=p[x];if(!S){const _=wV(y);_&&(S=p[_])}if(S)return S(y,v,g)}return!1}}function ZV({circular:e,createCustomConfig:t,strict:r}){let n={areArrayBuffersEqual:AV,areArraysEqual:r?Ts:EV,areDataViewsEqual:jV,areDatesEqual:TV,areErrorsEqual:CV,areFunctionsEqual:$V,areMapsEqual:r?Sp(Gw,Ts):Gw,areNumbersEqual:kV,areObjectsEqual:r?Ts:NV,arePrimitiveWrappersEqual:MV,areRegExpsEqual:IV,areSetsEqual:r?Sp(Xw,Ts):Xw,areTypedArraysEqual:r?Sp(Mf,Ts):Mf,areUrlsEqual:RV,unknownTagComparators:void 0};if(t&&(n=Object.assign({},n,t(n))),e){const i=nc(n.areArraysEqual),a=nc(n.areMapsEqual),o=nc(n.areObjectsEqual),s=nc(n.areSetsEqual);n=Object.assign({},n,{areArraysEqual:i,areMapsEqual:a,areObjectsEqual:o,areSetsEqual:s})}return n}function eG(e){return function(t,r,n,i,a,o,s){return e(t,r,s)}}function tG({circular:e,comparator:t,createState:r,equals:n,strict:i}){if(r)return function(s,l){const{cache:u=e?new WeakMap:void 0,meta:f}=r();return t(s,l,{cache:u,equals:n,meta:f,strict:i})};if(e)return function(s,l){return t(s,l,{cache:new WeakMap,equals:n,meta:void 0,strict:i})};const a={cache:void 0,equals:n,meta:void 0,strict:i};return function(s,l){return t(s,l,a)}}const rG=xi();xi({strict:!0});xi({circular:!0});xi({circular:!0,strict:!0});xi({createInternalComparator:()=>Sa});xi({strict:!0,createInternalComparator:()=>Sa});xi({circular:!0,createInternalComparator:()=>Sa});xi({circular:!0,createInternalComparator:()=>Sa,strict:!0});function xi(e={}){const{circular:t=!1,createInternalComparator:r,createState:n,strict:i=!1}=e,a=ZV(e),o=JV(a),s=r?r(o):eG(o);return tG({circular:t,comparator:o,createState:n,equals:s,strict:i})}function nG(e){typeof requestAnimationFrame<"u"&&requestAnimationFrame(e)}function Qw(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:0,r=-1,n=function i(a){r<0&&(r=a),a-r>t?(e(a),r=-1):nG(i)};requestAnimationFrame(n)}function Dy(e){"@babel/helpers - typeof";return Dy=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Dy(e)}function iG(e){return lG(e)||sG(e)||oG(e)||aG()}function aG(){throw new TypeError(`Invalid attempt to destructure non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function oG(e,t){if(e){if(typeof e=="string")return Yw(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);if(r==="Object"&&e.constructor&&(r=e.constructor.name),r==="Map"||r==="Set")return Array.from(e);if(r==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return Yw(e,t)}}function Yw(e,t){(t==null||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);re.length)&&(t=e.length);for(var r=0,n=new Array(t);r1?1:v<0?0:v},m=function(v){for(var g=v>1?1:v,b=g,w=0;w<8;++w){var x=c(b)-g,S=h(b);if(Math.abs(x-g)0&&arguments[0]!==void 0?arguments[0]:{},r=t.stiff,n=r===void 0?100:r,i=t.damping,a=i===void 0?8:i,o=t.dt,s=o===void 0?17:o,l=function(f,c,d){var h=-(f-c)*n,p=d*a,m=d+(h-p)*s/1e3,y=d*s/1e3+f;return Math.abs(y-c)e.length)&&(t=e.length);for(var r=0,n=new Array(t);r=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function BG(e,t){if(e==null)return{};var r={},n=Object.keys(e),i,a;for(a=0;a=0)&&(r[i]=e[i]);return r}function Op(e){return WG(e)||zG(e)||UG(e)||FG()}function FG(){throw new TypeError(`Invalid attempt to spread non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function UG(e,t){if(e){if(typeof e=="string")return zy(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);if(r==="Object"&&e.constructor&&(r=e.constructor.name),r==="Map"||r==="Set")return Array.from(e);if(r==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return zy(e,t)}}function zG(e){if(typeof Symbol<"u"&&e[Symbol.iterator]!=null||e["@@iterator"]!=null)return Array.from(e)}function WG(e){if(Array.isArray(e))return zy(e)}function zy(e,t){(t==null||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r"u"||!Reflect.construct||Reflect.construct.sham)return!1;if(typeof Proxy=="function")return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){})),!0}catch{return!1}}function Df(e){return Df=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(r){return r.__proto__||Object.getPrototypeOf(r)},Df(e)}var On=function(e){GG(r,e);var t=XG(r);function r(n,i){var a;HG(this,r),a=t.call(this,n,i);var o=a.props,s=o.isActive,l=o.attributeName,u=o.from,f=o.to,c=o.steps,d=o.children,h=o.duration;if(a.handleStyleChange=a.handleStyleChange.bind(qy(a)),a.changeStyle=a.changeStyle.bind(qy(a)),!s||h<=0)return a.state={style:{}},typeof d=="function"&&(a.state={style:f}),Hy(a);if(c&&c.length)a.state={style:c[0].style};else if(u){if(typeof d=="function")return a.state={style:u},Hy(a);a.state={style:l?Ds({},l,u):u}}else a.state={style:{}};return a}return KG(r,[{key:"componentDidMount",value:function(){var i=this.props,a=i.isActive,o=i.canBegin;this.mounted=!0,!(!a||!o)&&this.runAnimation(this.props)}},{key:"componentDidUpdate",value:function(i){var a=this.props,o=a.isActive,s=a.canBegin,l=a.attributeName,u=a.shouldReAnimate,f=a.to,c=a.from,d=this.state.style;if(s){if(!o){var h={style:l?Ds({},l,f):f};this.state&&d&&(l&&d[l]!==f||!l&&d!==f)&&this.setState(h);return}if(!(rG(i.to,f)&&i.canBegin&&i.isActive)){var p=!i.canBegin||!i.isActive;this.manager&&this.manager.stop(),this.stopJSAnimation&&this.stopJSAnimation();var m=p||u?c:i.to;if(this.state&&d){var y={style:l?Ds({},l,m):m};(l&&d[l]!==m||!l&&d!==m)&&this.setState(y)}this.runAnimation(wr(wr({},this.props),{},{from:m,begin:0}))}}}},{key:"componentWillUnmount",value:function(){this.mounted=!1;var i=this.props.onAnimationEnd;this.unSubscribe&&this.unSubscribe(),this.manager&&(this.manager.stop(),this.manager=null),this.stopJSAnimation&&this.stopJSAnimation(),i&&i()}},{key:"handleStyleChange",value:function(i){this.changeStyle(i)}},{key:"changeStyle",value:function(i){this.mounted&&this.setState({style:i})}},{key:"runJSAnimation",value:function(i){var a=this,o=i.from,s=i.to,l=i.duration,u=i.easing,f=i.begin,c=i.onAnimationEnd,d=i.onAnimationStart,h=RG(o,s,PG(u),l,this.changeStyle),p=function(){a.stopJSAnimation=h()};this.manager.start([d,f,p,l,c])}},{key:"runStepAnimation",value:function(i){var a=this,o=i.steps,s=i.begin,l=i.onAnimationStart,u=o[0],f=u.style,c=u.duration,d=c===void 0?0:c,h=function(m,y,v){if(v===0)return m;var g=y.duration,b=y.easing,w=b===void 0?"ease":b,x=y.style,S=y.properties,_=y.onAnimationEnd,P=v>0?o[v-1]:y,A=S||Object.keys(x);if(typeof w=="function"||w==="spring")return[].concat(Op(m),[a.runJSAnimation.bind(a,{from:P.style,to:x,duration:g,easing:w}),g]);var C=eS(A,g,w),N=wr(wr(wr({},P.style),x),{},{transition:C});return[].concat(Op(m),[N,g,_]).filter(hG)};return this.manager.start([l].concat(Op(o.reduce(h,[f,Math.max(d,s)])),[i.onAnimationEnd]))}},{key:"runAnimation",value:function(i){this.manager||(this.manager=uG());var a=i.begin,o=i.duration,s=i.attributeName,l=i.to,u=i.easing,f=i.onAnimationStart,c=i.onAnimationEnd,d=i.steps,h=i.children,p=this.manager;if(this.unSubscribe=p.subscribe(this.handleStyleChange),typeof u=="function"||typeof h=="function"||u==="spring"){this.runJSAnimation(i);return}if(d.length>1){this.runStepAnimation(i);return}var m=s?Ds({},s,l):l,y=eS(Object.keys(m),o,u);p.start([f,a,wr(wr({},m),{},{transition:y}),o,c])}},{key:"render",value:function(){var i=this.props,a=i.children;i.begin;var o=i.duration;i.attributeName,i.easing;var s=i.isActive;i.steps,i.from,i.to,i.canBegin,i.onAnimationEnd,i.shouldReAnimate,i.onAnimationReStart;var l=LG(i,DG),u=j.Children.count(a),f=this.state.style;if(typeof a=="function")return a(f);if(!s||u===0||o<=0)return a;var c=function(h){var p=h.props,m=p.style,y=m===void 0?{}:m,v=p.className,g=j.cloneElement(h,wr(wr({},l),{},{style:wr(wr({},y),f),className:v}));return g};return u===1?c(j.Children.only(a)):T.createElement("div",null,j.Children.map(a,function(d){return c(d)}))}}]),r}(j.PureComponent);On.displayName="Animate";On.defaultProps={begin:0,duration:1e3,from:"",to:"",attributeName:"",easing:"ease",isActive:!0,canBegin:!0,steps:[],onAnimationEnd:function(){},onAnimationStart:function(){}};On.propTypes={from:ye.oneOfType([ye.object,ye.string]),to:ye.oneOfType([ye.object,ye.string]),attributeName:ye.string,duration:ye.number,begin:ye.number,easing:ye.oneOfType([ye.string,ye.func]),steps:ye.arrayOf(ye.shape({duration:ye.number.isRequired,style:ye.object.isRequired,easing:ye.oneOfType([ye.oneOf(["ease","ease-in","ease-out","ease-in-out","linear"]),ye.func]),properties:ye.arrayOf("string"),onAnimationEnd:ye.func})),children:ye.oneOfType([ye.node,ye.func]),isActive:ye.bool,canBegin:ye.bool,onAnimationEnd:ye.func,shouldReAnimate:ye.bool,onAnimationStart:ye.func,onAnimationReStart:ye.func};function ql(e){"@babel/helpers - typeof";return ql=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},ql(e)}function Lf(){return Lf=Object.assign?Object.assign.bind():function(e){for(var t=1;te.length)&&(t=e.length);for(var r=0,n=new Array(t);r=0?1:-1,l=n>=0?1:-1,u=i>=0&&n>=0||i<0&&n<0?1:0,f;if(o>0&&a instanceof Array){for(var c=[0,0,0,0],d=0,h=4;do?o:a[d];f="M".concat(t,",").concat(r+s*c[0]),c[0]>0&&(f+="A ".concat(c[0],",").concat(c[0],",0,0,").concat(u,",").concat(t+l*c[0],",").concat(r)),f+="L ".concat(t+n-l*c[1],",").concat(r),c[1]>0&&(f+="A ".concat(c[1],",").concat(c[1],",0,0,").concat(u,`, + `).concat(t+n,",").concat(r+s*c[1])),f+="L ".concat(t+n,",").concat(r+i-s*c[2]),c[2]>0&&(f+="A ".concat(c[2],",").concat(c[2],",0,0,").concat(u,`, + `).concat(t+n-l*c[2],",").concat(r+i)),f+="L ".concat(t+l*c[3],",").concat(r+i),c[3]>0&&(f+="A ".concat(c[3],",").concat(c[3],",0,0,").concat(u,`, + `).concat(t,",").concat(r+i-s*c[3])),f+="Z"}else if(o>0&&a===+a&&a>0){var p=Math.min(o,a);f="M ".concat(t,",").concat(r+s*p,` + A `).concat(p,",").concat(p,",0,0,").concat(u,",").concat(t+l*p,",").concat(r,` + L `).concat(t+n-l*p,",").concat(r,` + A `).concat(p,",").concat(p,",0,0,").concat(u,",").concat(t+n,",").concat(r+s*p,` + L `).concat(t+n,",").concat(r+i-s*p,` + A `).concat(p,",").concat(p,",0,0,").concat(u,",").concat(t+n-l*p,",").concat(r+i,` + L `).concat(t+l*p,",").concat(r+i,` + A `).concat(p,",").concat(p,",0,0,").concat(u,",").concat(t,",").concat(r+i-s*p," Z")}else f="M ".concat(t,",").concat(r," h ").concat(n," v ").concat(i," h ").concat(-n," Z");return f},aX=function(t,r){if(!t||!r)return!1;var n=t.x,i=t.y,a=r.x,o=r.y,s=r.width,l=r.height;if(Math.abs(s)>0&&Math.abs(l)>0){var u=Math.min(a,a+s),f=Math.max(a,a+s),c=Math.min(o,o+l),d=Math.max(o,o+l);return n>=u&&n<=f&&i>=c&&i<=d}return!1},oX={x:0,y:0,width:0,height:0,radius:0,isAnimationActive:!1,isUpdateAnimationActive:!1,animationBegin:0,animationDuration:1500,animationEasing:"ease"},O0=function(t){var r=lS(lS({},oX),t),n=j.useRef(),i=j.useState(-1),a=YG(i,2),o=a[0],s=a[1];j.useEffect(function(){if(n.current&&n.current.getTotalLength)try{var w=n.current.getTotalLength();w&&s(w)}catch{}},[]);var l=r.x,u=r.y,f=r.width,c=r.height,d=r.radius,h=r.className,p=r.animationEasing,m=r.animationDuration,y=r.animationBegin,v=r.isAnimationActive,g=r.isUpdateAnimationActive;if(l!==+l||u!==+u||f!==+f||c!==+c||f===0||c===0)return null;var b=oe("recharts-rectangle",h);return g?T.createElement(On,{canBegin:o>0,from:{width:f,height:c,x:l,y:u},to:{width:f,height:c,x:l,y:u},duration:m,animationEasing:p,isActive:g},function(w){var x=w.width,S=w.height,_=w.x,P=w.y;return T.createElement(On,{canBegin:o>0,from:"0px ".concat(o===-1?1:o,"px"),to:"".concat(o,"px 0px"),attributeName:"strokeDasharray",begin:y,duration:m,isActive:v,easing:p},T.createElement("path",Lf({},te(r,!0),{className:b,d:uS(_,P,x,S,d),ref:n})))}):T.createElement("path",Lf({},te(r,!0),{className:b,d:uS(l,u,f,c,d)}))},sX=["points","className","baseLinePoints","connectNulls"];function Ua(){return Ua=Object.assign?Object.assign.bind():function(e){for(var t=1;t=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function uX(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}function cS(e){return hX(e)||dX(e)||fX(e)||cX()}function cX(){throw new TypeError(`Invalid attempt to spread non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function fX(e,t){if(e){if(typeof e=="string")return Ky(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);if(r==="Object"&&e.constructor&&(r=e.constructor.name),r==="Map"||r==="Set")return Array.from(e);if(r==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return Ky(e,t)}}function dX(e){if(typeof Symbol<"u"&&e[Symbol.iterator]!=null||e["@@iterator"]!=null)return Array.from(e)}function hX(e){if(Array.isArray(e))return Ky(e)}function Ky(e,t){(t==null||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r0&&arguments[0]!==void 0?arguments[0]:[],r=[[]];return t.forEach(function(n){fS(n)?r[r.length-1].push(n):r[r.length-1].length>0&&r.push([])}),fS(t[0])&&r[r.length-1].push(t[0]),r[r.length-1].length<=0&&(r=r.slice(0,-1)),r},Js=function(t,r){var n=pX(t);r&&(n=[n.reduce(function(a,o){return[].concat(cS(a),cS(o))},[])]);var i=n.map(function(a){return a.reduce(function(o,s,l){return"".concat(o).concat(l===0?"M":"L").concat(s.x,",").concat(s.y)},"")}).join("");return n.length===1?"".concat(i,"Z"):i},mX=function(t,r,n){var i=Js(t,n);return"".concat(i.slice(-1)==="Z"?i.slice(0,-1):i,"L").concat(Js(r.reverse(),n).slice(1))},yX=function(t){var r=t.points,n=t.className,i=t.baseLinePoints,a=t.connectNulls,o=lX(t,sX);if(!r||!r.length)return null;var s=oe("recharts-polygon",n);if(i&&i.length){var l=o.stroke&&o.stroke!=="none",u=mX(r,i,a);return T.createElement("g",{className:s},T.createElement("path",Ua({},te(o,!0),{fill:u.slice(-1)==="Z"?o.fill:"none",stroke:"none",d:u})),l?T.createElement("path",Ua({},te(o,!0),{fill:"none",d:Js(r,a)})):null,l?T.createElement("path",Ua({},te(o,!0),{fill:"none",d:Js(i,a)})):null)}var f=Js(r,a);return T.createElement("path",Ua({},te(o,!0),{fill:f.slice(-1)==="Z"?o.fill:"none",className:s,d:f}))};function Vy(){return Vy=Object.assign?Object.assign.bind():function(e){for(var t=1;t=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function OX(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}var _X=function(t,r,n,i,a,o){return"M".concat(t,",").concat(a,"v").concat(i,"M").concat(o,",").concat(r,"h").concat(n)},PX=function(t){var r=t.x,n=r===void 0?0:r,i=t.y,a=i===void 0?0:i,o=t.top,s=o===void 0?0:o,l=t.left,u=l===void 0?0:l,f=t.width,c=f===void 0?0:f,d=t.height,h=d===void 0?0:d,p=t.className,m=SX(t,vX),y=gX({x:n,y:a,top:s,left:u,width:c,height:h},m);return!V(n)||!V(a)||!V(c)||!V(h)||!V(s)||!V(u)?null:T.createElement("path",Gy({},te(y,!0),{className:oe("recharts-cross",p),d:_X(n,a,c,h,s,u)}))},AX=Zd,EX=vj,jX=vi;function TX(e,t){return e&&e.length?AX(e,jX(t),EX):void 0}var CX=TX;const $X=Se(CX);var kX=Zd,NX=vi,MX=gj;function IX(e,t){return e&&e.length?kX(e,NX(t),MX):void 0}var RX=IX;const DX=Se(RX);var LX=["cx","cy","angle","ticks","axisLine"],BX=["ticks","tick","angle","tickFormatter","stroke"];function No(e){"@babel/helpers - typeof";return No=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},No(e)}function Zs(){return Zs=Object.assign?Object.assign.bind():function(e){for(var t=1;t=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function FX(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}function UX(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function mS(e,t){for(var r=0;rgS?o=i==="outer"?"start":"end":a<-gS?o=i==="outer"?"end":"start":o="middle",o}},{key:"renderAxisLine",value:function(){var n=this.props,i=n.cx,a=n.cy,o=n.radius,s=n.axisLine,l=n.axisLineType,u=Pi(Pi({},te(this.props,!1)),{},{fill:"none"},te(s,!1));if(l==="circle")return T.createElement(_0,Ni({className:"recharts-polar-angle-axis-line"},u,{cx:i,cy:a,r:o}));var f=this.props.ticks,c=f.map(function(d){return je(i,a,o,d.coordinate)});return T.createElement(yX,Ni({className:"recharts-polar-angle-axis-line"},u,{points:c}))}},{key:"renderTicks",value:function(){var n=this,i=this.props,a=i.ticks,o=i.tick,s=i.tickLine,l=i.tickFormatter,u=i.stroke,f=te(this.props,!1),c=te(o,!1),d=Pi(Pi({},f),{},{fill:"none"},te(s,!1)),h=a.map(function(p,m){var y=n.getTickLineCoord(p),v=n.getTickTextAnchor(p),g=Pi(Pi(Pi({textAnchor:v},f),{},{stroke:"none",fill:u},c),{},{index:m,payload:p,x:y.x2,y:y.y2});return T.createElement(ge,Ni({className:oe("recharts-polar-angle-axis-tick",qj(o)),key:"tick-".concat(p.coordinate)},fa(n.props,p,m)),s&&T.createElement("line",Ni({className:"recharts-polar-angle-axis-tick-line"},d,y)),o&&t.renderTickItem(o,g,l?l(p.value,m):p.value))});return T.createElement(ge,{className:"recharts-polar-angle-axis-ticks"},h)}},{key:"render",value:function(){var n=this.props,i=n.ticks,a=n.radius,o=n.axisLine;return a<=0||!i||!i.length?null:T.createElement(ge,{className:oe("recharts-polar-angle-axis",this.props.className)},o&&this.renderAxisLine(),this.renderTicks())}}],[{key:"renderTickItem",value:function(n,i,a){var o;return T.isValidElement(n)?o=T.cloneElement(n,i):re(n)?o=n(i):o=T.createElement(da,Ni({},i,{className:"recharts-polar-angle-axis-tick-value"}),a),o}}])}(j.PureComponent);lh(uh,"displayName","PolarAngleAxis");lh(uh,"axisType","angleAxis");lh(uh,"defaultProps",{type:"category",angleAxisId:0,scale:"auto",cx:0,cy:0,orientation:"outer",axisLine:!0,tickLine:!0,tickSize:8,tick:!0,hide:!1,allowDuplicatedCategory:!0});var rQ=mE,nQ=rQ(Object.getPrototypeOf,Object),iQ=nQ,aQ=An,oQ=iQ,sQ=En,lQ="[object Object]",uQ=Function.prototype,cQ=Object.prototype,uT=uQ.toString,fQ=cQ.hasOwnProperty,dQ=uT.call(Object);function hQ(e){if(!sQ(e)||aQ(e)!=lQ)return!1;var t=oQ(e);if(t===null)return!0;var r=fQ.call(t,"constructor")&&t.constructor;return typeof r=="function"&&r instanceof r&&uT.call(r)==dQ}var pQ=hQ;const mQ=Se(pQ);var yQ=An,vQ=En,gQ="[object Boolean]";function bQ(e){return e===!0||e===!1||vQ(e)&&yQ(e)==gQ}var xQ=bQ;const wQ=Se(xQ);function Vl(e){"@babel/helpers - typeof";return Vl=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Vl(e)}function Uf(){return Uf=Object.assign?Object.assign.bind():function(e){for(var t=1;te.length)&&(t=e.length);for(var r=0,n=new Array(t);r0,from:{upperWidth:0,lowerWidth:0,height:d,x:l,y:u},to:{upperWidth:f,lowerWidth:c,height:d,x:l,y:u},duration:m,animationEasing:p,isActive:v},function(b){var w=b.upperWidth,x=b.lowerWidth,S=b.height,_=b.x,P=b.y;return T.createElement(On,{canBegin:o>0,from:"0px ".concat(o===-1?1:o,"px"),to:"".concat(o,"px 0px"),attributeName:"strokeDasharray",begin:y,duration:m,easing:p},T.createElement("path",Uf({},te(r,!0),{className:g,d:SS(_,P,w,x,S),ref:n})))}):T.createElement("g",null,T.createElement("path",Uf({},te(r,!0),{className:g,d:SS(l,u,f,c,d)})))},kQ=["option","shapeType","propTransformer","activeClassName","isActive"];function Gl(e){"@babel/helpers - typeof";return Gl=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Gl(e)}function NQ(e,t){if(e==null)return{};var r=MQ(e,t),n,i;if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(i=0;i=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function MQ(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}function OS(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function zf(e){for(var t=1;t0?Jt(b,"paddingAngle",0):0;if(x){var _=In(x.endAngle-x.startAngle,b.endAngle-b.startAngle),P=Pe(Pe({},b),{},{startAngle:g+S,endAngle:g+_(m)+S});y.push(P),g=P.endAngle}else{var A=b.endAngle,C=b.startAngle,N=In(0,A-C),$=N(m),L=Pe(Pe({},b),{},{startAngle:g+S,endAngle:g+$+S});y.push(L),g=L.endAngle}}),T.createElement(ge,null,n.renderSectorsStatically(y))})}},{key:"attachKeyboardHandlers",value:function(n){var i=this;n.onkeydown=function(a){if(!a.altKey)switch(a.key){case"ArrowLeft":{var o=++i.state.sectorToFocus%i.sectorRefs.length;i.sectorRefs[o].focus(),i.setState({sectorToFocus:o});break}case"ArrowRight":{var s=--i.state.sectorToFocus<0?i.sectorRefs.length-1:i.state.sectorToFocus%i.sectorRefs.length;i.sectorRefs[s].focus(),i.setState({sectorToFocus:s});break}case"Escape":{i.sectorRefs[i.state.sectorToFocus].blur(),i.setState({sectorToFocus:0});break}}}}},{key:"renderSectors",value:function(){var n=this.props,i=n.sectors,a=n.isAnimationActive,o=this.state.prevSectors;return a&&i&&i.length&&(!o||!rh(o,i))?this.renderSectorsWithAnimation():this.renderSectorsStatically(i)}},{key:"componentDidMount",value:function(){this.pieRef&&this.attachKeyboardHandlers(this.pieRef)}},{key:"render",value:function(){var n=this,i=this.props,a=i.hide,o=i.sectors,s=i.className,l=i.label,u=i.cx,f=i.cy,c=i.innerRadius,d=i.outerRadius,h=i.isAnimationActive,p=this.state.isAnimationFinished;if(a||!o||!o.length||!V(u)||!V(f)||!V(c)||!V(d))return null;var m=oe("recharts-pie",s);return T.createElement(ge,{tabIndex:this.props.rootTabIndex,className:m,ref:function(v){n.pieRef=v}},this.renderSectors(),l&&this.renderLabels(o),lt.renderCallByParent(this.props,null,!1),(!h||p)&&ui.renderCallByParent(this.props,o,!1))}}],[{key:"getDerivedStateFromProps",value:function(n,i){return i.prevIsAnimationActive!==n.isAnimationActive?{prevIsAnimationActive:n.isAnimationActive,prevAnimationId:n.animationId,curSectors:n.sectors,prevSectors:[],isAnimationFinished:!0}:n.isAnimationActive&&n.animationId!==i.prevAnimationId?{prevAnimationId:n.animationId,curSectors:n.sectors,prevSectors:i.curSectors,isAnimationFinished:!0}:n.sectors!==i.curSectors?{curSectors:n.sectors,isAnimationFinished:!0}:null}},{key:"getTextAnchor",value:function(n,i){return n>i?"start":n=360?g:g-1)*l,w=y-g*h-b,x=i.reduce(function(P,A){var C=xt(A,v,0);return P+(V(C)?C:0)},0),S;if(x>0){var _;S=i.map(function(P,A){var C=xt(P,v,0),N=xt(P,f,A),$=(V(C)?C:0)/x,L;A?L=_.endAngle+At(m)*l*(C!==0?1:0):L=o;var I=L+At(m)*((C!==0?h:0)+$*w),R=(L+I)/2,B=(p.innerRadius+p.outerRadius)/2,z=[{name:N,value:C,payload:P,dataKey:v,type:d}],k=je(p.cx,p.cy,B,R);return _=Pe(Pe(Pe({percent:$,cornerRadius:a,name:N,tooltipPayload:z,midAngle:R,middleRadius:B,tooltipPosition:k},P),p),{},{value:xt(P,v),startAngle:L,endAngle:I,payload:P,paddingAngle:At(m)*l}),_})}return Pe(Pe({},p),{},{sectors:S,data:i})});var tY=Math.ceil,rY=Math.max;function nY(e,t,r,n){for(var i=-1,a=rY(tY((t-e)/(r||1)),0),o=Array(a);a--;)o[n?a:++i]=e,e+=r;return o}var iY=nY,aY=NE,ES=1/0,oY=17976931348623157e292;function sY(e){if(!e)return e===0?e:0;if(e=aY(e),e===ES||e===-ES){var t=e<0?-1:1;return t*oY}return e===e?e:0}var lY=sY,uY=iY,cY=qd,_p=lY;function fY(e){return function(t,r,n){return n&&typeof n!="number"&&cY(t,r,n)&&(r=n=void 0),t=_p(t),r===void 0?(r=t,t=0):r=_p(r),n=n===void 0?t0&&n.handleDrag(i.changedTouches[0])}),Kt(n,"handleDragEnd",function(){n.setState({isTravellerMoving:!1,isSlideMoving:!1},function(){var i=n.props,a=i.endIndex,o=i.onDragEnd,s=i.startIndex;o==null||o({endIndex:a,startIndex:s})}),n.detachDragEndListener()}),Kt(n,"handleLeaveWrapper",function(){(n.state.isTravellerMoving||n.state.isSlideMoving)&&(n.leaveTimer=window.setTimeout(n.handleDragEnd,n.props.leaveTimeOut))}),Kt(n,"handleEnterSlideOrTraveller",function(){n.setState({isTextActive:!0})}),Kt(n,"handleLeaveSlideOrTraveller",function(){n.setState({isTextActive:!1})}),Kt(n,"handleSlideDragStart",function(i){var a=kS(i)?i.changedTouches[0]:i;n.setState({isTravellerMoving:!1,isSlideMoving:!0,slideMoveStartX:a.pageX}),n.attachDragEndListener()}),n.travellerDragStartHandlers={startX:n.handleTravellerDragStart.bind(n,"startX"),endX:n.handleTravellerDragStart.bind(n,"endX")},n.state={},n}return PY(t,e),wY(t,[{key:"componentWillUnmount",value:function(){this.leaveTimer&&(clearTimeout(this.leaveTimer),this.leaveTimer=null),this.detachDragEndListener()}},{key:"getIndex",value:function(n){var i=n.startX,a=n.endX,o=this.state.scaleValues,s=this.props,l=s.gap,u=s.data,f=u.length-1,c=Math.min(i,a),d=Math.max(i,a),h=t.getIndexInRange(o,c),p=t.getIndexInRange(o,d);return{startIndex:h-h%l,endIndex:p===f?f:p-p%l}}},{key:"getTextOfTick",value:function(n){var i=this.props,a=i.data,o=i.tickFormatter,s=i.dataKey,l=xt(a[n],s,n);return re(o)?o(l,n):l}},{key:"attachDragEndListener",value:function(){window.addEventListener("mouseup",this.handleDragEnd,!0),window.addEventListener("touchend",this.handleDragEnd,!0),window.addEventListener("mousemove",this.handleDrag,!0)}},{key:"detachDragEndListener",value:function(){window.removeEventListener("mouseup",this.handleDragEnd,!0),window.removeEventListener("touchend",this.handleDragEnd,!0),window.removeEventListener("mousemove",this.handleDrag,!0)}},{key:"handleSlideDrag",value:function(n){var i=this.state,a=i.slideMoveStartX,o=i.startX,s=i.endX,l=this.props,u=l.x,f=l.width,c=l.travellerWidth,d=l.startIndex,h=l.endIndex,p=l.onChange,m=n.pageX-a;m>0?m=Math.min(m,u+f-c-s,u+f-c-o):m<0&&(m=Math.max(m,u-o,u-s));var y=this.getIndex({startX:o+m,endX:s+m});(y.startIndex!==d||y.endIndex!==h)&&p&&p(y),this.setState({startX:o+m,endX:s+m,slideMoveStartX:n.pageX})}},{key:"handleTravellerDragStart",value:function(n,i){var a=kS(i)?i.changedTouches[0]:i;this.setState({isSlideMoving:!1,isTravellerMoving:!0,movingTravellerId:n,brushMoveStartX:a.pageX}),this.attachDragEndListener()}},{key:"handleTravellerMove",value:function(n){var i=this.state,a=i.brushMoveStartX,o=i.movingTravellerId,s=i.endX,l=i.startX,u=this.state[o],f=this.props,c=f.x,d=f.width,h=f.travellerWidth,p=f.onChange,m=f.gap,y=f.data,v={startX:this.state.startX,endX:this.state.endX},g=n.pageX-a;g>0?g=Math.min(g,c+d-h-u):g<0&&(g=Math.max(g,c-u)),v[o]=u+g;var b=this.getIndex(v),w=b.startIndex,x=b.endIndex,S=function(){var P=y.length-1;return o==="startX"&&(s>l?w%m===0:x%m===0)||sl?x%m===0:w%m===0)||s>l&&x===P};this.setState(Kt(Kt({},o,u+g),"brushMoveStartX",n.pageX),function(){p&&S()&&p(b)})}},{key:"handleTravellerMoveKeyboard",value:function(n,i){var a=this,o=this.state,s=o.scaleValues,l=o.startX,u=o.endX,f=this.state[i],c=s.indexOf(f);if(c!==-1){var d=c+n;if(!(d===-1||d>=s.length)){var h=s[d];i==="startX"&&h>=u||i==="endX"&&h<=l||this.setState(Kt({},i,h),function(){a.props.onChange(a.getIndex({startX:a.state.startX,endX:a.state.endX}))})}}}},{key:"renderBackground",value:function(){var n=this.props,i=n.x,a=n.y,o=n.width,s=n.height,l=n.fill,u=n.stroke;return T.createElement("rect",{stroke:u,fill:l,x:i,y:a,width:o,height:s})}},{key:"renderPanorama",value:function(){var n=this.props,i=n.x,a=n.y,o=n.width,s=n.height,l=n.data,u=n.children,f=n.padding,c=j.Children.only(u);return c?T.cloneElement(c,{x:i,y:a,width:o,height:s,margin:f,compact:!0,data:l}):null}},{key:"renderTravellerLayer",value:function(n,i){var a,o,s=this,l=this.props,u=l.y,f=l.travellerWidth,c=l.height,d=l.traveller,h=l.ariaLabel,p=l.data,m=l.startIndex,y=l.endIndex,v=Math.max(n,this.props.x),g=Pp(Pp({},te(this.props,!1)),{},{x:v,y:u,width:f,height:c}),b=h||"Min value: ".concat((a=p[m])===null||a===void 0?void 0:a.name,", Max value: ").concat((o=p[y])===null||o===void 0?void 0:o.name);return T.createElement(ge,{tabIndex:0,role:"slider","aria-label":b,"aria-valuenow":n,className:"recharts-brush-traveller",onMouseEnter:this.handleEnterSlideOrTraveller,onMouseLeave:this.handleLeaveSlideOrTraveller,onMouseDown:this.travellerDragStartHandlers[i],onTouchStart:this.travellerDragStartHandlers[i],onKeyDown:function(x){["ArrowLeft","ArrowRight"].includes(x.key)&&(x.preventDefault(),x.stopPropagation(),s.handleTravellerMoveKeyboard(x.key==="ArrowRight"?1:-1,i))},onFocus:function(){s.setState({isTravellerFocused:!0})},onBlur:function(){s.setState({isTravellerFocused:!1})},style:{cursor:"col-resize"}},t.renderTraveller(d,g))}},{key:"renderSlide",value:function(n,i){var a=this.props,o=a.y,s=a.height,l=a.stroke,u=a.travellerWidth,f=Math.min(n,i)+u,c=Math.max(Math.abs(i-n)-u,0);return T.createElement("rect",{className:"recharts-brush-slide",onMouseEnter:this.handleEnterSlideOrTraveller,onMouseLeave:this.handleLeaveSlideOrTraveller,onMouseDown:this.handleSlideDragStart,onTouchStart:this.handleSlideDragStart,style:{cursor:"move"},stroke:"none",fill:l,fillOpacity:.2,x:f,y:o,width:c,height:s})}},{key:"renderText",value:function(){var n=this.props,i=n.startIndex,a=n.endIndex,o=n.y,s=n.height,l=n.travellerWidth,u=n.stroke,f=this.state,c=f.startX,d=f.endX,h=5,p={pointerEvents:"none",fill:u};return T.createElement(ge,{className:"recharts-brush-texts"},T.createElement(da,qf({textAnchor:"end",verticalAnchor:"middle",x:Math.min(c,d)-h,y:o+s/2},p),this.getTextOfTick(i)),T.createElement(da,qf({textAnchor:"start",verticalAnchor:"middle",x:Math.max(c,d)+l+h,y:o+s/2},p),this.getTextOfTick(a)))}},{key:"render",value:function(){var n=this.props,i=n.data,a=n.className,o=n.children,s=n.x,l=n.y,u=n.width,f=n.height,c=n.alwaysShowText,d=this.state,h=d.startX,p=d.endX,m=d.isTextActive,y=d.isSlideMoving,v=d.isTravellerMoving,g=d.isTravellerFocused;if(!i||!i.length||!V(s)||!V(l)||!V(u)||!V(f)||u<=0||f<=0)return null;var b=oe("recharts-brush",a),w=T.Children.count(o)===1,x=bY("userSelect","none");return T.createElement(ge,{className:b,onMouseLeave:this.handleLeaveWrapper,onTouchMove:this.handleTouchMove,style:x},this.renderBackground(),w&&this.renderPanorama(),this.renderSlide(h,p),this.renderTravellerLayer(h,"startX"),this.renderTravellerLayer(p,"endX"),(m||y||v||g||c)&&this.renderText())}}],[{key:"renderDefaultTraveller",value:function(n){var i=n.x,a=n.y,o=n.width,s=n.height,l=n.stroke,u=Math.floor(a+s/2)-1;return T.createElement(T.Fragment,null,T.createElement("rect",{x:i,y:a,width:o,height:s,fill:l,stroke:"none"}),T.createElement("line",{x1:i+1,y1:u,x2:i+o-1,y2:u,fill:"none",stroke:"#fff"}),T.createElement("line",{x1:i+1,y1:u+2,x2:i+o-1,y2:u+2,fill:"none",stroke:"#fff"}))}},{key:"renderTraveller",value:function(n,i){var a;return T.isValidElement(n)?a=T.cloneElement(n,i):re(n)?a=n(i):a=t.renderDefaultTraveller(i),a}},{key:"getDerivedStateFromProps",value:function(n,i){var a=n.data,o=n.width,s=n.x,l=n.travellerWidth,u=n.updateId,f=n.startIndex,c=n.endIndex;if(a!==i.prevData||u!==i.prevUpdateId)return Pp({prevData:a,prevTravellerWidth:l,prevUpdateId:u,prevX:s,prevWidth:o},a&&a.length?EY({data:a,width:o,x:s,travellerWidth:l,startIndex:f,endIndex:c}):{scale:null,scaleValues:null});if(i.scale&&(o!==i.prevWidth||s!==i.prevX||l!==i.prevTravellerWidth)){i.scale.range([s,s+o-l]);var d=i.scale.domain().map(function(h){return i.scale(h)});return{prevData:a,prevTravellerWidth:l,prevUpdateId:u,prevX:s,prevWidth:o,startX:i.scale(n.startIndex),endX:i.scale(n.endIndex),scaleValues:d}}return null}},{key:"getIndexInRange",value:function(n,i){for(var a=n.length,o=0,s=a-1;s-o>1;){var l=Math.floor((o+s)/2);n[l]>i?s=l:o=l}return i>=n[s]?s:o}}])}(j.PureComponent);Kt(Do,"displayName","Brush");Kt(Do,"defaultProps",{height:40,travellerWidth:5,gap:1,fill:"#fff",stroke:"#666",padding:{top:1,right:1,bottom:1,left:1},leaveTimeOut:1e3,alwaysShowText:!1});var jY=Xg;function TY(e,t){var r;return jY(e,function(n,i,a){return r=t(n,i,a),!r}),!!r}var CY=TY,$Y=sE,kY=vi,NY=CY,MY=qt,IY=qd;function RY(e,t,r){var n=MY(e)?$Y:NY;return r&&IY(e,t,r)&&(t=void 0),n(e,kY(t))}var DY=RY;const LY=Se(DY);var Vr=function(t,r){var n=t.alwaysShow,i=t.ifOverflow;return n&&(i="extendDomain"),i===r},NS=jE;function BY(e,t,r){t=="__proto__"&&NS?NS(e,t,{configurable:!0,enumerable:!0,value:r,writable:!0}):e[t]=r}var FY=BY,UY=FY,zY=AE,WY=vi;function HY(e,t){var r={};return t=WY(t),zY(e,function(n,i,a){UY(r,i,t(n,i,a))}),r}var qY=HY;const KY=Se(qY);function VY(e,t){for(var r=-1,n=e==null?0:e.length;++r=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function fJ(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}function dJ(e,t){var r=e.x,n=e.y,i=cJ(e,oJ),a="".concat(r),o=parseInt(a,10),s="".concat(n),l=parseInt(s,10),u="".concat(t.height||i.height),f=parseInt(u,10),c="".concat(t.width||i.width),d=parseInt(c,10);return Cs(Cs(Cs(Cs(Cs({},t),i),o?{x:o}:{}),l?{y:l}:{}),{},{height:f,width:d,name:t.name,radius:t.radius})}function IS(e){return T.createElement(cT,Zy({shapeType:"rectangle",propTransformer:dJ,activeClassName:"recharts-active-bar"},e))}var hJ=function(t){var r=arguments.length>1&&arguments[1]!==void 0?arguments[1]:0;return function(n,i){if(typeof t=="number")return t;var a=V(n)||kD(n);return a?t(n,i):(a||pa(),r)}},pJ=["value","background"],yT;function Lo(e){"@babel/helpers - typeof";return Lo=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Lo(e)}function mJ(e,t){if(e==null)return{};var r=yJ(e,t),n,i;if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(i=0;i=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function yJ(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}function Vf(){return Vf=Object.assign?Object.assign.bind():function(e){for(var t=1;t0&&Math.abs(R)0&&Math.abs(I)0&&(L=Math.min((le||0)-(I[Oe-1]||0),L))}),Number.isFinite(L)){var R=L/$,B=m.layout==="vertical"?n.height:n.width;if(m.padding==="gap"&&(_=R*B/2),m.padding==="no-gap"){var z=Et(t.barCategoryGap,R*B),k=R*B/2;_=k-z-(k-z)/B*z}}}i==="xAxis"?P=[n.left+(b.left||0)+(_||0),n.left+n.width-(b.right||0)-(_||0)]:i==="yAxis"?P=l==="horizontal"?[n.top+n.height-(b.bottom||0),n.top+(b.top||0)]:[n.top+(b.top||0)+(_||0),n.top+n.height-(b.bottom||0)-(_||0)]:P=m.range,x&&(P=[P[1],P[0]]);var F=Lj(m,a,d),U=F.scale,K=F.realScaleType;U.domain(v).range(P),Bj(U);var H=Fj(U,Er(Er({},m),{},{realScaleType:K}));i==="xAxis"?(N=y==="top"&&!w||y==="bottom"&&w,A=n.left,C=c[S]-N*m.height):i==="yAxis"&&(N=y==="left"&&!w||y==="right"&&w,A=c[S]-N*m.width,C=n.top);var J=Er(Er(Er({},m),H),{},{realScaleType:K,x:A,y:C,scale:U,width:i==="xAxis"?n.width:m.width,height:i==="yAxis"?n.height:m.height});return J.bandSize=$f(J,H),!m.hide&&i==="xAxis"?c[S]+=(N?-1:1)*J.height:m.hide||(c[S]+=(N?-1:1)*J.width),Er(Er({},h),{},dh({},p,J))},{})},xT=function(t,r){var n=t.x,i=t.y,a=r.x,o=r.y;return{x:Math.min(n,a),y:Math.min(i,o),width:Math.abs(a-n),height:Math.abs(o-i)}},jJ=function(t){var r=t.x1,n=t.y1,i=t.x2,a=t.y2;return xT({x:r,y:n},{x:i,y:a})},wT=function(){function e(t){_J(this,e),this.scale=t}return PJ(e,[{key:"domain",get:function(){return this.scale.domain}},{key:"range",get:function(){return this.scale.range}},{key:"rangeMin",get:function(){return this.range()[0]}},{key:"rangeMax",get:function(){return this.range()[1]}},{key:"bandwidth",get:function(){return this.scale.bandwidth}},{key:"apply",value:function(r){var n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},i=n.bandAware,a=n.position;if(r!==void 0){if(a)switch(a){case"start":return this.scale(r);case"middle":{var o=this.bandwidth?this.bandwidth()/2:0;return this.scale(r)+o}case"end":{var s=this.bandwidth?this.bandwidth():0;return this.scale(r)+s}default:return this.scale(r)}if(i){var l=this.bandwidth?this.bandwidth()/2:0;return this.scale(r)+l}return this.scale(r)}}},{key:"isInRange",value:function(r){var n=this.range(),i=n[0],a=n[n.length-1];return i<=a?r>=i&&r<=a:r>=a&&r<=i}}],[{key:"create",value:function(r){return new e(r)}}])}();dh(wT,"EPS",1e-4);var P0=function(t){var r=Object.keys(t).reduce(function(n,i){return Er(Er({},n),{},dh({},i,wT.create(t[i])))},{});return Er(Er({},r),{},{apply:function(i){var a=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},o=a.bandAware,s=a.position;return KY(i,function(l,u){return r[u].apply(l,{bandAware:o,position:s})})},isInRange:function(i){return aJ(i,function(a,o){return r[o].isInRange(a)})}})};function TJ(e){return(e%180+180)%180}var CJ=function(t){var r=t.width,n=t.height,i=arguments.length>1&&arguments[1]!==void 0?arguments[1]:0,a=TJ(i),o=a*Math.PI/180,s=Math.atan(n/r),l=o>s&&oe.length)&&(t=e.length);for(var r=0,n=new Array(t);re*i)return!1;var a=r();return e*(t-e*a/2-n)>=0&&e*(t+e*a/2-i)<=0}function vZ(e,t){return LT(e,t+1)}function gZ(e,t,r,n,i){for(var a=(n||[]).slice(),o=t.start,s=t.end,l=0,u=1,f=o,c=function(){var p=n==null?void 0:n[l];if(p===void 0)return{v:LT(n,u)};var m=l,y,v=function(){return y===void 0&&(y=r(p,m)),y},g=p.coordinate,b=l===0||Jf(e,g,v,f,s);b||(l=0,f=o,u+=1),b&&(f=g+e*(v()/2+i),l+=u)},d;u<=a.length;)if(d=c(),d)return d.v;return[]}function Zl(e){"@babel/helpers - typeof";return Zl=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Zl(e)}function GS(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function gt(e){for(var t=1;t0?h.coordinate-y*e:h.coordinate})}else a[d]=h=gt(gt({},h),{},{tickCoord:h.coordinate});var v=Jf(e,h.tickCoord,m,s,l);v&&(l=h.tickCoord-e*(m()/2+i),a[d]=gt(gt({},h),{},{isShow:!0}))},f=o-1;f>=0;f--)u(f);return a}function OZ(e,t,r,n,i,a){var o=(n||[]).slice(),s=o.length,l=t.start,u=t.end;if(a){var f=n[s-1],c=r(f,s-1),d=e*(f.coordinate+e*c/2-u);o[s-1]=f=gt(gt({},f),{},{tickCoord:d>0?f.coordinate-d*e:f.coordinate});var h=Jf(e,f.tickCoord,function(){return c},l,u);h&&(u=f.tickCoord-e*(c/2+i),o[s-1]=gt(gt({},f),{},{isShow:!0}))}for(var p=a?s-1:s,m=function(g){var b=o[g],w,x=function(){return w===void 0&&(w=r(b,g)),w};if(g===0){var S=e*(b.coordinate-e*x()/2-l);o[g]=b=gt(gt({},b),{},{tickCoord:S<0?b.coordinate-S*e:b.coordinate})}else o[g]=b=gt(gt({},b),{},{tickCoord:b.coordinate});var _=Jf(e,b.tickCoord,x,l,u);_&&(l=b.tickCoord+e*(x()/2+i),o[g]=gt(gt({},b),{},{isShow:!0}))},y=0;y=2?At(i[1].coordinate-i[0].coordinate):1,v=yZ(a,y,h);return l==="equidistantPreserveStart"?gZ(y,v,m,i,o):(l==="preserveStart"||l==="preserveStartEnd"?d=OZ(y,v,m,i,o,l==="preserveStartEnd"):d=SZ(y,v,m,i,o),d.filter(function(g){return g.isShow}))}var PZ=["viewBox"],AZ=["viewBox"],EZ=["ticks"];function zo(e){"@babel/helpers - typeof";return zo=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},zo(e)}function Wa(){return Wa=Object.assign?Object.assign.bind():function(e){for(var t=1;t=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function jZ(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}function TZ(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function QS(e,t){for(var r=0;r0?l(this.props):l(h)),o<=0||s<=0||!p||!p.length?null:T.createElement(ge,{className:oe("recharts-cartesian-axis",u),ref:function(y){n.layerReference=y}},a&&this.renderAxisLine(),this.renderTicks(p,this.state.fontSize,this.state.letterSpacing),lt.renderCallByParent(this.props))}}],[{key:"renderTickItem",value:function(n,i,a){var o,s=oe(i.className,"recharts-cartesian-axis-tick-value");return T.isValidElement(n)?o=T.cloneElement(n,Je(Je({},i),{},{className:s})):re(n)?o=n(Je(Je({},i),{},{className:s})):o=T.createElement(da,Wa({},i,{className:"recharts-cartesian-axis-tick-value"}),a),o}}])}(j.Component);j0(vh,"displayName","CartesianAxis");j0(vh,"defaultProps",{x:0,y:0,width:0,height:0,viewBox:{x:0,y:0,width:0,height:0},orientation:"bottom",ticks:[],stroke:"#666",tickLine:!0,axisLine:!0,tick:!0,mirror:!1,minTickGap:5,tickSize:6,tickMargin:2,interval:"preserveEnd"});function Wo(e){"@babel/helpers - typeof";return Wo=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Wo(e)}function RZ(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function DZ(e,t){for(var r=0;re.length)&&(t=e.length);for(var r=0,n=new Array(t);r=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function Oee(e,t){if(e==null)return{};var r={};for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){if(t.indexOf(n)>=0)continue;r[n]=e[n]}return r}function _ee(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function Pee(e,t){for(var r=0;re.length)&&(t=e.length);for(var r=0,n=new Array(t);r0?o:t&&t.length&&V(i)&&V(a)?t.slice(i,a+1):[]};function e2(e){return e==="number"?[0,"auto"]:void 0}var yv=function(t,r,n,i){var a=t.graphicalItems,o=t.tooltipAxis,s=xh(r,t);return n<0||!a||!a.length||n>=s.length?null:a.reduce(function(l,u){var f,c=(f=u.props.data)!==null&&f!==void 0?f:r;c&&t.dataStartIndex+t.dataEndIndex!==0&&t.dataEndIndex-t.dataStartIndex>=n&&(c=c.slice(t.dataStartIndex,t.dataEndIndex+1));var d;if(o.dataKey&&!o.allowDuplicatedCategory){var h=c===void 0?s:c;d=Hm(h,o.dataKey,i)}else d=c&&c[n]||s[n];return d?[].concat(Ko(l),[zj(u,d)]):l},[])},rO=function(t,r,n,i){var a=i||{x:t.chartX,y:t.chartY},o=Dee(a,n),s=t.orderedTooltipTicks,l=t.tooltipAxis,u=t.tooltipTicks,f=qq(o,s,u,l);if(f>=0&&u){var c=u[f]&&u[f].value,d=yv(t,r,f,c),h=Lee(n,s,f,a);return{activeTooltipIndex:f,activeLabel:c,activePayload:d,activeCoordinate:h}}return null},Bee=function(t,r){var n=r.axes,i=r.graphicalItems,a=r.axisType,o=r.axisIdKey,s=r.stackGroups,l=r.dataStartIndex,u=r.dataEndIndex,f=t.layout,c=t.children,d=t.stackOffset,h=Dj(f,a);return n.reduce(function(p,m){var y,v=m.type.defaultProps!==void 0?D(D({},m.type.defaultProps),m.props):m.props,g=v.type,b=v.dataKey,w=v.allowDataOverflow,x=v.allowDuplicatedCategory,S=v.scale,_=v.ticks,P=v.includeHidden,A=v[o];if(p[A])return p;var C=xh(t.data,{graphicalItems:i.filter(function(H){var J,le=o in H.props?H.props[o]:(J=H.type.defaultProps)===null||J===void 0?void 0:J[o];return le===A}),dataStartIndex:l,dataEndIndex:u}),N=C.length,$,L,I;fee(v.domain,w,g)&&($=$y(v.domain,null,w),h&&(g==="number"||S!=="auto")&&(I=Qs(C,b,"category")));var R=e2(g);if(!$||$.length===0){var B,z=(B=v.domain)!==null&&B!==void 0?B:R;if(b){if($=Qs(C,b,g),g==="category"&&h){var k=MD($);x&&k?(L=$,$=Hf(0,N)):x||($=kw(z,$,m).reduce(function(H,J){return H.indexOf(J)>=0?H:[].concat(Ko(H),[J])},[]))}else if(g==="category")x?$=$.filter(function(H){return H!==""&&!ue(H)}):$=kw(z,$,m).reduce(function(H,J){return H.indexOf(J)>=0||J===""||ue(J)?H:[].concat(Ko(H),[J])},[]);else if(g==="number"){var F=Qq(C,i.filter(function(H){var J,le,Oe=o in H.props?H.props[o]:(J=H.type.defaultProps)===null||J===void 0?void 0:J[o],He="hide"in H.props?H.props.hide:(le=H.type.defaultProps)===null||le===void 0?void 0:le.hide;return Oe===A&&(P||!He)}),b,a,f);F&&($=F)}h&&(g==="number"||S!=="auto")&&(I=Qs(C,b,"category"))}else h?$=Hf(0,N):s&&s[A]&&s[A].hasStack&&g==="number"?$=d==="expand"?[0,1]:Uj(s[A].stackGroups,l,u):$=Rj(C,i.filter(function(H){var J=o in H.props?H.props[o]:H.type.defaultProps[o],le="hide"in H.props?H.props.hide:H.type.defaultProps.hide;return J===A&&(P||!le)}),g,f,!0);if(g==="number")$=hv(c,$,A,a,_),z&&($=$y(z,$,w));else if(g==="category"&&z){var U=z,K=$.every(function(H){return U.indexOf(H)>=0});K&&($=U)}}return D(D({},p),{},ee({},A,D(D({},v),{},{axisType:a,domain:$,categoricalDomain:I,duplicateDomain:L,originalDomain:(y=v.domain)!==null&&y!==void 0?y:R,isCategorical:h,layout:f})))},{})},Fee=function(t,r){var n=r.graphicalItems,i=r.Axis,a=r.axisType,o=r.axisIdKey,s=r.stackGroups,l=r.dataStartIndex,u=r.dataEndIndex,f=t.layout,c=t.children,d=xh(t.data,{graphicalItems:n,dataStartIndex:l,dataEndIndex:u}),h=d.length,p=Dj(f,a),m=-1;return n.reduce(function(y,v){var g=v.type.defaultProps!==void 0?D(D({},v.type.defaultProps),v.props):v.props,b=g[o],w=e2("number");if(!y[b]){m++;var x;return p?x=Hf(0,h):s&&s[b]&&s[b].hasStack?(x=Uj(s[b].stackGroups,l,u),x=hv(c,x,b,a)):(x=$y(w,Rj(d,n.filter(function(S){var _,P,A=o in S.props?S.props[o]:(_=S.type.defaultProps)===null||_===void 0?void 0:_[o],C="hide"in S.props?S.props.hide:(P=S.type.defaultProps)===null||P===void 0?void 0:P.hide;return A===b&&!C}),"number",f),i.defaultProps.allowDataOverflow),x=hv(c,x,b,a)),D(D({},y),{},ee({},b,D(D({axisType:a},i.defaultProps),{},{hide:!0,orientation:Jt(Iee,"".concat(a,".").concat(m%2),null),domain:x,originalDomain:w,isCategorical:p,layout:f})))}return y},{})},Uee=function(t,r){var n=r.axisType,i=n===void 0?"xAxis":n,a=r.AxisComp,o=r.graphicalItems,s=r.stackGroups,l=r.dataStartIndex,u=r.dataEndIndex,f=t.children,c="".concat(i,"Id"),d=pr(f,a),h={};return d&&d.length?h=Bee(t,{axes:d,graphicalItems:o,axisType:i,axisIdKey:c,stackGroups:s,dataStartIndex:l,dataEndIndex:u}):o&&o.length&&(h=Fee(t,{Axis:a,graphicalItems:o,axisType:i,axisIdKey:c,stackGroups:s,dataStartIndex:l,dataEndIndex:u})),h},zee=function(t){var r=Ea(t),n=Ui(r,!1,!0);return{tooltipTicks:n,orderedTooltipTicks:Qg(n,function(i){return i.coordinate}),tooltipAxis:r,tooltipAxisBandSize:$f(r,n)}},nO=function(t){var r=t.children,n=t.defaultShowTooltip,i=Gt(r,Do),a=0,o=0;return t.data&&t.data.length!==0&&(o=t.data.length-1),i&&i.props&&(i.props.startIndex>=0&&(a=i.props.startIndex),i.props.endIndex>=0&&(o=i.props.endIndex)),{chartX:0,chartY:0,dataStartIndex:a,dataEndIndex:o,activeTooltipIndex:-1,isTooltipActive:!!n}},Wee=function(t){return!t||!t.length?!1:t.some(function(r){var n=fn(r&&r.type);return n&&n.indexOf("Bar")>=0})},iO=function(t){return t==="horizontal"?{numericAxisName:"yAxis",cateAxisName:"xAxis"}:t==="vertical"?{numericAxisName:"xAxis",cateAxisName:"yAxis"}:t==="centric"?{numericAxisName:"radiusAxis",cateAxisName:"angleAxis"}:{numericAxisName:"angleAxis",cateAxisName:"radiusAxis"}},Hee=function(t,r){var n=t.props,i=t.graphicalItems,a=t.xAxisMap,o=a===void 0?{}:a,s=t.yAxisMap,l=s===void 0?{}:s,u=n.width,f=n.height,c=n.children,d=n.margin||{},h=Gt(c,Do),p=Gt(c,Ja),m=Object.keys(l).reduce(function(x,S){var _=l[S],P=_.orientation;return!_.mirror&&!_.hide?D(D({},x),{},ee({},P,x[P]+_.width)):x},{left:d.left||0,right:d.right||0}),y=Object.keys(o).reduce(function(x,S){var _=o[S],P=_.orientation;return!_.mirror&&!_.hide?D(D({},x),{},ee({},P,Jt(x,"".concat(P))+_.height)):x},{top:d.top||0,bottom:d.bottom||0}),v=D(D({},y),m),g=v.bottom;h&&(v.bottom+=h.props.height||Do.defaultProps.height),p&&r&&(v=Gq(v,i,n,r));var b=u-v.left-v.right,w=f-v.top-v.bottom;return D(D({brushBottom:g},v),{},{width:Math.max(b,0),height:Math.max(w,0)})},qee=function(t,r){if(r==="xAxis")return t[r].width;if(r==="yAxis")return t[r].height},t2=function(t){var r=t.chartName,n=t.GraphicalChild,i=t.defaultTooltipEventType,a=i===void 0?"axis":i,o=t.validateTooltipEventTypes,s=o===void 0?["axis"]:o,l=t.axisComponents,u=t.legendContent,f=t.formatAxisMap,c=t.defaultProps,d=function(v,g){var b=g.graphicalItems,w=g.stackGroups,x=g.offset,S=g.updateId,_=g.dataStartIndex,P=g.dataEndIndex,A=v.barSize,C=v.layout,N=v.barGap,$=v.barCategoryGap,L=v.maxBarSize,I=iO(C),R=I.numericAxisName,B=I.cateAxisName,z=Wee(b),k=[];return b.forEach(function(F,U){var K=xh(v.data,{graphicalItems:[F],dataStartIndex:_,dataEndIndex:P}),H=F.type.defaultProps!==void 0?D(D({},F.type.defaultProps),F.props):F.props,J=H.dataKey,le=H.maxBarSize,Oe=H["".concat(R,"Id")],He=H["".concat(B,"Id")],rr={},kt=l.reduce(function(wi,Si){var Ah=g["".concat(Si.axisType,"Map")],N0=H["".concat(Si.axisType,"Id")];Ah&&Ah[N0]||Si.axisType==="zAxis"||pa();var M0=Ah[N0];return D(D({},wi),{},ee(ee({},Si.axisType,M0),"".concat(Si.axisType,"Ticks"),Ui(M0)))},rr),X=kt[B],ie=kt["".concat(B,"Ticks")],se=w&&w[Oe]&&w[Oe].hasStack&&aK(F,w[Oe].stackGroups),W=fn(F.type).indexOf("Bar")>=0,Be=$f(X,ie),he=[],Qe=z&&Kq({barSize:A,stackGroups:w,totalSize:qee(kt,B)});if(W){var Ye,Nt,Cn=ue(le)?L:le,Oa=(Ye=(Nt=$f(X,ie,!0))!==null&&Nt!==void 0?Nt:Cn)!==null&&Ye!==void 0?Ye:0;he=Vq({barGap:N,barCategoryGap:$,bandSize:Oa!==Be?Oa:Be,sizeList:Qe[He],maxBarSize:Cn}),Oa!==Be&&(he=he.map(function(wi){return D(D({},wi),{},{position:D(D({},wi.position),{},{offset:wi.position.offset-Oa/2})})}))}var Pu=F&&F.type&&F.type.getComposedData;Pu&&k.push({props:D(D({},Pu(D(D({},kt),{},{displayedData:K,props:v,dataKey:J,item:F,bandSize:Be,barPosition:he,offset:x,stackedData:se,layout:C,dataStartIndex:_,dataEndIndex:P}))),{},ee(ee(ee({key:F.key||"item-".concat(U)},R,kt[R]),B,kt[B]),"animationId",S)),childIndex:KD(F,v.children),item:F})}),k},h=function(v,g){var b=v.props,w=v.dataStartIndex,x=v.dataEndIndex,S=v.updateId;if(!Px({props:b}))return null;var _=b.children,P=b.layout,A=b.stackOffset,C=b.data,N=b.reverseStackOrder,$=iO(P),L=$.numericAxisName,I=$.cateAxisName,R=pr(_,n),B=nK(C,R,"".concat(L,"Id"),"".concat(I,"Id"),A,N),z=l.reduce(function(H,J){var le="".concat(J.axisType,"Map");return D(D({},H),{},ee({},le,Uee(b,D(D({},J),{},{graphicalItems:R,stackGroups:J.axisType===L&&B,dataStartIndex:w,dataEndIndex:x}))))},{}),k=Hee(D(D({},z),{},{props:b,graphicalItems:R}),g==null?void 0:g.legendBBox);Object.keys(z).forEach(function(H){z[H]=f(b,z[H],k,H.replace("Map",""),r)});var F=z["".concat(I,"Map")],U=zee(F),K=d(b,D(D({},z),{},{dataStartIndex:w,dataEndIndex:x,updateId:S,graphicalItems:R,stackGroups:B,offset:k}));return D(D({formattedGraphicalItems:K,graphicalItems:R,offset:k,stackGroups:B},U),z)},p=function(y){function v(g){var b,w,x;return _ee(this,v),x=Eee(this,v,[g]),ee(x,"eventEmitterSymbol",Symbol("rechartsEventEmitter")),ee(x,"accessibilityManager",new cee),ee(x,"handleLegendBBoxUpdate",function(S){if(S){var _=x.state,P=_.dataStartIndex,A=_.dataEndIndex,C=_.updateId;x.setState(D({legendBBox:S},h({props:x.props,dataStartIndex:P,dataEndIndex:A,updateId:C},D(D({},x.state),{},{legendBBox:S}))))}}),ee(x,"handleReceiveSyncEvent",function(S,_,P){if(x.props.syncId===S){if(P===x.eventEmitterSymbol&&typeof x.props.syncMethod!="function")return;x.applySyncEvent(_)}}),ee(x,"handleBrushChange",function(S){var _=S.startIndex,P=S.endIndex;if(_!==x.state.dataStartIndex||P!==x.state.dataEndIndex){var A=x.state.updateId;x.setState(function(){return D({dataStartIndex:_,dataEndIndex:P},h({props:x.props,dataStartIndex:_,dataEndIndex:P,updateId:A},x.state))}),x.triggerSyncEvent({dataStartIndex:_,dataEndIndex:P})}}),ee(x,"handleMouseEnter",function(S){var _=x.getMouseInfo(S);if(_){var P=D(D({},_),{},{isTooltipActive:!0});x.setState(P),x.triggerSyncEvent(P);var A=x.props.onMouseEnter;re(A)&&A(P,S)}}),ee(x,"triggeredAfterMouseMove",function(S){var _=x.getMouseInfo(S),P=_?D(D({},_),{},{isTooltipActive:!0}):{isTooltipActive:!1};x.setState(P),x.triggerSyncEvent(P);var A=x.props.onMouseMove;re(A)&&A(P,S)}),ee(x,"handleItemMouseEnter",function(S){x.setState(function(){return{isTooltipActive:!0,activeItem:S,activePayload:S.tooltipPayload,activeCoordinate:S.tooltipPosition||{x:S.cx,y:S.cy}}})}),ee(x,"handleItemMouseLeave",function(){x.setState(function(){return{isTooltipActive:!1}})}),ee(x,"handleMouseMove",function(S){S.persist(),x.throttleTriggeredAfterMouseMove(S)}),ee(x,"handleMouseLeave",function(S){x.throttleTriggeredAfterMouseMove.cancel();var _={isTooltipActive:!1};x.setState(_),x.triggerSyncEvent(_);var P=x.props.onMouseLeave;re(P)&&P(_,S)}),ee(x,"handleOuterEvent",function(S){var _=qD(S),P=Jt(x.props,"".concat(_));if(_&&re(P)){var A,C;/.*touch.*/i.test(_)?C=x.getMouseInfo(S.changedTouches[0]):C=x.getMouseInfo(S),P((A=C)!==null&&A!==void 0?A:{},S)}}),ee(x,"handleClick",function(S){var _=x.getMouseInfo(S);if(_){var P=D(D({},_),{},{isTooltipActive:!0});x.setState(P),x.triggerSyncEvent(P);var A=x.props.onClick;re(A)&&A(P,S)}}),ee(x,"handleMouseDown",function(S){var _=x.props.onMouseDown;if(re(_)){var P=x.getMouseInfo(S);_(P,S)}}),ee(x,"handleMouseUp",function(S){var _=x.props.onMouseUp;if(re(_)){var P=x.getMouseInfo(S);_(P,S)}}),ee(x,"handleTouchMove",function(S){S.changedTouches!=null&&S.changedTouches.length>0&&x.throttleTriggeredAfterMouseMove(S.changedTouches[0])}),ee(x,"handleTouchStart",function(S){S.changedTouches!=null&&S.changedTouches.length>0&&x.handleMouseDown(S.changedTouches[0])}),ee(x,"handleTouchEnd",function(S){S.changedTouches!=null&&S.changedTouches.length>0&&x.handleMouseUp(S.changedTouches[0])}),ee(x,"handleDoubleClick",function(S){var _=x.props.onDoubleClick;if(re(_)){var P=x.getMouseInfo(S);_(P,S)}}),ee(x,"handleContextMenu",function(S){var _=x.props.onContextMenu;if(re(_)){var P=x.getMouseInfo(S);_(P,S)}}),ee(x,"triggerSyncEvent",function(S){x.props.syncId!==void 0&&Ep.emit(jp,x.props.syncId,S,x.eventEmitterSymbol)}),ee(x,"applySyncEvent",function(S){var _=x.props,P=_.layout,A=_.syncMethod,C=x.state.updateId,N=S.dataStartIndex,$=S.dataEndIndex;if(S.dataStartIndex!==void 0||S.dataEndIndex!==void 0)x.setState(D({dataStartIndex:N,dataEndIndex:$},h({props:x.props,dataStartIndex:N,dataEndIndex:$,updateId:C},x.state)));else if(S.activeTooltipIndex!==void 0){var L=S.chartX,I=S.chartY,R=S.activeTooltipIndex,B=x.state,z=B.offset,k=B.tooltipTicks;if(!z)return;if(typeof A=="function")R=A(k,S);else if(A==="value"){R=-1;for(var F=0;F=0){var se,W;if(L.dataKey&&!L.allowDuplicatedCategory){var Be=typeof L.dataKey=="function"?ie:"payload.".concat(L.dataKey.toString());se=Hm(F,Be,R),W=U&&K&&Hm(K,Be,R)}else se=F==null?void 0:F[I],W=U&&K&&K[I];if(He||Oe){var he=S.props.activeIndex!==void 0?S.props.activeIndex:I;return[j.cloneElement(S,D(D(D({},A.props),kt),{},{activeIndex:he})),null,null]}if(!ue(se))return[X].concat(Ko(x.renderActivePoints({item:A,activePoint:se,basePoint:W,childIndex:I,isRange:U})))}else{var Qe,Ye=(Qe=x.getItemByXY(x.state.activeCoordinate))!==null&&Qe!==void 0?Qe:{graphicalItem:X},Nt=Ye.graphicalItem,Cn=Nt.item,Oa=Cn===void 0?S:Cn,Pu=Nt.childIndex,wi=D(D(D({},A.props),kt),{},{activeIndex:Pu});return[j.cloneElement(Oa,wi),null,null]}return U?[X,null,null]:[X,null]}),ee(x,"renderCustomized",function(S,_,P){return j.cloneElement(S,D(D({key:"recharts-customized-".concat(P)},x.props),x.state))}),ee(x,"renderMap",{CartesianGrid:{handler:ac,once:!0},ReferenceArea:{handler:x.renderReferenceElement},ReferenceLine:{handler:ac},ReferenceDot:{handler:x.renderReferenceElement},XAxis:{handler:ac},YAxis:{handler:ac},Brush:{handler:x.renderBrush,once:!0},Bar:{handler:x.renderGraphicChild},Line:{handler:x.renderGraphicChild},Area:{handler:x.renderGraphicChild},Radar:{handler:x.renderGraphicChild},RadialBar:{handler:x.renderGraphicChild},Scatter:{handler:x.renderGraphicChild},Pie:{handler:x.renderGraphicChild},Funnel:{handler:x.renderGraphicChild},Tooltip:{handler:x.renderCursor,once:!0},PolarGrid:{handler:x.renderPolarGrid,once:!0},PolarAngleAxis:{handler:x.renderPolarAxis},PolarRadiusAxis:{handler:x.renderPolarAxis},Customized:{handler:x.renderCustomized}}),x.clipPathId="".concat((b=g.id)!==null&&b!==void 0?b:yu("recharts"),"-clip"),x.throttleTriggeredAfterMouseMove=ME(x.triggeredAfterMouseMove,(w=g.throttleDelay)!==null&&w!==void 0?w:1e3/60),x.state={},x}return Cee(v,y),Aee(v,[{key:"componentDidMount",value:function(){var b,w;this.addListener(),this.accessibilityManager.setDetails({container:this.container,offset:{left:(b=this.props.margin.left)!==null&&b!==void 0?b:0,top:(w=this.props.margin.top)!==null&&w!==void 0?w:0},coordinateList:this.state.tooltipTicks,mouseHandlerCallback:this.triggeredAfterMouseMove,layout:this.props.layout}),this.displayDefaultTooltip()}},{key:"displayDefaultTooltip",value:function(){var b=this.props,w=b.children,x=b.data,S=b.height,_=b.layout,P=Gt(w,Ar);if(P){var A=P.props.defaultIndex;if(!(typeof A!="number"||A<0||A>this.state.tooltipTicks.length-1)){var C=this.state.tooltipTicks[A]&&this.state.tooltipTicks[A].value,N=yv(this.state,x,A,C),$=this.state.tooltipTicks[A].coordinate,L=(this.state.offset.top+S)/2,I=_==="horizontal",R=I?{x:$,y:L}:{y:$,x:L},B=this.state.formattedGraphicalItems.find(function(k){var F=k.item;return F.type.name==="Scatter"});B&&(R=D(D({},R),B.props.points[A].tooltipPosition),N=B.props.points[A].tooltipPayload);var z={activeTooltipIndex:A,isTooltipActive:!0,activeLabel:C,activePayload:N,activeCoordinate:R};this.setState(z),this.renderCursor(P),this.accessibilityManager.setIndex(A)}}}},{key:"getSnapshotBeforeUpdate",value:function(b,w){if(!this.props.accessibilityLayer)return null;if(this.state.tooltipTicks!==w.tooltipTicks&&this.accessibilityManager.setDetails({coordinateList:this.state.tooltipTicks}),this.props.layout!==b.layout&&this.accessibilityManager.setDetails({layout:this.props.layout}),this.props.margin!==b.margin){var x,S;this.accessibilityManager.setDetails({offset:{left:(x=this.props.margin.left)!==null&&x!==void 0?x:0,top:(S=this.props.margin.top)!==null&&S!==void 0?S:0}})}return null}},{key:"componentDidUpdate",value:function(b){Km([Gt(b.children,Ar)],[Gt(this.props.children,Ar)])||this.displayDefaultTooltip()}},{key:"componentWillUnmount",value:function(){this.removeListener(),this.throttleTriggeredAfterMouseMove.cancel()}},{key:"getTooltipEventType",value:function(){var b=Gt(this.props.children,Ar);if(b&&typeof b.props.shared=="boolean"){var w=b.props.shared?"axis":"item";return s.indexOf(w)>=0?w:a}return a}},{key:"getMouseInfo",value:function(b){if(!this.container)return null;var w=this.container,x=w.getBoundingClientRect(),S=bW(x),_={chartX:Math.round(b.pageX-S.left),chartY:Math.round(b.pageY-S.top)},P=x.width/w.offsetWidth||1,A=this.inRange(_.chartX,_.chartY,P);if(!A)return null;var C=this.state,N=C.xAxisMap,$=C.yAxisMap,L=this.getTooltipEventType(),I=rO(this.state,this.props.data,this.props.layout,A);if(L!=="axis"&&N&&$){var R=Ea(N).scale,B=Ea($).scale,z=R&&R.invert?R.invert(_.chartX):null,k=B&&B.invert?B.invert(_.chartY):null;return D(D({},_),{},{xValue:z,yValue:k},I)}return I?D(D({},_),I):null}},{key:"inRange",value:function(b,w){var x=arguments.length>2&&arguments[2]!==void 0?arguments[2]:1,S=this.props.layout,_=b/x,P=w/x;if(S==="horizontal"||S==="vertical"){var A=this.state.offset,C=_>=A.left&&_<=A.left+A.width&&P>=A.top&&P<=A.top+A.height;return C?{x:_,y:P}:null}var N=this.state,$=N.angleAxisMap,L=N.radiusAxisMap;if($&&L){var I=Ea($);return Iw({x:_,y:P},I)}return null}},{key:"parseEventsOfWrapper",value:function(){var b=this.props.children,w=this.getTooltipEventType(),x=Gt(b,Ar),S={};x&&w==="axis"&&(x.props.trigger==="click"?S={onClick:this.handleClick}:S={onMouseEnter:this.handleMouseEnter,onDoubleClick:this.handleDoubleClick,onMouseMove:this.handleMouseMove,onMouseLeave:this.handleMouseLeave,onTouchMove:this.handleTouchMove,onTouchStart:this.handleTouchStart,onTouchEnd:this.handleTouchEnd,onContextMenu:this.handleContextMenu});var _=rf(this.props,this.handleOuterEvent);return D(D({},_),S)}},{key:"addListener",value:function(){Ep.on(jp,this.handleReceiveSyncEvent)}},{key:"removeListener",value:function(){Ep.removeListener(jp,this.handleReceiveSyncEvent)}},{key:"filterFormatItem",value:function(b,w,x){for(var S=this.state.formattedGraphicalItems,_=0,P=S.length;_t=>{const r=Gee.call(t);return e[r]||(e[r]=r.slice(8,-1).toLowerCase())})(Object.create(null)),Mr=e=>(e=e.toLowerCase(),t=>Sh(t)===e),Oh=e=>t=>typeof t===e,{isArray:fs}=Array,Vo=Oh("undefined");function wu(e){return e!==null&&!Vo(e)&&e.constructor!==null&&!Vo(e.constructor)&&Wt(e.constructor.isBuffer)&&e.constructor.isBuffer(e)}const i2=Mr("ArrayBuffer");function Xee(e){let t;return typeof ArrayBuffer<"u"&&ArrayBuffer.isView?t=ArrayBuffer.isView(e):t=e&&e.buffer&&i2(e.buffer),t}const Qee=Oh("string"),Wt=Oh("function"),a2=Oh("number"),Su=e=>e!==null&&typeof e=="object",Yee=e=>e===!0||e===!1,Sc=e=>{if(Sh(e)!=="object")return!1;const t=T0(e);return(t===null||t===Object.prototype||Object.getPrototypeOf(t)===null)&&!(n2 in e)&&!(wh in e)},Jee=e=>{if(!Su(e)||wu(e))return!1;try{return Object.keys(e).length===0&&Object.getPrototypeOf(e)===Object.prototype}catch{return!1}},Zee=Mr("Date"),ete=Mr("File"),tte=Mr("Blob"),rte=Mr("FileList"),nte=e=>Su(e)&&Wt(e.pipe),ite=e=>{let t;return e&&(typeof FormData=="function"&&e instanceof FormData||Wt(e.append)&&((t=Sh(e))==="formdata"||t==="object"&&Wt(e.toString)&&e.toString()==="[object FormData]"))},ate=Mr("URLSearchParams"),[ote,ste,lte,ute]=["ReadableStream","Request","Response","Headers"].map(Mr),cte=e=>e.trim?e.trim():e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"");function Ou(e,t,{allOwnKeys:r=!1}={}){if(e===null||typeof e>"u")return;let n,i;if(typeof e!="object"&&(e=[e]),fs(e))for(n=0,i=e.length;n0;)if(i=r[n],t===i.toLowerCase())return i;return null}const zi=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:global,s2=e=>!Vo(e)&&e!==zi;function vv(){const{caseless:e,skipUndefined:t}=s2(this)&&this||{},r={},n=(i,a)=>{const o=e&&o2(r,a)||a;Sc(r[o])&&Sc(i)?r[o]=vv(r[o],i):Sc(i)?r[o]=vv({},i):fs(i)?r[o]=i.slice():(!t||!Vo(i))&&(r[o]=i)};for(let i=0,a=arguments.length;i(Ou(t,(i,a)=>{r&&Wt(i)?e[a]=r2(i,r):e[a]=i},{allOwnKeys:n}),e),dte=e=>(e.charCodeAt(0)===65279&&(e=e.slice(1)),e),hte=(e,t,r,n)=>{e.prototype=Object.create(t.prototype,n),e.prototype.constructor=e,Object.defineProperty(e,"super",{value:t.prototype}),r&&Object.assign(e.prototype,r)},pte=(e,t,r,n)=>{let i,a,o;const s={};if(t=t||{},e==null)return t;do{for(i=Object.getOwnPropertyNames(e),a=i.length;a-- >0;)o=i[a],(!n||n(o,e,t))&&!s[o]&&(t[o]=e[o],s[o]=!0);e=r!==!1&&T0(e)}while(e&&(!r||r(e,t))&&e!==Object.prototype);return t},mte=(e,t,r)=>{e=String(e),(r===void 0||r>e.length)&&(r=e.length),r-=t.length;const n=e.indexOf(t,r);return n!==-1&&n===r},yte=e=>{if(!e)return null;if(fs(e))return e;let t=e.length;if(!a2(t))return null;const r=new Array(t);for(;t-- >0;)r[t]=e[t];return r},vte=(e=>t=>e&&t instanceof e)(typeof Uint8Array<"u"&&T0(Uint8Array)),gte=(e,t)=>{const n=(e&&e[wh]).call(e);let i;for(;(i=n.next())&&!i.done;){const a=i.value;t.call(e,a[0],a[1])}},bte=(e,t)=>{let r;const n=[];for(;(r=e.exec(t))!==null;)n.push(r);return n},xte=Mr("HTMLFormElement"),wte=e=>e.toLowerCase().replace(/[-_\s]([a-z\d])(\w*)/g,function(r,n,i){return n.toUpperCase()+i}),aO=(({hasOwnProperty:e})=>(t,r)=>e.call(t,r))(Object.prototype),Ste=Mr("RegExp"),l2=(e,t)=>{const r=Object.getOwnPropertyDescriptors(e),n={};Ou(r,(i,a)=>{let o;(o=t(i,a,e))!==!1&&(n[a]=o||i)}),Object.defineProperties(e,n)},Ote=e=>{l2(e,(t,r)=>{if(Wt(e)&&["arguments","caller","callee"].indexOf(r)!==-1)return!1;const n=e[r];if(Wt(n)){if(t.enumerable=!1,"writable"in t){t.writable=!1;return}t.set||(t.set=()=>{throw Error("Can not rewrite read-only method '"+r+"'")})}})},_te=(e,t)=>{const r={},n=i=>{i.forEach(a=>{r[a]=!0})};return fs(e)?n(e):n(String(e).split(t)),r},Pte=()=>{},Ate=(e,t)=>e!=null&&Number.isFinite(e=+e)?e:t;function Ete(e){return!!(e&&Wt(e.append)&&e[n2]==="FormData"&&e[wh])}const jte=e=>{const t=new Array(10),r=(n,i)=>{if(Su(n)){if(t.indexOf(n)>=0)return;if(wu(n))return n;if(!("toJSON"in n)){t[i]=n;const a=fs(n)?[]:{};return Ou(n,(o,s)=>{const l=r(o,i+1);!Vo(l)&&(a[s]=l)}),t[i]=void 0,a}}return n};return r(e,0)},Tte=Mr("AsyncFunction"),Cte=e=>e&&(Su(e)||Wt(e))&&Wt(e.then)&&Wt(e.catch),u2=((e,t)=>e?setImmediate:t?((r,n)=>(zi.addEventListener("message",({source:i,data:a})=>{i===zi&&a===r&&n.length&&n.shift()()},!1),i=>{n.push(i),zi.postMessage(r,"*")}))(`axios@${Math.random()}`,[]):r=>setTimeout(r))(typeof setImmediate=="function",Wt(zi.postMessage)),$te=typeof queueMicrotask<"u"?queueMicrotask.bind(zi):typeof process<"u"&&process.nextTick||u2,kte=e=>e!=null&&Wt(e[wh]),M={isArray:fs,isArrayBuffer:i2,isBuffer:wu,isFormData:ite,isArrayBufferView:Xee,isString:Qee,isNumber:a2,isBoolean:Yee,isObject:Su,isPlainObject:Sc,isEmptyObject:Jee,isReadableStream:ote,isRequest:ste,isResponse:lte,isHeaders:ute,isUndefined:Vo,isDate:Zee,isFile:ete,isBlob:tte,isRegExp:Ste,isFunction:Wt,isStream:nte,isURLSearchParams:ate,isTypedArray:vte,isFileList:rte,forEach:Ou,merge:vv,extend:fte,trim:cte,stripBOM:dte,inherits:hte,toFlatObject:pte,kindOf:Sh,kindOfTest:Mr,endsWith:mte,toArray:yte,forEachEntry:gte,matchAll:bte,isHTMLForm:xte,hasOwnProperty:aO,hasOwnProp:aO,reduceDescriptors:l2,freezeMethods:Ote,toObjectSet:_te,toCamelCase:wte,noop:Pte,toFiniteNumber:Ate,findKey:o2,global:zi,isContextDefined:s2,isSpecCompliantForm:Ete,toJSONObject:jte,isAsyncFn:Tte,isThenable:Cte,setImmediate:u2,asap:$te,isIterable:kte};function ne(e,t,r,n,i){Error.call(this),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=new Error().stack,this.message=e,this.name="AxiosError",t&&(this.code=t),r&&(this.config=r),n&&(this.request=n),i&&(this.response=i,this.status=i.status?i.status:null)}M.inherits(ne,Error,{toJSON:function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:M.toJSONObject(this.config),code:this.code,status:this.status}}});const c2=ne.prototype,f2={};["ERR_BAD_OPTION_VALUE","ERR_BAD_OPTION","ECONNABORTED","ETIMEDOUT","ERR_NETWORK","ERR_FR_TOO_MANY_REDIRECTS","ERR_DEPRECATED","ERR_BAD_RESPONSE","ERR_BAD_REQUEST","ERR_CANCELED","ERR_NOT_SUPPORT","ERR_INVALID_URL"].forEach(e=>{f2[e]={value:e}});Object.defineProperties(ne,f2);Object.defineProperty(c2,"isAxiosError",{value:!0});ne.from=(e,t,r,n,i,a)=>{const o=Object.create(c2);M.toFlatObject(e,o,function(f){return f!==Error.prototype},u=>u!=="isAxiosError");const s=e&&e.message?e.message:"Error",l=t==null&&e?e.code:t;return ne.call(o,s,l,r,n,i),e&&o.cause==null&&Object.defineProperty(o,"cause",{value:e,configurable:!0}),o.name=e&&e.name||"Error",a&&Object.assign(o,a),o};const Nte=null;function gv(e){return M.isPlainObject(e)||M.isArray(e)}function d2(e){return M.endsWith(e,"[]")?e.slice(0,-2):e}function oO(e,t,r){return e?e.concat(t).map(function(i,a){return i=d2(i),!r&&a?"["+i+"]":i}).join(r?".":""):t}function Mte(e){return M.isArray(e)&&!e.some(gv)}const Ite=M.toFlatObject(M,{},null,function(t){return/^is[A-Z]/.test(t)});function _h(e,t,r){if(!M.isObject(e))throw new TypeError("target must be an object");t=t||new FormData,r=M.toFlatObject(r,{metaTokens:!0,dots:!1,indexes:!1},!1,function(m,y){return!M.isUndefined(y[m])});const n=r.metaTokens,i=r.visitor||f,a=r.dots,o=r.indexes,l=(r.Blob||typeof Blob<"u"&&Blob)&&M.isSpecCompliantForm(t);if(!M.isFunction(i))throw new TypeError("visitor must be a function");function u(p){if(p===null)return"";if(M.isDate(p))return p.toISOString();if(M.isBoolean(p))return p.toString();if(!l&&M.isBlob(p))throw new ne("Blob is not supported. Use a Buffer instead.");return M.isArrayBuffer(p)||M.isTypedArray(p)?l&&typeof Blob=="function"?new Blob([p]):Buffer.from(p):p}function f(p,m,y){let v=p;if(p&&!y&&typeof p=="object"){if(M.endsWith(m,"{}"))m=n?m:m.slice(0,-2),p=JSON.stringify(p);else if(M.isArray(p)&&Mte(p)||(M.isFileList(p)||M.endsWith(m,"[]"))&&(v=M.toArray(p)))return m=d2(m),v.forEach(function(b,w){!(M.isUndefined(b)||b===null)&&t.append(o===!0?oO([m],w,a):o===null?m:m+"[]",u(b))}),!1}return gv(p)?!0:(t.append(oO(y,m,a),u(p)),!1)}const c=[],d=Object.assign(Ite,{defaultVisitor:f,convertValue:u,isVisitable:gv});function h(p,m){if(!M.isUndefined(p)){if(c.indexOf(p)!==-1)throw Error("Circular reference detected in "+m.join("."));c.push(p),M.forEach(p,function(v,g){(!(M.isUndefined(v)||v===null)&&i.call(t,v,M.isString(g)?g.trim():g,m,d))===!0&&h(v,m?m.concat(g):[g])}),c.pop()}}if(!M.isObject(e))throw new TypeError("data must be an object");return h(e),t}function sO(e){const t={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+","%00":"\0"};return encodeURIComponent(e).replace(/[!'()~]|%20|%00/g,function(n){return t[n]})}function C0(e,t){this._pairs=[],e&&_h(e,this,t)}const h2=C0.prototype;h2.append=function(t,r){this._pairs.push([t,r])};h2.toString=function(t){const r=t?function(n){return t.call(this,n,sO)}:sO;return this._pairs.map(function(i){return r(i[0])+"="+r(i[1])},"").join("&")};function Rte(e){return encodeURIComponent(e).replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+")}function p2(e,t,r){if(!t)return e;const n=r&&r.encode||Rte;M.isFunction(r)&&(r={serialize:r});const i=r&&r.serialize;let a;if(i?a=i(t,r):a=M.isURLSearchParams(t)?t.toString():new C0(t,r).toString(n),a){const o=e.indexOf("#");o!==-1&&(e=e.slice(0,o)),e+=(e.indexOf("?")===-1?"?":"&")+a}return e}class lO{constructor(){this.handlers=[]}use(t,r,n){return this.handlers.push({fulfilled:t,rejected:r,synchronous:n?n.synchronous:!1,runWhen:n?n.runWhen:null}),this.handlers.length-1}eject(t){this.handlers[t]&&(this.handlers[t]=null)}clear(){this.handlers&&(this.handlers=[])}forEach(t){M.forEach(this.handlers,function(n){n!==null&&t(n)})}}const m2={silentJSONParsing:!0,forcedJSONParsing:!0,clarifyTimeoutError:!1},Dte=typeof URLSearchParams<"u"?URLSearchParams:C0,Lte=typeof FormData<"u"?FormData:null,Bte=typeof Blob<"u"?Blob:null,Fte={isBrowser:!0,classes:{URLSearchParams:Dte,FormData:Lte,Blob:Bte},protocols:["http","https","file","blob","url","data"]},$0=typeof window<"u"&&typeof document<"u",bv=typeof navigator=="object"&&navigator||void 0,Ute=$0&&(!bv||["ReactNative","NativeScript","NS"].indexOf(bv.product)<0),zte=typeof WorkerGlobalScope<"u"&&self instanceof WorkerGlobalScope&&typeof self.importScripts=="function",Wte=$0&&window.location.href||"http://localhost",Hte=Object.freeze(Object.defineProperty({__proto__:null,hasBrowserEnv:$0,hasStandardBrowserEnv:Ute,hasStandardBrowserWebWorkerEnv:zte,navigator:bv,origin:Wte},Symbol.toStringTag,{value:"Module"})),wt={...Hte,...Fte};function qte(e,t){return _h(e,new wt.classes.URLSearchParams,{visitor:function(r,n,i,a){return wt.isNode&&M.isBuffer(r)?(this.append(n,r.toString("base64")),!1):a.defaultVisitor.apply(this,arguments)},...t})}function Kte(e){return M.matchAll(/\w+|\[(\w*)]/g,e).map(t=>t[0]==="[]"?"":t[1]||t[0])}function Vte(e){const t={},r=Object.keys(e);let n;const i=r.length;let a;for(n=0;n=r.length;return o=!o&&M.isArray(i)?i.length:o,l?(M.hasOwnProp(i,o)?i[o]=[i[o],n]:i[o]=n,!s):((!i[o]||!M.isObject(i[o]))&&(i[o]=[]),t(r,n,i[o],a)&&M.isArray(i[o])&&(i[o]=Vte(i[o])),!s)}if(M.isFormData(e)&&M.isFunction(e.entries)){const r={};return M.forEachEntry(e,(n,i)=>{t(Kte(n),i,r,0)}),r}return null}function Gte(e,t,r){if(M.isString(e))try{return(t||JSON.parse)(e),M.trim(e)}catch(n){if(n.name!=="SyntaxError")throw n}return(r||JSON.stringify)(e)}const _u={transitional:m2,adapter:["xhr","http","fetch"],transformRequest:[function(t,r){const n=r.getContentType()||"",i=n.indexOf("application/json")>-1,a=M.isObject(t);if(a&&M.isHTMLForm(t)&&(t=new FormData(t)),M.isFormData(t))return i?JSON.stringify(y2(t)):t;if(M.isArrayBuffer(t)||M.isBuffer(t)||M.isStream(t)||M.isFile(t)||M.isBlob(t)||M.isReadableStream(t))return t;if(M.isArrayBufferView(t))return t.buffer;if(M.isURLSearchParams(t))return r.setContentType("application/x-www-form-urlencoded;charset=utf-8",!1),t.toString();let s;if(a){if(n.indexOf("application/x-www-form-urlencoded")>-1)return qte(t,this.formSerializer).toString();if((s=M.isFileList(t))||n.indexOf("multipart/form-data")>-1){const l=this.env&&this.env.FormData;return _h(s?{"files[]":t}:t,l&&new l,this.formSerializer)}}return a||i?(r.setContentType("application/json",!1),Gte(t)):t}],transformResponse:[function(t){const r=this.transitional||_u.transitional,n=r&&r.forcedJSONParsing,i=this.responseType==="json";if(M.isResponse(t)||M.isReadableStream(t))return t;if(t&&M.isString(t)&&(n&&!this.responseType||i)){const o=!(r&&r.silentJSONParsing)&&i;try{return JSON.parse(t,this.parseReviver)}catch(s){if(o)throw s.name==="SyntaxError"?ne.from(s,ne.ERR_BAD_RESPONSE,this,null,this.response):s}}return t}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,maxBodyLength:-1,env:{FormData:wt.classes.FormData,Blob:wt.classes.Blob},validateStatus:function(t){return t>=200&&t<300},headers:{common:{Accept:"application/json, text/plain, */*","Content-Type":void 0}}};M.forEach(["delete","get","head","post","put","patch"],e=>{_u.headers[e]={}});const Xte=M.toObjectSet(["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"]),Qte=e=>{const t={};let r,n,i;return e&&e.split(` +`).forEach(function(o){i=o.indexOf(":"),r=o.substring(0,i).trim().toLowerCase(),n=o.substring(i+1).trim(),!(!r||t[r]&&Xte[r])&&(r==="set-cookie"?t[r]?t[r].push(n):t[r]=[n]:t[r]=t[r]?t[r]+", "+n:n)}),t},uO=Symbol("internals");function $s(e){return e&&String(e).trim().toLowerCase()}function Oc(e){return e===!1||e==null?e:M.isArray(e)?e.map(Oc):String(e)}function Yte(e){const t=Object.create(null),r=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;let n;for(;n=r.exec(e);)t[n[1]]=n[2];return t}const Jte=e=>/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(e.trim());function Cp(e,t,r,n,i){if(M.isFunction(n))return n.call(this,t,r);if(i&&(t=r),!!M.isString(t)){if(M.isString(n))return t.indexOf(n)!==-1;if(M.isRegExp(n))return n.test(t)}}function Zte(e){return e.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,(t,r,n)=>r.toUpperCase()+n)}function ere(e,t){const r=M.toCamelCase(" "+t);["get","set","has"].forEach(n=>{Object.defineProperty(e,n+r,{value:function(i,a,o){return this[n].call(this,t,i,a,o)},configurable:!0})})}let Ht=class{constructor(t){t&&this.set(t)}set(t,r,n){const i=this;function a(s,l,u){const f=$s(l);if(!f)throw new Error("header name must be a non-empty string");const c=M.findKey(i,f);(!c||i[c]===void 0||u===!0||u===void 0&&i[c]!==!1)&&(i[c||l]=Oc(s))}const o=(s,l)=>M.forEach(s,(u,f)=>a(u,f,l));if(M.isPlainObject(t)||t instanceof this.constructor)o(t,r);else if(M.isString(t)&&(t=t.trim())&&!Jte(t))o(Qte(t),r);else if(M.isObject(t)&&M.isIterable(t)){let s={},l,u;for(const f of t){if(!M.isArray(f))throw TypeError("Object iterator must return a key-value pair");s[u=f[0]]=(l=s[u])?M.isArray(l)?[...l,f[1]]:[l,f[1]]:f[1]}o(s,r)}else t!=null&&a(r,t,n);return this}get(t,r){if(t=$s(t),t){const n=M.findKey(this,t);if(n){const i=this[n];if(!r)return i;if(r===!0)return Yte(i);if(M.isFunction(r))return r.call(this,i,n);if(M.isRegExp(r))return r.exec(i);throw new TypeError("parser must be boolean|regexp|function")}}}has(t,r){if(t=$s(t),t){const n=M.findKey(this,t);return!!(n&&this[n]!==void 0&&(!r||Cp(this,this[n],n,r)))}return!1}delete(t,r){const n=this;let i=!1;function a(o){if(o=$s(o),o){const s=M.findKey(n,o);s&&(!r||Cp(n,n[s],s,r))&&(delete n[s],i=!0)}}return M.isArray(t)?t.forEach(a):a(t),i}clear(t){const r=Object.keys(this);let n=r.length,i=!1;for(;n--;){const a=r[n];(!t||Cp(this,this[a],a,t,!0))&&(delete this[a],i=!0)}return i}normalize(t){const r=this,n={};return M.forEach(this,(i,a)=>{const o=M.findKey(n,a);if(o){r[o]=Oc(i),delete r[a];return}const s=t?Zte(a):String(a).trim();s!==a&&delete r[a],r[s]=Oc(i),n[s]=!0}),this}concat(...t){return this.constructor.concat(this,...t)}toJSON(t){const r=Object.create(null);return M.forEach(this,(n,i)=>{n!=null&&n!==!1&&(r[i]=t&&M.isArray(n)?n.join(", "):n)}),r}[Symbol.iterator](){return Object.entries(this.toJSON())[Symbol.iterator]()}toString(){return Object.entries(this.toJSON()).map(([t,r])=>t+": "+r).join(` +`)}getSetCookie(){return this.get("set-cookie")||[]}get[Symbol.toStringTag](){return"AxiosHeaders"}static from(t){return t instanceof this?t:new this(t)}static concat(t,...r){const n=new this(t);return r.forEach(i=>n.set(i)),n}static accessor(t){const n=(this[uO]=this[uO]={accessors:{}}).accessors,i=this.prototype;function a(o){const s=$s(o);n[s]||(ere(i,o),n[s]=!0)}return M.isArray(t)?t.forEach(a):a(t),this}};Ht.accessor(["Content-Type","Content-Length","Accept","Accept-Encoding","User-Agent","Authorization"]);M.reduceDescriptors(Ht.prototype,({value:e},t)=>{let r=t[0].toUpperCase()+t.slice(1);return{get:()=>e,set(n){this[r]=n}}});M.freezeMethods(Ht);function $p(e,t){const r=this||_u,n=t||r,i=Ht.from(n.headers);let a=n.data;return M.forEach(e,function(s){a=s.call(r,a,i.normalize(),t?t.status:void 0)}),i.normalize(),a}function v2(e){return!!(e&&e.__CANCEL__)}function ds(e,t,r){ne.call(this,e??"canceled",ne.ERR_CANCELED,t,r),this.name="CanceledError"}M.inherits(ds,ne,{__CANCEL__:!0});function g2(e,t,r){const n=r.config.validateStatus;!r.status||!n||n(r.status)?e(r):t(new ne("Request failed with status code "+r.status,[ne.ERR_BAD_REQUEST,ne.ERR_BAD_RESPONSE][Math.floor(r.status/100)-4],r.config,r.request,r))}function tre(e){const t=/^([-+\w]{1,25})(:?\/\/|:)/.exec(e);return t&&t[1]||""}function rre(e,t){e=e||10;const r=new Array(e),n=new Array(e);let i=0,a=0,o;return t=t!==void 0?t:1e3,function(l){const u=Date.now(),f=n[a];o||(o=u),r[i]=l,n[i]=u;let c=a,d=0;for(;c!==i;)d+=r[c++],c=c%e;if(i=(i+1)%e,i===a&&(a=(a+1)%e),u-o{r=f,i=null,a&&(clearTimeout(a),a=null),e(...u)};return[(...u)=>{const f=Date.now(),c=f-r;c>=n?o(u,f):(i=u,a||(a=setTimeout(()=>{a=null,o(i)},n-c)))},()=>i&&o(i)]}const nd=(e,t,r=3)=>{let n=0;const i=rre(50,250);return nre(a=>{const o=a.loaded,s=a.lengthComputable?a.total:void 0,l=o-n,u=i(l),f=o<=s;n=o;const c={loaded:o,total:s,progress:s?o/s:void 0,bytes:l,rate:u||void 0,estimated:u&&s&&f?(s-o)/u:void 0,event:a,lengthComputable:s!=null,[t?"download":"upload"]:!0};e(c)},r)},cO=(e,t)=>{const r=e!=null;return[n=>t[0]({lengthComputable:r,total:e,loaded:n}),t[1]]},fO=e=>(...t)=>M.asap(()=>e(...t)),ire=wt.hasStandardBrowserEnv?((e,t)=>r=>(r=new URL(r,wt.origin),e.protocol===r.protocol&&e.host===r.host&&(t||e.port===r.port)))(new URL(wt.origin),wt.navigator&&/(msie|trident)/i.test(wt.navigator.userAgent)):()=>!0,are=wt.hasStandardBrowserEnv?{write(e,t,r,n,i,a,o){if(typeof document>"u")return;const s=[`${e}=${encodeURIComponent(t)}`];M.isNumber(r)&&s.push(`expires=${new Date(r).toUTCString()}`),M.isString(n)&&s.push(`path=${n}`),M.isString(i)&&s.push(`domain=${i}`),a===!0&&s.push("secure"),M.isString(o)&&s.push(`SameSite=${o}`),document.cookie=s.join("; ")},read(e){if(typeof document>"u")return null;const t=document.cookie.match(new RegExp("(?:^|; )"+e+"=([^;]*)"));return t?decodeURIComponent(t[1]):null},remove(e){this.write(e,"",Date.now()-864e5,"/")}}:{write(){},read(){return null},remove(){}};function ore(e){return/^([a-z][a-z\d+\-.]*:)?\/\//i.test(e)}function sre(e,t){return t?e.replace(/\/?\/$/,"")+"/"+t.replace(/^\/+/,""):e}function b2(e,t,r){let n=!ore(t);return e&&(n||r==!1)?sre(e,t):t}const dO=e=>e instanceof Ht?{...e}:e;function ma(e,t){t=t||{};const r={};function n(u,f,c,d){return M.isPlainObject(u)&&M.isPlainObject(f)?M.merge.call({caseless:d},u,f):M.isPlainObject(f)?M.merge({},f):M.isArray(f)?f.slice():f}function i(u,f,c,d){if(M.isUndefined(f)){if(!M.isUndefined(u))return n(void 0,u,c,d)}else return n(u,f,c,d)}function a(u,f){if(!M.isUndefined(f))return n(void 0,f)}function o(u,f){if(M.isUndefined(f)){if(!M.isUndefined(u))return n(void 0,u)}else return n(void 0,f)}function s(u,f,c){if(c in t)return n(u,f);if(c in e)return n(void 0,u)}const l={url:a,method:a,data:a,baseURL:o,transformRequest:o,transformResponse:o,paramsSerializer:o,timeout:o,timeoutMessage:o,withCredentials:o,withXSRFToken:o,adapter:o,responseType:o,xsrfCookieName:o,xsrfHeaderName:o,onUploadProgress:o,onDownloadProgress:o,decompress:o,maxContentLength:o,maxBodyLength:o,beforeRedirect:o,transport:o,httpAgent:o,httpsAgent:o,cancelToken:o,socketPath:o,responseEncoding:o,validateStatus:s,headers:(u,f,c)=>i(dO(u),dO(f),c,!0)};return M.forEach(Object.keys({...e,...t}),function(f){const c=l[f]||i,d=c(e[f],t[f],f);M.isUndefined(d)&&c!==s||(r[f]=d)}),r}const x2=e=>{const t=ma({},e);let{data:r,withXSRFToken:n,xsrfHeaderName:i,xsrfCookieName:a,headers:o,auth:s}=t;if(t.headers=o=Ht.from(o),t.url=p2(b2(t.baseURL,t.url,t.allowAbsoluteUrls),e.params,e.paramsSerializer),s&&o.set("Authorization","Basic "+btoa((s.username||"")+":"+(s.password?unescape(encodeURIComponent(s.password)):""))),M.isFormData(r)){if(wt.hasStandardBrowserEnv||wt.hasStandardBrowserWebWorkerEnv)o.setContentType(void 0);else if(M.isFunction(r.getHeaders)){const l=r.getHeaders(),u=["content-type","content-length"];Object.entries(l).forEach(([f,c])=>{u.includes(f.toLowerCase())&&o.set(f,c)})}}if(wt.hasStandardBrowserEnv&&(n&&M.isFunction(n)&&(n=n(t)),n||n!==!1&&ire(t.url))){const l=i&&a&&are.read(a);l&&o.set(i,l)}return t},lre=typeof XMLHttpRequest<"u",ure=lre&&function(e){return new Promise(function(r,n){const i=x2(e);let a=i.data;const o=Ht.from(i.headers).normalize();let{responseType:s,onUploadProgress:l,onDownloadProgress:u}=i,f,c,d,h,p;function m(){h&&h(),p&&p(),i.cancelToken&&i.cancelToken.unsubscribe(f),i.signal&&i.signal.removeEventListener("abort",f)}let y=new XMLHttpRequest;y.open(i.method.toUpperCase(),i.url,!0),y.timeout=i.timeout;function v(){if(!y)return;const b=Ht.from("getAllResponseHeaders"in y&&y.getAllResponseHeaders()),x={data:!s||s==="text"||s==="json"?y.responseText:y.response,status:y.status,statusText:y.statusText,headers:b,config:e,request:y};g2(function(_){r(_),m()},function(_){n(_),m()},x),y=null}"onloadend"in y?y.onloadend=v:y.onreadystatechange=function(){!y||y.readyState!==4||y.status===0&&!(y.responseURL&&y.responseURL.indexOf("file:")===0)||setTimeout(v)},y.onabort=function(){y&&(n(new ne("Request aborted",ne.ECONNABORTED,e,y)),y=null)},y.onerror=function(w){const x=w&&w.message?w.message:"Network Error",S=new ne(x,ne.ERR_NETWORK,e,y);S.event=w||null,n(S),y=null},y.ontimeout=function(){let w=i.timeout?"timeout of "+i.timeout+"ms exceeded":"timeout exceeded";const x=i.transitional||m2;i.timeoutErrorMessage&&(w=i.timeoutErrorMessage),n(new ne(w,x.clarifyTimeoutError?ne.ETIMEDOUT:ne.ECONNABORTED,e,y)),y=null},a===void 0&&o.setContentType(null),"setRequestHeader"in y&&M.forEach(o.toJSON(),function(w,x){y.setRequestHeader(x,w)}),M.isUndefined(i.withCredentials)||(y.withCredentials=!!i.withCredentials),s&&s!=="json"&&(y.responseType=i.responseType),u&&([d,p]=nd(u,!0),y.addEventListener("progress",d)),l&&y.upload&&([c,h]=nd(l),y.upload.addEventListener("progress",c),y.upload.addEventListener("loadend",h)),(i.cancelToken||i.signal)&&(f=b=>{y&&(n(!b||b.type?new ds(null,e,y):b),y.abort(),y=null)},i.cancelToken&&i.cancelToken.subscribe(f),i.signal&&(i.signal.aborted?f():i.signal.addEventListener("abort",f)));const g=tre(i.url);if(g&&wt.protocols.indexOf(g)===-1){n(new ne("Unsupported protocol "+g+":",ne.ERR_BAD_REQUEST,e));return}y.send(a||null)})},cre=(e,t)=>{const{length:r}=e=e?e.filter(Boolean):[];if(t||r){let n=new AbortController,i;const a=function(u){if(!i){i=!0,s();const f=u instanceof Error?u:this.reason;n.abort(f instanceof ne?f:new ds(f instanceof Error?f.message:f))}};let o=t&&setTimeout(()=>{o=null,a(new ne(`timeout ${t} of ms exceeded`,ne.ETIMEDOUT))},t);const s=()=>{e&&(o&&clearTimeout(o),o=null,e.forEach(u=>{u.unsubscribe?u.unsubscribe(a):u.removeEventListener("abort",a)}),e=null)};e.forEach(u=>u.addEventListener("abort",a));const{signal:l}=n;return l.unsubscribe=()=>M.asap(s),l}},fre=function*(e,t){let r=e.byteLength;if(r{const i=dre(e,t);let a=0,o,s=l=>{o||(o=!0,n&&n(l))};return new ReadableStream({async pull(l){try{const{done:u,value:f}=await i.next();if(u){s(),l.close();return}let c=f.byteLength;if(r){let d=a+=c;r(d)}l.enqueue(new Uint8Array(f))}catch(u){throw s(u),u}},cancel(l){return s(l),i.return()}},{highWaterMark:2})},pO=64*1024,{isFunction:oc}=M,pre=(({Request:e,Response:t})=>({Request:e,Response:t}))(M.global),{ReadableStream:mO,TextEncoder:yO}=M.global,vO=(e,...t)=>{try{return!!e(...t)}catch{return!1}},mre=e=>{e=M.merge.call({skipUndefined:!0},pre,e);const{fetch:t,Request:r,Response:n}=e,i=t?oc(t):typeof fetch=="function",a=oc(r),o=oc(n);if(!i)return!1;const s=i&&oc(mO),l=i&&(typeof yO=="function"?(p=>m=>p.encode(m))(new yO):async p=>new Uint8Array(await new r(p).arrayBuffer())),u=a&&s&&vO(()=>{let p=!1;const m=new r(wt.origin,{body:new mO,method:"POST",get duplex(){return p=!0,"half"}}).headers.has("Content-Type");return p&&!m}),f=o&&s&&vO(()=>M.isReadableStream(new n("").body)),c={stream:f&&(p=>p.body)};i&&["text","arrayBuffer","blob","formData","stream"].forEach(p=>{!c[p]&&(c[p]=(m,y)=>{let v=m&&m[p];if(v)return v.call(m);throw new ne(`Response type '${p}' is not supported`,ne.ERR_NOT_SUPPORT,y)})});const d=async p=>{if(p==null)return 0;if(M.isBlob(p))return p.size;if(M.isSpecCompliantForm(p))return(await new r(wt.origin,{method:"POST",body:p}).arrayBuffer()).byteLength;if(M.isArrayBufferView(p)||M.isArrayBuffer(p))return p.byteLength;if(M.isURLSearchParams(p)&&(p=p+""),M.isString(p))return(await l(p)).byteLength},h=async(p,m)=>{const y=M.toFiniteNumber(p.getContentLength());return y??d(m)};return async p=>{let{url:m,method:y,data:v,signal:g,cancelToken:b,timeout:w,onDownloadProgress:x,onUploadProgress:S,responseType:_,headers:P,withCredentials:A="same-origin",fetchOptions:C}=x2(p),N=t||fetch;_=_?(_+"").toLowerCase():"text";let $=cre([g,b&&b.toAbortSignal()],w),L=null;const I=$&&$.unsubscribe&&(()=>{$.unsubscribe()});let R;try{if(S&&u&&y!=="get"&&y!=="head"&&(R=await h(P,v))!==0){let K=new r(m,{method:"POST",body:v,duplex:"half"}),H;if(M.isFormData(v)&&(H=K.headers.get("content-type"))&&P.setContentType(H),K.body){const[J,le]=cO(R,nd(fO(S)));v=hO(K.body,pO,J,le)}}M.isString(A)||(A=A?"include":"omit");const B=a&&"credentials"in r.prototype,z={...C,signal:$,method:y.toUpperCase(),headers:P.normalize().toJSON(),body:v,duplex:"half",credentials:B?A:void 0};L=a&&new r(m,z);let k=await(a?N(L,C):N(m,z));const F=f&&(_==="stream"||_==="response");if(f&&(x||F&&I)){const K={};["status","statusText","headers"].forEach(Oe=>{K[Oe]=k[Oe]});const H=M.toFiniteNumber(k.headers.get("content-length")),[J,le]=x&&cO(H,nd(fO(x),!0))||[];k=new n(hO(k.body,pO,J,()=>{le&&le(),I&&I()}),K)}_=_||"text";let U=await c[M.findKey(c,_)||"text"](k,p);return!F&&I&&I(),await new Promise((K,H)=>{g2(K,H,{data:U,headers:Ht.from(k.headers),status:k.status,statusText:k.statusText,config:p,request:L})})}catch(B){throw I&&I(),B&&B.name==="TypeError"&&/Load failed|fetch/i.test(B.message)?Object.assign(new ne("Network Error",ne.ERR_NETWORK,p,L),{cause:B.cause||B}):ne.from(B,B&&B.code,p,L)}}},yre=new Map,w2=e=>{let t=e&&e.env||{};const{fetch:r,Request:n,Response:i}=t,a=[n,i,r];let o=a.length,s=o,l,u,f=yre;for(;s--;)l=a[s],u=f.get(l),u===void 0&&f.set(l,u=s?new Map:mre(t)),f=u;return u};w2();const k0={http:Nte,xhr:ure,fetch:{get:w2}};M.forEach(k0,(e,t)=>{if(e){try{Object.defineProperty(e,"name",{value:t})}catch{}Object.defineProperty(e,"adapterName",{value:t})}});const gO=e=>`- ${e}`,vre=e=>M.isFunction(e)||e===null||e===!1;function gre(e,t){e=M.isArray(e)?e:[e];const{length:r}=e;let n,i;const a={};for(let o=0;o`adapter ${l} `+(u===!1?"is not supported by the environment":"is not available in the build"));let s=r?o.length>1?`since : +`+o.map(gO).join(` +`):" "+gO(o[0]):"as no adapter specified";throw new ne("There is no suitable adapter to dispatch the request "+s,"ERR_NOT_SUPPORT")}return i}const S2={getAdapter:gre,adapters:k0};function kp(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw new ds(null,e)}function bO(e){return kp(e),e.headers=Ht.from(e.headers),e.data=$p.call(e,e.transformRequest),["post","put","patch"].indexOf(e.method)!==-1&&e.headers.setContentType("application/x-www-form-urlencoded",!1),S2.getAdapter(e.adapter||_u.adapter,e)(e).then(function(n){return kp(e),n.data=$p.call(e,e.transformResponse,n),n.headers=Ht.from(n.headers),n},function(n){return v2(n)||(kp(e),n&&n.response&&(n.response.data=$p.call(e,e.transformResponse,n.response),n.response.headers=Ht.from(n.response.headers))),Promise.reject(n)})}const O2="1.13.2",Ph={};["object","boolean","number","function","string","symbol"].forEach((e,t)=>{Ph[e]=function(n){return typeof n===e||"a"+(t<1?"n ":" ")+e}});const xO={};Ph.transitional=function(t,r,n){function i(a,o){return"[Axios v"+O2+"] Transitional option '"+a+"'"+o+(n?". "+n:"")}return(a,o,s)=>{if(t===!1)throw new ne(i(o," has been removed"+(r?" in "+r:"")),ne.ERR_DEPRECATED);return r&&!xO[o]&&(xO[o]=!0,console.warn(i(o," has been deprecated since v"+r+" and will be removed in the near future"))),t?t(a,o,s):!0}};Ph.spelling=function(t){return(r,n)=>(console.warn(`${n} is likely a misspelling of ${t}`),!0)};function bre(e,t,r){if(typeof e!="object")throw new ne("options must be an object",ne.ERR_BAD_OPTION_VALUE);const n=Object.keys(e);let i=n.length;for(;i-- >0;){const a=n[i],o=t[a];if(o){const s=e[a],l=s===void 0||o(s,a,e);if(l!==!0)throw new ne("option "+a+" must be "+l,ne.ERR_BAD_OPTION_VALUE);continue}if(r!==!0)throw new ne("Unknown option "+a,ne.ERR_BAD_OPTION)}}const _c={assertOptions:bre,validators:Ph},Rr=_c.validators;let ra=class{constructor(t){this.defaults=t||{},this.interceptors={request:new lO,response:new lO}}async request(t,r){try{return await this._request(t,r)}catch(n){if(n instanceof Error){let i={};Error.captureStackTrace?Error.captureStackTrace(i):i=new Error;const a=i.stack?i.stack.replace(/^.+\n/,""):"";try{n.stack?a&&!String(n.stack).endsWith(a.replace(/^.+\n.+\n/,""))&&(n.stack+=` +`+a):n.stack=a}catch{}}throw n}}_request(t,r){typeof t=="string"?(r=r||{},r.url=t):r=t||{},r=ma(this.defaults,r);const{transitional:n,paramsSerializer:i,headers:a}=r;n!==void 0&&_c.assertOptions(n,{silentJSONParsing:Rr.transitional(Rr.boolean),forcedJSONParsing:Rr.transitional(Rr.boolean),clarifyTimeoutError:Rr.transitional(Rr.boolean)},!1),i!=null&&(M.isFunction(i)?r.paramsSerializer={serialize:i}:_c.assertOptions(i,{encode:Rr.function,serialize:Rr.function},!0)),r.allowAbsoluteUrls!==void 0||(this.defaults.allowAbsoluteUrls!==void 0?r.allowAbsoluteUrls=this.defaults.allowAbsoluteUrls:r.allowAbsoluteUrls=!0),_c.assertOptions(r,{baseUrl:Rr.spelling("baseURL"),withXsrfToken:Rr.spelling("withXSRFToken")},!0),r.method=(r.method||this.defaults.method||"get").toLowerCase();let o=a&&M.merge(a.common,a[r.method]);a&&M.forEach(["delete","get","head","post","put","patch","common"],p=>{delete a[p]}),r.headers=Ht.concat(o,a);const s=[];let l=!0;this.interceptors.request.forEach(function(m){typeof m.runWhen=="function"&&m.runWhen(r)===!1||(l=l&&m.synchronous,s.unshift(m.fulfilled,m.rejected))});const u=[];this.interceptors.response.forEach(function(m){u.push(m.fulfilled,m.rejected)});let f,c=0,d;if(!l){const p=[bO.bind(this),void 0];for(p.unshift(...s),p.push(...u),d=p.length,f=Promise.resolve(r);c{if(!n._listeners)return;let a=n._listeners.length;for(;a-- >0;)n._listeners[a](i);n._listeners=null}),this.promise.then=i=>{let a;const o=new Promise(s=>{n.subscribe(s),a=s}).then(i);return o.cancel=function(){n.unsubscribe(a)},o},t(function(a,o,s){n.reason||(n.reason=new ds(a,o,s),r(n.reason))})}throwIfRequested(){if(this.reason)throw this.reason}subscribe(t){if(this.reason){t(this.reason);return}this._listeners?this._listeners.push(t):this._listeners=[t]}unsubscribe(t){if(!this._listeners)return;const r=this._listeners.indexOf(t);r!==-1&&this._listeners.splice(r,1)}toAbortSignal(){const t=new AbortController,r=n=>{t.abort(n)};return this.subscribe(r),t.signal.unsubscribe=()=>this.unsubscribe(r),t.signal}static source(){let t;return{token:new _2(function(i){t=i}),cancel:t}}};function wre(e){return function(r){return e.apply(null,r)}}function Sre(e){return M.isObject(e)&&e.isAxiosError===!0}const xv={Continue:100,SwitchingProtocols:101,Processing:102,EarlyHints:103,Ok:200,Created:201,Accepted:202,NonAuthoritativeInformation:203,NoContent:204,ResetContent:205,PartialContent:206,MultiStatus:207,AlreadyReported:208,ImUsed:226,MultipleChoices:300,MovedPermanently:301,Found:302,SeeOther:303,NotModified:304,UseProxy:305,Unused:306,TemporaryRedirect:307,PermanentRedirect:308,BadRequest:400,Unauthorized:401,PaymentRequired:402,Forbidden:403,NotFound:404,MethodNotAllowed:405,NotAcceptable:406,ProxyAuthenticationRequired:407,RequestTimeout:408,Conflict:409,Gone:410,LengthRequired:411,PreconditionFailed:412,PayloadTooLarge:413,UriTooLong:414,UnsupportedMediaType:415,RangeNotSatisfiable:416,ExpectationFailed:417,ImATeapot:418,MisdirectedRequest:421,UnprocessableEntity:422,Locked:423,FailedDependency:424,TooEarly:425,UpgradeRequired:426,PreconditionRequired:428,TooManyRequests:429,RequestHeaderFieldsTooLarge:431,UnavailableForLegalReasons:451,InternalServerError:500,NotImplemented:501,BadGateway:502,ServiceUnavailable:503,GatewayTimeout:504,HttpVersionNotSupported:505,VariantAlsoNegotiates:506,InsufficientStorage:507,LoopDetected:508,NotExtended:510,NetworkAuthenticationRequired:511,WebServerIsDown:521,ConnectionTimedOut:522,OriginIsUnreachable:523,TimeoutOccurred:524,SslHandshakeFailed:525,InvalidSslCertificate:526};Object.entries(xv).forEach(([e,t])=>{xv[t]=e});function P2(e){const t=new ra(e),r=r2(ra.prototype.request,t);return M.extend(r,ra.prototype,t,{allOwnKeys:!0}),M.extend(r,t,null,{allOwnKeys:!0}),r.create=function(i){return P2(ma(e,i))},r}const Ke=P2(_u);Ke.Axios=ra;Ke.CanceledError=ds;Ke.CancelToken=xre;Ke.isCancel=v2;Ke.VERSION=O2;Ke.toFormData=_h;Ke.AxiosError=ne;Ke.Cancel=Ke.CanceledError;Ke.all=function(t){return Promise.all(t)};Ke.spread=wre;Ke.isAxiosError=Sre;Ke.mergeConfig=ma;Ke.AxiosHeaders=Ht;Ke.formToJSON=e=>y2(M.isHTMLForm(e)?new FormData(e):e);Ke.getAdapter=S2.getAdapter;Ke.HttpStatusCode=xv;Ke.default=Ke;const{Axios:zre,AxiosError:Wre,CanceledError:Hre,isCancel:qre,CancelToken:Kre,VERSION:Vre,all:Gre,Cancel:Xre,isAxiosError:Qre,spread:Yre,toFormData:Jre,AxiosHeaders:Zre,HttpStatusCode:ene,formToJSON:tne,getAdapter:rne,mergeConfig:nne}=Ke,A2="http://localhost:8000",de=Ke.create({baseURL:`${A2}/api`,headers:{"Content-Type":"application/json"}}),Pc={list:e=>de.get("/species",{params:e}),get:e=>de.get(`/species/${e}`),create:e=>de.post("/species",e),update:(e,t)=>de.put(`/species/${e}`,t),delete:e=>de.delete(`/species/${e}`),import:e=>{const t=new FormData;return t.append("file",e),de.post("/species/import",t,{headers:{"Content-Type":"multipart/form-data"}})},genera:()=>de.get("/species/genera/list")},ki={list:e=>de.get("/images",{params:e}),get:e=>de.get(`/images/${e}`),delete:e=>de.delete(`/images/${e}`),bulkDelete:e=>de.post("/images/bulk-delete",e),sources:()=>de.get("/images/sources"),licenses:()=>de.get("/images/licenses")},Ls={list:e=>de.get("/jobs",{params:e}),get:e=>de.get(`/jobs/${e}`),create:e=>de.post("/jobs",e),progress:e=>de.get(`/jobs/${e}/progress`),pause:e=>de.post(`/jobs/${e}/pause`),resume:e=>de.post(`/jobs/${e}/resume`),cancel:e=>de.post(`/jobs/${e}/cancel`)},el={list:e=>de.get("/exports",{params:e}),get:e=>de.get(`/exports/${e}`),create:e=>de.post("/exports",e),preview:e=>de.post("/exports/preview",e),progress:e=>de.get(`/exports/${e}/progress`),download:e=>`${A2}/api/exports/${e}/download`,delete:e=>de.delete(`/exports/${e}`)},wv={list:()=>de.get("/sources"),get:e=>de.get(`/sources/${e}`),update:(e,t)=>de.put(`/sources/${e}`,{source:e,...t}),test:e=>de.post(`/sources/${e}/test`),delete:e=>de.delete(`/sources/${e}`)},Ore={get:()=>de.get("/stats"),sources:()=>de.get("/stats/sources"),species:e=>de.get("/stats/species",{params:e})},wO=["#22c55e","#3b82f6","#f59e0b","#ef4444","#8b5cf6","#ec4899"];function sc({title:e,value:t,icon:r,color:n}){return O.jsx("div",{className:"bg-white rounded-lg shadow p-6",children:O.jsxs("div",{className:"flex items-center justify-between",children:[O.jsxs("div",{children:[O.jsx("p",{className:"text-sm text-gray-500",children:e}),O.jsx("p",{className:"text-2xl font-bold mt-1",children:t})]}),O.jsx("div",{className:`p-3 rounded-full ${n}`,children:O.jsx(r,{className:"w-6 h-6 text-white"})})]})})}function _re(){const{data:e,isLoading:t}=zr({queryKey:["stats"],queryFn:()=>Ore.get().then(i=>i.data),refetchInterval:5e3});if(t)return O.jsx("div",{className:"flex items-center justify-center h-64",children:O.jsx("div",{className:"animate-spin rounded-full h-8 w-8 border-b-2 border-green-600"})});if(!e)return O.jsx("div",{children:"Failed to load stats"});const r=e.sources.map(i=>({name:i.source,downloaded:i.downloaded,pending:i.pending,rejected:i.rejected})),n=e.licenses.map((i,a)=>({name:i.license,value:i.count,color:wO[a%wO.length]}));return O.jsxs("div",{className:"space-y-6",children:[O.jsx("h1",{className:"text-2xl font-bold",children:"Dashboard"}),O.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4",children:[O.jsx(sc,{title:"Total Species",value:e.total_species.toLocaleString(),icon:_g,color:"bg-green-500"}),O.jsx(sc,{title:"Downloaded Images",value:e.images_downloaded.toLocaleString(),icon:vA,color:"bg-blue-500"}),O.jsx(sc,{title:"Pending Images",value:e.images_pending.toLocaleString(),icon:So,color:"bg-yellow-500"}),O.jsx(sc,{title:"Disk Usage",value:`${e.disk_usage_mb.toFixed(1)} MB`,icon:XN,color:"bg-purple-500"})]}),O.jsxs("div",{className:"bg-white rounded-lg shadow p-6",children:[O.jsx("h2",{className:"text-lg font-semibold mb-4",children:"Jobs Status"}),O.jsxs("div",{className:"flex gap-6",children:[O.jsxs("div",{className:"flex items-center gap-2",children:[O.jsx("div",{className:"w-3 h-3 rounded-full bg-blue-500 animate-pulse"}),O.jsxs("span",{children:["Running: ",e.jobs.running]})]}),O.jsxs("div",{className:"flex items-center gap-2",children:[O.jsx(So,{className:"w-4 h-4 text-yellow-500"}),O.jsxs("span",{children:["Pending: ",e.jobs.pending]})]}),O.jsxs("div",{className:"flex items-center gap-2",children:[O.jsx(Od,{className:"w-4 h-4 text-green-500"}),O.jsxs("span",{children:["Completed: ",e.jobs.completed]})]}),O.jsxs("div",{className:"flex items-center gap-2",children:[O.jsx(Pg,{className:"w-4 h-4 text-red-500"}),O.jsxs("span",{children:["Failed: ",e.jobs.failed]})]})]})]}),O.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-2 gap-6",children:[O.jsxs("div",{className:"bg-white rounded-lg shadow p-6",children:[O.jsx("h2",{className:"text-lg font-semibold mb-4",children:"Images by Source"}),r.length>0?O.jsx(_1,{width:"100%",height:300,children:O.jsxs(Kee,{data:r,children:[O.jsx(gh,{dataKey:"name"}),O.jsx(bh,{}),O.jsx(Ar,{}),O.jsx(pn,{dataKey:"downloaded",fill:"#22c55e",name:"Downloaded"}),O.jsx(pn,{dataKey:"pending",fill:"#f59e0b",name:"Pending"}),O.jsx(pn,{dataKey:"rejected",fill:"#ef4444",name:"Rejected"})]})}):O.jsx("div",{className:"h-[300px] flex items-center justify-center text-gray-400",children:"No data yet"})]}),O.jsxs("div",{className:"bg-white rounded-lg shadow p-6",children:[O.jsx("h2",{className:"text-lg font-semibold mb-4",children:"Images by License"}),n.length>0?O.jsx(_1,{width:"100%",height:300,children:O.jsxs(Vee,{children:[O.jsx(Tn,{data:n,dataKey:"value",nameKey:"name",cx:"50%",cy:"50%",outerRadius:100,label:({name:i,percent:a})=>`${i} (${(a*100).toFixed(0)}%)`,children:n.map((i,a)=>O.jsx(Kd,{fill:i.color},a))}),O.jsx(Ar,{})]})}):O.jsx("div",{className:"h-[300px] flex items-center justify-center text-gray-400",children:"No data yet"})]})]}),O.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-2 gap-6",children:[O.jsxs("div",{className:"bg-white rounded-lg shadow p-6",children:[O.jsx("h2",{className:"text-lg font-semibold mb-4",children:"Top Species"}),O.jsxs("table",{className:"w-full",children:[O.jsx("thead",{children:O.jsxs("tr",{className:"text-left text-sm text-gray-500",children:[O.jsx("th",{className:"pb-2",children:"Species"}),O.jsx("th",{className:"pb-2 text-right",children:"Images"})]})}),O.jsxs("tbody",{children:[e.top_species.map(i=>O.jsxs("tr",{className:"border-t",children:[O.jsxs("td",{className:"py-2",children:[O.jsx("div",{className:"font-medium",children:i.scientific_name}),i.common_name&&O.jsx("div",{className:"text-sm text-gray-500",children:i.common_name})]}),O.jsx("td",{className:"py-2 text-right",children:i.image_count})]},i.id)),e.top_species.length===0&&O.jsx("tr",{children:O.jsx("td",{colSpan:2,className:"py-4 text-center text-gray-400",children:"No species yet"})})]})]})]}),O.jsxs("div",{className:"bg-white rounded-lg shadow p-6",children:[O.jsxs("h2",{className:"text-lg font-semibold mb-4 flex items-center gap-2",children:[O.jsx(Og,{className:"w-5 h-5 text-yellow-500"}),"Under-represented Species"]}),O.jsx("p",{className:"text-sm text-gray-500 mb-4",children:"Species with fewer than 100 images"}),O.jsxs("table",{className:"w-full",children:[O.jsx("thead",{children:O.jsxs("tr",{className:"text-left text-sm text-gray-500",children:[O.jsx("th",{className:"pb-2",children:"Species"}),O.jsx("th",{className:"pb-2 text-right",children:"Images"})]})}),O.jsxs("tbody",{children:[e.under_represented.map(i=>O.jsxs("tr",{className:"border-t",children:[O.jsxs("td",{className:"py-2",children:[O.jsx("div",{className:"font-medium",children:i.scientific_name}),i.common_name&&O.jsx("div",{className:"text-sm text-gray-500",children:i.common_name})]}),O.jsx("td",{className:"py-2 text-right text-yellow-600",children:i.image_count})]},i.id)),e.under_represented.length===0&&O.jsx("tr",{children:O.jsx("td",{colSpan:2,className:"py-4 text-center text-gray-400",children:"All species have 100+ images"})})]})]})]})]})]})}function Pre(){var w,x;const e=Pn(),t=j.useRef(null),[r,n]=j.useState(1),[i,a]=j.useState(""),[o,s]=j.useState([]),[l,u]=j.useState(!1),[f,c]=j.useState(!1),{data:d,isLoading:h}=zr({queryKey:["species",r,i],queryFn:()=>Pc.list({page:r,page_size:50,search:i||void 0}).then(S=>S.data)}),p=zt({mutationFn:S=>Pc.import(S),onSuccess:S=>{e.invalidateQueries({queryKey:["species"]}),alert(`Imported ${S.data.imported} species, skipped ${S.data.skipped}`)}}),m=zt({mutationFn:S=>Pc.delete(S),onSuccess:()=>{e.invalidateQueries({queryKey:["species"]})}}),y=zt({mutationFn:S=>Ls.create(S),onSuccess:()=>{c(!1),s([]),alert("Scrape job created!")}}),v=S=>{var P;const _=(P=S.target.files)==null?void 0:P[0];_&&p.mutate(_)},g=()=>{d&&(o.length===d.items.length?s([]):s(d.items.map(S=>S.id)))},b=S=>{s(_=>_.includes(S)?_.filter(P=>P!==S):[..._,S])};return O.jsxs("div",{className:"space-y-6",children:[O.jsxs("div",{className:"flex items-center justify-between",children:[O.jsx("h1",{className:"text-2xl font-bold",children:"Species"}),O.jsxs("div",{className:"flex gap-2",children:[O.jsxs("button",{onClick:()=>{var S;return(S=t.current)==null?void 0:S.click()},className:"flex items-center gap-2 px-4 py-2 bg-gray-100 rounded-lg hover:bg-gray-200",children:[O.jsx(eM,{className:"w-4 h-4"}),"Import CSV"]}),O.jsx("input",{ref:t,type:"file",accept:".csv",onChange:v,className:"hidden"}),O.jsxs("button",{onClick:()=>u(!0),className:"flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700",children:[O.jsx(JN,{className:"w-4 h-4"}),"Add Species"]})]})]}),O.jsxs("div",{className:"flex items-center justify-between",children:[O.jsxs("div",{className:"relative",children:[O.jsx(bA,{className:"absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400"}),O.jsx("input",{type:"text",placeholder:"Search species...",value:i,onChange:S=>{a(S.target.value),n(1)},className:"pl-10 pr-4 py-2 border rounded-lg w-80"})]}),o.length>0&&O.jsxs("div",{className:"flex items-center gap-4",children:[O.jsxs("span",{className:"text-sm text-gray-600",children:[o.length," selected"]}),O.jsxs("button",{onClick:()=>c(!0),className:"flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700",children:[O.jsx(ef,{className:"w-4 h-4"}),"Start Scrape"]})]})]}),O.jsx("div",{className:"bg-white rounded-lg shadow overflow-hidden",children:O.jsxs("table",{className:"w-full",children:[O.jsx("thead",{className:"bg-gray-50",children:O.jsxs("tr",{children:[O.jsx("th",{className:"px-4 py-3 text-left",children:O.jsx("input",{type:"checkbox",checked:(((w=d==null?void 0:d.items)==null?void 0:w.length)??0)>0&&o.length===(((x=d==null?void 0:d.items)==null?void 0:x.length)??0),onChange:g,className:"rounded"})}),O.jsx("th",{className:"px-4 py-3 text-left text-sm font-medium text-gray-600",children:"Scientific Name"}),O.jsx("th",{className:"px-4 py-3 text-left text-sm font-medium text-gray-600",children:"Common Name"}),O.jsx("th",{className:"px-4 py-3 text-left text-sm font-medium text-gray-600",children:"Genus"}),O.jsx("th",{className:"px-4 py-3 text-right text-sm font-medium text-gray-600",children:"Images"}),O.jsx("th",{className:"px-4 py-3 text-right text-sm font-medium text-gray-600",children:"Actions"})]})}),O.jsx("tbody",{children:h?O.jsx("tr",{children:O.jsx("td",{colSpan:6,className:"px-4 py-8 text-center text-gray-400",children:"Loading..."})}):(d==null?void 0:d.items.length)===0?O.jsx("tr",{children:O.jsx("td",{colSpan:6,className:"px-4 py-8 text-center text-gray-400",children:"No species found. Import a CSV to get started."})}):d==null?void 0:d.items.map(S=>O.jsxs("tr",{className:"border-t hover:bg-gray-50",children:[O.jsx("td",{className:"px-4 py-3",children:O.jsx("input",{type:"checkbox",checked:o.includes(S.id),onChange:()=>b(S.id),className:"rounded"})}),O.jsx("td",{className:"px-4 py-3 font-medium",children:S.scientific_name}),O.jsx("td",{className:"px-4 py-3 text-gray-600",children:S.common_name||"-"}),O.jsx("td",{className:"px-4 py-3 text-gray-600",children:S.genus||"-"}),O.jsx("td",{className:"px-4 py-3 text-right",children:O.jsx("span",{className:`inline-block px-2 py-1 rounded text-sm ${S.image_count>=100?"bg-green-100 text-green-700":S.image_count>0?"bg-yellow-100 text-yellow-700":"bg-gray-100 text-gray-600"}`,children:S.image_count})}),O.jsx("td",{className:"px-4 py-3 text-right",children:O.jsx("button",{onClick:()=>m.mutate(S.id),className:"p-1 text-red-500 hover:bg-red-50 rounded",children:O.jsx(tf,{className:"w-4 h-4"})})})]},S.id))})]})}),d&&d.pages>1&&O.jsxs("div",{className:"flex items-center justify-between",children:[O.jsxs("span",{className:"text-sm text-gray-600",children:["Showing ",(r-1)*50+1," to ",Math.min(r*50,d.total)," of"," ",d.total]}),O.jsxs("div",{className:"flex gap-2",children:[O.jsx("button",{onClick:()=>n(S=>Math.max(1,S-1)),disabled:r===1,className:"p-2 rounded border disabled:opacity-50",children:O.jsx(pA,{className:"w-4 h-4"})}),O.jsxs("span",{className:"px-4 py-2",children:["Page ",r," of ",d.pages]}),O.jsx("button",{onClick:()=>n(S=>Math.min(d.pages,S+1)),disabled:r===d.pages,className:"p-2 rounded border disabled:opacity-50",children:O.jsx(mA,{className:"w-4 h-4"})})]})]}),l&&O.jsx(Are,{onClose:()=>u(!1)}),f&&O.jsx(Ere,{selectedIds:o,onClose:()=>c(!1),onSubmit:S=>{y.mutate({name:`Scrape ${o.length} species from ${S}`,source:S,species_ids:o})}})]})}function Are({onClose:e}){const t=Pn(),[r,n]=j.useState({scientific_name:"",common_name:"",genus:"",family:""}),i=zt({mutationFn:()=>Pc.create(r),onSuccess:()=>{t.invalidateQueries({queryKey:["species"]}),e()}});return O.jsx("div",{className:"fixed inset-0 bg-black/50 flex items-center justify-center z-50",children:O.jsxs("div",{className:"bg-white rounded-lg p-6 w-full max-w-md",children:[O.jsx("h2",{className:"text-xl font-bold mb-4",children:"Add Species"}),O.jsxs("div",{className:"space-y-4",children:[O.jsxs("div",{children:[O.jsx("label",{className:"block text-sm font-medium mb-1",children:"Scientific Name *"}),O.jsx("input",{type:"text",value:r.scientific_name,onChange:a=>n({...r,scientific_name:a.target.value}),className:"w-full px-3 py-2 border rounded-lg",placeholder:"e.g. Monstera deliciosa"})]}),O.jsxs("div",{children:[O.jsx("label",{className:"block text-sm font-medium mb-1",children:"Common Name"}),O.jsx("input",{type:"text",value:r.common_name,onChange:a=>n({...r,common_name:a.target.value}),className:"w-full px-3 py-2 border rounded-lg",placeholder:"e.g. Swiss Cheese Plant"})]}),O.jsxs("div",{className:"grid grid-cols-2 gap-4",children:[O.jsxs("div",{children:[O.jsx("label",{className:"block text-sm font-medium mb-1",children:"Genus"}),O.jsx("input",{type:"text",value:r.genus,onChange:a=>n({...r,genus:a.target.value}),className:"w-full px-3 py-2 border rounded-lg",placeholder:"e.g. Monstera"})]}),O.jsxs("div",{children:[O.jsx("label",{className:"block text-sm font-medium mb-1",children:"Family"}),O.jsx("input",{type:"text",value:r.family,onChange:a=>n({...r,family:a.target.value}),className:"w-full px-3 py-2 border rounded-lg",placeholder:"e.g. Araceae"})]})]})]}),O.jsxs("div",{className:"flex justify-end gap-2 mt-6",children:[O.jsx("button",{onClick:e,className:"px-4 py-2 border rounded-lg hover:bg-gray-50",children:"Cancel"}),O.jsx("button",{onClick:()=>i.mutate(),disabled:!r.scientific_name,className:"px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50",children:"Add Species"})]})]})})}function Ere({selectedIds:e,onClose:t,onSubmit:r}){const[n,i]=j.useState("inaturalist"),a=[{value:"inaturalist",label:"iNaturalist/GBIF"},{value:"flickr",label:"Flickr"},{value:"wikimedia",label:"Wikimedia Commons"},{value:"trefle",label:"Trefle.io"}];return O.jsx("div",{className:"fixed inset-0 bg-black/50 flex items-center justify-center z-50",children:O.jsxs("div",{className:"bg-white rounded-lg p-6 w-full max-w-md",children:[O.jsx("h2",{className:"text-xl font-bold mb-4",children:"Start Scrape Job"}),O.jsxs("p",{className:"text-gray-600 mb-4",children:["Scrape images for ",e.length," selected species"]}),O.jsxs("div",{children:[O.jsx("label",{className:"block text-sm font-medium mb-2",children:"Select Source"}),O.jsx("div",{className:"space-y-2",children:a.map(o=>O.jsxs("label",{className:`flex items-center p-3 border rounded-lg cursor-pointer ${n===o.value?"border-green-500 bg-green-50":""}`,children:[O.jsx("input",{type:"radio",value:o.value,checked:n===o.value,onChange:s=>i(s.target.value),className:"mr-3"}),o.label]},o.value))})]}),O.jsxs("div",{className:"flex justify-end gap-2 mt-6",children:[O.jsx("button",{onClick:t,className:"px-4 py-2 border rounded-lg hover:bg-gray-50",children:"Cancel"}),O.jsx("button",{onClick:()=>r(n),className:"px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700",children:"Start Scrape"})]})]})})}function jre(){var b;const e=Pn(),[t,r]=j.useState(1),[n,i]=j.useState(""),[a,o]=j.useState({source:"",license:"",status:"downloaded",min_quality:void 0}),[s,l]=j.useState([]),[u,f]=j.useState(null),{data:c,isLoading:d}=zr({queryKey:["images",t,n,a],queryFn:()=>ki.list({page:t,page_size:48,search:n||void 0,source:a.source||void 0,license:a.license||void 0,status:a.status||void 0,min_quality:a.min_quality}).then(w=>w.data)}),{data:h}=zr({queryKey:["image-sources"],queryFn:()=>ki.sources().then(w=>w.data)}),{data:p}=zr({queryKey:["image-licenses"],queryFn:()=>ki.licenses().then(w=>w.data)}),{data:m}=zr({queryKey:["image",u],queryFn:()=>ki.get(u).then(w=>w.data),enabled:!!u}),y=zt({mutationFn:w=>ki.delete(w),onSuccess:()=>{e.invalidateQueries({queryKey:["images"]}),f(null)}}),v=zt({mutationFn:w=>ki.bulkDelete(w),onSuccess:()=>{e.invalidateQueries({queryKey:["images"]}),l([])}}),g=w=>{l(x=>x.includes(w)?x.filter(S=>S!==w):[...x,w])};return O.jsxs("div",{className:"space-y-6",children:[O.jsxs("div",{className:"flex items-center justify-between",children:[O.jsx("h1",{className:"text-2xl font-bold",children:"Images"}),s.length>0&&O.jsxs("button",{onClick:()=>v.mutate(s),className:"flex items-center gap-2 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700",children:[O.jsx(tf,{className:"w-4 h-4"}),"Delete ",s.length," images"]})]}),O.jsxs("div",{className:"flex flex-wrap gap-4",children:[O.jsxs("div",{className:"relative",children:[O.jsx(bA,{className:"absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400"}),O.jsx("input",{type:"text",placeholder:"Search species...",value:n,onChange:w=>{i(w.target.value),r(1)},className:"pl-10 pr-4 py-2 border rounded-lg w-64"})]}),O.jsxs("select",{value:a.source,onChange:w=>o({...a,source:w.target.value}),className:"px-3 py-2 border rounded-lg",children:[O.jsx("option",{value:"",children:"All Sources"}),h==null?void 0:h.map(w=>O.jsx("option",{value:w,children:w},w))]}),O.jsxs("select",{value:a.license,onChange:w=>o({...a,license:w.target.value}),className:"px-3 py-2 border rounded-lg",children:[O.jsx("option",{value:"",children:"All Licenses"}),p==null?void 0:p.map(w=>O.jsx("option",{value:w,children:w},w))]}),O.jsxs("select",{value:a.status,onChange:w=>o({...a,status:w.target.value}),className:"px-3 py-2 border rounded-lg",children:[O.jsx("option",{value:"",children:"All Status"}),O.jsx("option",{value:"downloaded",children:"Downloaded"}),O.jsx("option",{value:"pending",children:"Pending"}),O.jsx("option",{value:"rejected",children:"Rejected"})]})]}),d?O.jsx("div",{className:"flex items-center justify-center h-64",children:O.jsx("div",{className:"animate-spin rounded-full h-8 w-8 border-b-2 border-green-600"})}):(c==null?void 0:c.items.length)===0?O.jsxs("div",{className:"flex flex-col items-center justify-center h-64 text-gray-400",children:[O.jsx(GN,{className:"w-12 h-12 mb-4"}),O.jsx("p",{children:"No images found"})]}):O.jsx("div",{className:"grid grid-cols-2 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 gap-2",children:c==null?void 0:c.items.map(w=>O.jsxs("div",{className:`relative aspect-square bg-gray-100 rounded-lg overflow-hidden cursor-pointer group ${s.includes(w.id)?"ring-2 ring-green-500":""}`,onClick:()=>f(w.id),children:[w.local_path?O.jsx("img",{src:`/api/images/${w.id}/file`,alt:w.species_name||"",className:"w-full h-full object-cover",loading:"lazy"}):O.jsx("div",{className:"flex items-center justify-center h-full text-gray-400 text-xs",children:"Pending"}),O.jsx("div",{className:"absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors"}),O.jsx("div",{className:"absolute top-1 left-1",children:O.jsx("input",{type:"checkbox",checked:s.includes(w.id),onChange:x=>{x.stopPropagation(),g(w.id)},className:"rounded opacity-0 group-hover:opacity-100 checked:opacity-100"})}),O.jsx("div",{className:"absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/60 to-transparent p-1 opacity-0 group-hover:opacity-100 transition-opacity",children:O.jsx("p",{className:"text-white text-xs truncate",children:w.species_name})})]},w.id))}),c&&c.pages>1&&O.jsxs("div",{className:"flex items-center justify-between",children:[O.jsxs("span",{className:"text-sm text-gray-600",children:[c.total," images"]}),O.jsxs("div",{className:"flex gap-2",children:[O.jsx("button",{onClick:()=>r(w=>Math.max(1,w-1)),disabled:t===1,className:"p-2 rounded border disabled:opacity-50",children:O.jsx(pA,{className:"w-4 h-4"})}),O.jsxs("span",{className:"px-4 py-2",children:["Page ",t," of ",c.pages]}),O.jsx("button",{onClick:()=>r(w=>Math.min(c.pages,w+1)),disabled:t===c.pages,className:"p-2 rounded border disabled:opacity-50",children:O.jsx(mA,{className:"w-4 h-4"})})]})]}),u&&m&&O.jsx("div",{className:"fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-8",children:O.jsxs("div",{className:"bg-white rounded-lg w-full max-w-4xl max-h-full overflow-auto",children:[O.jsxs("div",{className:"flex justify-between items-center p-4 border-b",children:[O.jsx("h2",{className:"text-lg font-semibold",children:"Image Details"}),O.jsx("button",{onClick:()=>f(null),className:"p-1 hover:bg-gray-100 rounded",children:O.jsx(tM,{className:"w-5 h-5"})})]}),O.jsxs("div",{className:"grid grid-cols-2 gap-6 p-6",children:[O.jsx("div",{className:"aspect-square bg-gray-100 rounded-lg overflow-hidden",children:m.local_path?O.jsx("img",{src:`/api/images/${m.id}/file`,alt:m.species_name||"",className:"w-full h-full object-contain"}):O.jsx("div",{className:"flex items-center justify-center h-full text-gray-400",children:"Not downloaded"})}),O.jsxs("div",{className:"space-y-4",children:[O.jsxs("div",{children:[O.jsx("label",{className:"text-sm text-gray-500",children:"Species"}),O.jsx("p",{className:"font-medium",children:m.species_name})]}),O.jsxs("div",{children:[O.jsx("label",{className:"text-sm text-gray-500",children:"Source"}),O.jsx("p",{children:m.source})]}),O.jsxs("div",{children:[O.jsx("label",{className:"text-sm text-gray-500",children:"License"}),O.jsx("p",{children:m.license})]}),m.attribution&&O.jsxs("div",{children:[O.jsx("label",{className:"text-sm text-gray-500",children:"Attribution"}),O.jsx("p",{className:"text-sm",children:m.attribution})]}),O.jsxs("div",{className:"grid grid-cols-2 gap-4",children:[O.jsxs("div",{children:[O.jsx("label",{className:"text-sm text-gray-500",children:"Dimensions"}),O.jsxs("p",{children:[m.width||"?"," x ",m.height||"?"]})]}),O.jsxs("div",{children:[O.jsx("label",{className:"text-sm text-gray-500",children:"Quality Score"}),O.jsx("p",{children:((b=m.quality_score)==null?void 0:b.toFixed(1))||"N/A"})]})]}),O.jsxs("div",{children:[O.jsx("label",{className:"text-sm text-gray-500",children:"Status"}),O.jsx("p",{children:O.jsx("span",{className:`inline-block px-2 py-1 rounded text-sm ${m.status==="downloaded"?"bg-green-100 text-green-700":m.status==="pending"?"bg-yellow-100 text-yellow-700":"bg-red-100 text-red-700"}`,children:m.status})})]}),O.jsxs("div",{className:"flex gap-2 pt-4",children:[O.jsxs("a",{href:m.url,target:"_blank",rel:"noopener noreferrer",className:"flex items-center gap-2 px-4 py-2 border rounded-lg hover:bg-gray-50",children:[O.jsx(qN,{className:"w-4 h-4"}),"View Original"]}),O.jsxs("button",{onClick:()=>y.mutate(m.id),className:"flex items-center gap-2 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700",children:[O.jsx(tf,{className:"w-4 h-4"}),"Delete"]})]})]})]})]})})]})}function Tre(){const e=Pn(),{data:t,isLoading:r,refetch:n}=zr({queryKey:["jobs"],queryFn:()=>Ls.list({limit:100}).then(u=>u.data),refetchInterval:3e3}),i=zt({mutationFn:u=>Ls.pause(u),onSuccess:()=>e.invalidateQueries({queryKey:["jobs"]})}),a=zt({mutationFn:u=>Ls.resume(u),onSuccess:()=>e.invalidateQueries({queryKey:["jobs"]})}),o=zt({mutationFn:u=>Ls.cancel(u),onSuccess:()=>e.invalidateQueries({queryKey:["jobs"]})}),s=u=>{switch(u){case"running":return O.jsx(ef,{className:"w-4 h-4 text-blue-500"});case"pending":return O.jsx(So,{className:"w-4 h-4 text-yellow-500"});case"paused":return O.jsx(fx,{className:"w-4 h-4 text-gray-500"});case"completed":return O.jsx(Od,{className:"w-4 h-4 text-green-500"});case"failed":return O.jsx(Og,{className:"w-4 h-4 text-red-500"});default:return null}},l=u=>{switch(u){case"running":return"bg-blue-100 text-blue-700";case"pending":return"bg-yellow-100 text-yellow-700";case"paused":return"bg-gray-100 text-gray-700";case"completed":return"bg-green-100 text-green-700";case"failed":return"bg-red-100 text-red-700";default:return"bg-gray-100 text-gray-700"}};return O.jsxs("div",{className:"space-y-6",children:[O.jsxs("div",{className:"flex items-center justify-between",children:[O.jsx("h1",{className:"text-2xl font-bold",children:"Jobs"}),O.jsxs("button",{onClick:()=>n(),className:"flex items-center gap-2 px-4 py-2 border rounded-lg hover:bg-gray-50",children:[O.jsx(gA,{className:"w-4 h-4"}),"Refresh"]})]}),r?O.jsx("div",{className:"flex items-center justify-center h-64",children:O.jsx("div",{className:"animate-spin rounded-full h-8 w-8 border-b-2 border-green-600"})}):(t==null?void 0:t.items.length)===0?O.jsxs("div",{className:"bg-white rounded-lg shadow p-8 text-center text-gray-400",children:[O.jsx(So,{className:"w-12 h-12 mx-auto mb-4"}),O.jsx("p",{children:"No jobs yet"}),O.jsx("p",{className:"text-sm mt-2",children:"Select species and start a scrape job to get started"})]}):O.jsx("div",{className:"space-y-4",children:t==null?void 0:t.items.map(u=>O.jsx("div",{className:"bg-white rounded-lg shadow p-6",children:O.jsxs("div",{className:"flex items-start justify-between",children:[O.jsxs("div",{className:"flex-1",children:[O.jsxs("div",{className:"flex items-center gap-3",children:[s(u.status),O.jsx("h3",{className:"font-semibold",children:u.name}),O.jsx("span",{className:`px-2 py-0.5 rounded text-xs ${l(u.status)}`,children:u.status})]}),O.jsxs("div",{className:"mt-2 text-sm text-gray-600",children:[O.jsxs("span",{className:"mr-4",children:["Source: ",u.source]}),O.jsxs("span",{className:"mr-4",children:["Downloaded: ",u.images_downloaded]}),O.jsxs("span",{children:["Rejected: ",u.images_rejected]})]}),(u.status==="running"||u.status==="paused")&&u.progress_total>0&&O.jsxs("div",{className:"mt-4",children:[O.jsxs("div",{className:"flex justify-between text-sm text-gray-600 mb-1",children:[O.jsxs("span",{children:[u.progress_current," / ",u.progress_total," species"]}),O.jsxs("span",{children:[Math.round(u.progress_current/u.progress_total*100),"%"]})]}),O.jsx("div",{className:"h-2 bg-gray-200 rounded-full overflow-hidden",children:O.jsx("div",{className:`h-full rounded-full ${u.status==="running"?"bg-blue-500":"bg-gray-400"}`,style:{width:`${u.progress_current/u.progress_total*100}%`}})})]}),u.error_message&&O.jsxs("div",{className:"mt-2 text-sm text-red-600",children:["Error: ",u.error_message]}),O.jsxs("div",{className:"mt-2 text-xs text-gray-400",children:[u.started_at&&O.jsxs("span",{className:"mr-4",children:["Started: ",new Date(u.started_at).toLocaleString()]}),u.completed_at&&O.jsxs("span",{children:["Completed: ",new Date(u.completed_at).toLocaleString()]})]})]}),O.jsxs("div",{className:"flex gap-2 ml-4",children:[u.status==="running"&&O.jsx("button",{onClick:()=>i.mutate(u.id),className:"p-2 text-gray-600 hover:bg-gray-100 rounded",title:"Pause",children:O.jsx(fx,{className:"w-5 h-5"})}),u.status==="paused"&&O.jsx("button",{onClick:()=>a.mutate(u.id),className:"p-2 text-blue-600 hover:bg-blue-50 rounded",title:"Resume",children:O.jsx(ef,{className:"w-5 h-5"})}),(u.status==="running"||u.status==="paused"||u.status==="pending")&&O.jsx("button",{onClick:()=>o.mutate(u.id),className:"p-2 text-red-600 hover:bg-red-50 rounded",title:"Cancel",children:O.jsx(Pg,{className:"w-5 h-5"})})]})]})},u.id))})]})}function Cre(){const e=Pn(),[t,r]=j.useState(!1),{data:n,isLoading:i}=zr({queryKey:["exports"],queryFn:()=>el.list({limit:50}).then(l=>l.data),refetchInterval:5e3}),a=zt({mutationFn:l=>el.delete(l),onSuccess:()=>e.invalidateQueries({queryKey:["exports"]})}),o=l=>{switch(l){case"generating":return O.jsx(So,{className:"w-4 h-4 text-blue-500 animate-pulse"});case"completed":return O.jsx(Od,{className:"w-4 h-4 text-green-500"});case"failed":return O.jsx(Og,{className:"w-4 h-4 text-red-500"});default:return O.jsx(So,{className:"w-4 h-4 text-gray-400"})}},s=l=>l?l<1024?`${l} B`:l<1024*1024?`${(l/1024).toFixed(1)} KB`:l<1024*1024*1024?`${(l/1024/1024).toFixed(1)} MB`:`${(l/1024/1024/1024).toFixed(1)} GB`:"N/A";return O.jsxs("div",{className:"space-y-6",children:[O.jsxs("div",{className:"flex items-center justify-between",children:[O.jsx("h1",{className:"text-2xl font-bold",children:"Export Dataset"}),O.jsxs("button",{onClick:()=>r(!0),className:"flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700",children:[O.jsx(cx,{className:"w-4 h-4"}),"Create Export"]})]}),O.jsxs("div",{className:"bg-blue-50 border border-blue-200 rounded-lg p-4",children:[O.jsx("h3",{className:"font-medium text-blue-800",children:"Export Format"}),O.jsx("p",{className:"text-sm text-blue-700 mt-1",children:"Exports are created in Create ML-compatible format with Training and Testing folders. Each species has its own subfolder with images."})]}),i?O.jsx("div",{className:"flex items-center justify-center h-64",children:O.jsx("div",{className:"animate-spin rounded-full h-8 w-8 border-b-2 border-green-600"})}):(n==null?void 0:n.items.length)===0?O.jsxs("div",{className:"bg-white rounded-lg shadow p-8 text-center text-gray-400",children:[O.jsx(cx,{className:"w-12 h-12 mx-auto mb-4"}),O.jsx("p",{children:"No exports yet"}),O.jsx("p",{className:"text-sm mt-2",children:"Create an export to download your dataset for CoreML training"})]}):O.jsx("div",{className:"space-y-4",children:n==null?void 0:n.items.map(l=>O.jsx("div",{className:"bg-white rounded-lg shadow p-6",children:O.jsxs("div",{className:"flex items-start justify-between",children:[O.jsxs("div",{className:"flex-1",children:[O.jsxs("div",{className:"flex items-center gap-3",children:[o(l.status),O.jsx("h3",{className:"font-semibold",children:l.name})]}),O.jsxs("div",{className:"mt-2 grid grid-cols-4 gap-4 text-sm",children:[O.jsxs("div",{children:[O.jsx("span",{className:"text-gray-500",children:"Species:"})," ",l.species_count??"N/A"]}),O.jsxs("div",{children:[O.jsx("span",{className:"text-gray-500",children:"Images:"})," ",l.image_count??"N/A"]}),O.jsxs("div",{children:[O.jsx("span",{className:"text-gray-500",children:"Size:"})," ",s(l.file_size)]}),O.jsxs("div",{children:[O.jsx("span",{className:"text-gray-500",children:"Split:"})," ",Math.round(l.train_split*100),"% / ",Math.round((1-l.train_split)*100),"%"]})]}),l.error_message&&O.jsxs("div",{className:"mt-2 text-sm text-red-600",children:["Error: ",l.error_message]}),O.jsxs("div",{className:"mt-2 text-xs text-gray-400",children:["Created: ",new Date(l.created_at).toLocaleString(),l.completed_at&&O.jsxs("span",{className:"ml-4",children:["Completed: ",new Date(l.completed_at).toLocaleString()]})]})]}),O.jsxs("div",{className:"flex gap-2 ml-4",children:[l.status==="completed"&&O.jsxs("a",{href:el.download(l.id),className:"flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700",children:[O.jsx(yA,{className:"w-4 h-4"}),"Download"]}),O.jsx("button",{onClick:()=>a.mutate(l.id),className:"p-2 text-red-600 hover:bg-red-50 rounded",title:"Delete",children:O.jsx(tf,{className:"w-5 h-5"})})]})]})},l.id))}),t&&O.jsx($re,{onClose:()=>r(!1)})]})}function $re({onClose:e}){const t=Pn(),[r,n]=j.useState({name:`Export ${new Date().toLocaleDateString()}`,min_images:100,train_split:.8,licenses:[],min_quality:void 0}),{data:i}=zr({queryKey:["image-licenses"],queryFn:()=>ki.licenses().then(l=>l.data)}),a=zt({mutationFn:()=>el.preview({name:r.name,filter_criteria:{min_images_per_species:r.min_images,licenses:r.licenses.length>0?r.licenses:void 0,min_quality:r.min_quality},train_split:r.train_split})}),o=zt({mutationFn:()=>el.create({name:r.name,filter_criteria:{min_images_per_species:r.min_images,licenses:r.licenses.length>0?r.licenses:void 0,min_quality:r.min_quality},train_split:r.train_split}),onSuccess:()=>{t.invalidateQueries({queryKey:["exports"]}),e()}}),s=l=>{n(u=>({...u,licenses:u.licenses.includes(l)?u.licenses.filter(f=>f!==l):[...u.licenses,l]}))};return O.jsx("div",{className:"fixed inset-0 bg-black/50 flex items-center justify-center z-50",children:O.jsxs("div",{className:"bg-white rounded-lg p-6 w-full max-w-lg",children:[O.jsx("h2",{className:"text-xl font-bold mb-4",children:"Create Export"}),O.jsxs("div",{className:"space-y-4",children:[O.jsxs("div",{children:[O.jsx("label",{className:"block text-sm font-medium mb-1",children:"Export Name"}),O.jsx("input",{type:"text",value:r.name,onChange:l=>n({...r,name:l.target.value}),className:"w-full px-3 py-2 border rounded-lg"})]}),O.jsxs("div",{children:[O.jsx("label",{className:"block text-sm font-medium mb-1",children:"Minimum Images per Species"}),O.jsx("input",{type:"number",value:r.min_images,onChange:l=>n({...r,min_images:parseInt(l.target.value)||0}),className:"w-full px-3 py-2 border rounded-lg",min:1}),O.jsx("p",{className:"text-xs text-gray-500 mt-1",children:"Species with fewer images will be excluded"})]}),O.jsxs("div",{children:[O.jsx("label",{className:"block text-sm font-medium mb-1",children:"Train/Test Split"}),O.jsxs("div",{className:"flex items-center gap-4",children:[O.jsx("input",{type:"range",value:r.train_split,onChange:l=>n({...r,train_split:parseFloat(l.target.value)}),min:.5,max:.95,step:.05,className:"flex-1"}),O.jsxs("span",{className:"text-sm w-20 text-right",children:[Math.round(r.train_split*100),"% /"," ",Math.round((1-r.train_split)*100),"%"]})]})]}),O.jsxs("div",{children:[O.jsx("label",{className:"block text-sm font-medium mb-2",children:"Filter by License (optional)"}),O.jsx("div",{className:"flex flex-wrap gap-2",children:i==null?void 0:i.map(l=>O.jsx("button",{onClick:()=>s(l),className:`px-3 py-1 rounded-full text-sm ${r.licenses.includes(l)?"bg-green-100 text-green-700 border-green-300":"bg-gray-100 text-gray-600"} border`,children:l},l))}),r.licenses.length===0&&O.jsx("p",{className:"text-xs text-gray-500 mt-1",children:"All licenses will be included"})]}),a.data&&O.jsxs("div",{className:"bg-gray-50 rounded-lg p-4",children:[O.jsx("h4",{className:"font-medium mb-2",children:"Preview"}),O.jsxs("div",{className:"grid grid-cols-3 gap-4 text-sm",children:[O.jsxs("div",{children:[O.jsx("span",{className:"text-gray-500",children:"Species:"})," ",a.data.data.species_count]}),O.jsxs("div",{children:[O.jsx("span",{className:"text-gray-500",children:"Images:"})," ",a.data.data.image_count]}),O.jsxs("div",{children:[O.jsx("span",{className:"text-gray-500",children:"Est. Size:"})," ",a.data.data.estimated_size_mb.toFixed(0)," MB"]})]})]})]}),O.jsxs("div",{className:"flex justify-between mt-6",children:[O.jsx("button",{onClick:()=>a.mutate(),className:"px-4 py-2 border rounded-lg hover:bg-gray-50",children:"Preview"}),O.jsxs("div",{className:"flex gap-2",children:[O.jsx("button",{onClick:e,className:"px-4 py-2 border rounded-lg hover:bg-gray-50",children:"Cancel"}),O.jsx("button",{onClick:()=>o.mutate(),disabled:!r.name,className:"px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50",children:"Create Export"})]})]})]})})}function kre(){const[e,t]=j.useState(null),{data:r,isLoading:n}=zr({queryKey:["sources"],queryFn:()=>wv.list().then(i=>i.data)});return O.jsxs("div",{className:"space-y-6",children:[O.jsx("h1",{className:"text-2xl font-bold",children:"Settings"}),O.jsxs("div",{className:"bg-white rounded-lg shadow",children:[O.jsxs("div",{className:"px-6 py-4 border-b",children:[O.jsxs("h2",{className:"text-lg font-semibold flex items-center gap-2",children:[O.jsx(QN,{className:"w-5 h-5"}),"API Keys"]}),O.jsx("p",{className:"text-sm text-gray-500 mt-1",children:"Configure API keys for each data source"})]}),n?O.jsx("div",{className:"p-6 text-center",children:O.jsx(gA,{className:"w-6 h-6 animate-spin mx-auto text-gray-400"})}):O.jsx("div",{className:"divide-y",children:r==null?void 0:r.map(i=>O.jsx(Nre,{source:i,isEditing:e===i.name,onEdit:()=>t(i.name),onClose:()=>t(null)},i.name))})]}),O.jsxs("div",{className:"bg-yellow-50 border border-yellow-200 rounded-lg p-4",children:[O.jsx("h3",{className:"font-medium text-yellow-800",children:"Rate Limits"}),O.jsxs("ul",{className:"text-sm text-yellow-700 mt-2 space-y-1 list-disc list-inside",children:[O.jsx("li",{children:"iNaturalist: 1 req/sec, 10k/day, 5GB/hr media downloads"}),O.jsx("li",{children:"Flickr: 3600 req/hr with API key"}),O.jsx("li",{children:"Wikimedia: Generous limits, no key required"}),O.jsx("li",{children:"Trefle: Rate limits apply with free tier"})]})]})]})}function Nre({source:e,isEditing:t,onEdit:r,onClose:n}){const i=Pn(),[a,o]=j.useState(!1),[s,l]=j.useState({api_key:"",api_secret:"",rate_limit_per_sec:e.rate_limit_per_sec,enabled:e.enabled}),[u,f]=j.useState(null),c=zt({mutationFn:()=>wv.update(e.name,{api_key:s.api_key,api_secret:s.api_secret||void 0,rate_limit_per_sec:s.rate_limit_per_sec,enabled:s.enabled}),onSuccess:()=>{i.invalidateQueries({queryKey:["sources"]}),n()}}),d=zt({mutationFn:()=>wv.test(e.name),onSuccess:h=>{f({status:h.data.status,message:h.data.message})},onError:h=>{var p,m;f({status:"error",message:((m=(p=h.response)==null?void 0:p.data)==null?void 0:m.message)||"Connection failed"})}});return t?O.jsxs("div",{className:"p-6 bg-gray-50",children:[O.jsxs("div",{className:"flex items-center justify-between mb-4",children:[O.jsx("h3",{className:"font-medium",children:e.label}),O.jsx("button",{onClick:n,className:"text-gray-500 hover:text-gray-700",children:"Cancel"})]}),O.jsxs("div",{className:"space-y-4",children:[O.jsxs("div",{children:[O.jsx("label",{className:"block text-sm font-medium mb-1",children:"API Key"}),O.jsxs("div",{className:"relative",children:[O.jsx("input",{type:a?"text":"password",value:s.api_key,onChange:h=>l({...s,api_key:h.target.value}),placeholder:e.api_key_masked||"Enter API key",className:"w-full px-3 py-2 border rounded-lg pr-10"}),O.jsx("button",{type:"button",onClick:()=>o(!a),className:"absolute right-2 top-1/2 -translate-y-1/2 text-gray-400",children:a?O.jsx(KN,{className:"w-4 h-4"}):O.jsx(VN,{className:"w-4 h-4"})})]})]}),e.requires_secret&&O.jsxs("div",{children:[O.jsx("label",{className:"block text-sm font-medium mb-1",children:"API Secret"}),O.jsx("input",{type:"password",value:s.api_secret,onChange:h=>l({...s,api_secret:h.target.value}),placeholder:e.has_secret?"••••••••":"Enter API secret",className:"w-full px-3 py-2 border rounded-lg"})]}),O.jsxs("div",{children:[O.jsx("label",{className:"block text-sm font-medium mb-1",children:"Rate Limit (requests/sec)"}),O.jsx("input",{type:"number",value:s.rate_limit_per_sec,onChange:h=>l({...s,rate_limit_per_sec:parseFloat(h.target.value)||1}),className:"w-full px-3 py-2 border rounded-lg",min:.1,max:10,step:.1})]}),O.jsxs("div",{className:"flex items-center gap-2",children:[O.jsx("input",{type:"checkbox",id:"enabled",checked:s.enabled,onChange:h=>l({...s,enabled:h.target.checked}),className:"rounded"}),O.jsx("label",{htmlFor:"enabled",className:"text-sm",children:"Enable this source"})]}),u&&O.jsx("div",{className:`p-3 rounded-lg ${u.status==="success"?"bg-green-50 text-green-700":"bg-red-50 text-red-700"}`,children:u.message}),O.jsxs("div",{className:"flex justify-between",children:[e.configured&&O.jsx("button",{onClick:()=>d.mutate(),disabled:d.isPending,className:"px-4 py-2 border rounded-lg hover:bg-white",children:d.isPending?"Testing...":"Test Connection"}),O.jsx("button",{onClick:()=>c.mutate(),disabled:!s.api_key&&!e.configured,className:"px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 ml-auto",children:"Save"})]})]})]}):O.jsxs("div",{className:"px-6 py-4 flex items-center justify-between",children:[O.jsxs("div",{className:"flex items-center gap-4",children:[O.jsx("div",{className:`w-2 h-2 rounded-full ${e.configured&&e.enabled?"bg-green-500":e.configured?"bg-yellow-500":"bg-gray-300"}`}),O.jsxs("div",{children:[O.jsx("h3",{className:"font-medium",children:e.label}),O.jsx("p",{className:"text-sm text-gray-500",children:e.configured?`Key: ${e.api_key_masked}`:"Not configured"})]})]}),O.jsxs("div",{className:"flex items-center gap-4",children:[e.configured&&O.jsx("span",{className:`flex items-center gap-1 text-sm ${e.enabled?"text-green-600":"text-gray-400"}`,children:e.enabled?O.jsxs(O.Fragment,{children:[O.jsx(Od,{className:"w-4 h-4"}),"Enabled"]}):O.jsxs(O.Fragment,{children:[O.jsx(Pg,{className:"w-4 h-4"}),"Disabled"]})}),O.jsx("button",{onClick:r,className:"px-3 py-1 text-sm border rounded hover:bg-gray-50",children:e.configured?"Edit":"Configure"})]})]})}const Mre=[{to:"/",icon:YN,label:"Dashboard"},{to:"/species",icon:_g,label:"Species"},{to:"/images",icon:vA,label:"Images"},{to:"/jobs",icon:ef,label:"Jobs"},{to:"/export",icon:yA,label:"Export"},{to:"/settings",icon:ZN,label:"Settings"}];function Ire(){return O.jsxs("aside",{className:"w-64 bg-white border-r border-gray-200 min-h-screen",children:[O.jsx("div",{className:"p-4 border-b border-gray-200",children:O.jsxs("h1",{className:"text-xl font-bold text-green-600 flex items-center gap-2",children:[O.jsx(_g,{className:"w-6 h-6"}),"PlantScraper"]})}),O.jsx("nav",{className:"p-4",children:O.jsx("ul",{className:"space-y-2",children:Mre.map(e=>O.jsx("li",{children:O.jsxs(BN,{to:e.to,className:({isActive:t})=>oe("flex items-center gap-3 px-3 py-2 rounded-lg transition-colors",t?"bg-green-50 text-green-700":"text-gray-600 hover:bg-gray-100"),children:[O.jsx(e.icon,{className:"w-5 h-5"}),e.label]})},e.to))})})]})}function Rre(){return O.jsx(IN,{children:O.jsxs("div",{className:"flex min-h-screen",children:[O.jsx(Ire,{}),O.jsx("main",{className:"flex-1 p-8",children:O.jsxs(EN,{children:[O.jsx(ji,{path:"/",element:O.jsx(_re,{})}),O.jsx(ji,{path:"/species",element:O.jsx(Pre,{})}),O.jsx(ji,{path:"/images",element:O.jsx(jre,{})}),O.jsx(ji,{path:"/jobs",element:O.jsx(Tre,{})}),O.jsx(ji,{path:"/export",element:O.jsx(Cre,{})}),O.jsx(ji,{path:"/settings",element:O.jsx(kre,{})})]})})]})})}const Dre=new _k({defaultOptions:{queries:{refetchOnWindowFocus:!1,retry:1}}});Np.createRoot(document.getElementById("root")).render(O.jsx(T.StrictMode,{children:O.jsx(Pk,{client:Dre,children:O.jsx(Rre,{})})})); diff --git a/frontend/dist/assets/index-uHzGA3u6.css b/frontend/dist/assets/index-uHzGA3u6.css new file mode 100644 index 0000000..af569a3 --- /dev/null +++ b/frontend/dist/assets/index-uHzGA3u6.css @@ -0,0 +1 @@ +*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.bottom-0{bottom:0}.left-0{left:0}.left-1{left:.25rem}.left-3{left:.75rem}.right-0{right:0}.right-2{right:.5rem}.top-1{top:.25rem}.top-1\/2{top:50%}.z-50{z-index:50}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.ml-4{margin-left:1rem}.ml-auto{margin-left:auto}.mr-3{margin-right:.75rem}.mr-4{margin-right:1rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.table{display:table}.grid{display:grid}.hidden{display:none}.aspect-square{aspect-ratio:1 / 1}.h-12{height:3rem}.h-2{height:.5rem}.h-3{height:.75rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-8{height:2rem}.h-\[300px\]{height:300px}.h-full{height:100%}.max-h-full{max-height:100%}.min-h-screen{min-height:100vh}.w-12{width:3rem}.w-2{width:.5rem}.w-20{width:5rem}.w-3{width:.75rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-8{width:2rem}.w-80{width:20rem}.w-full{width:100%}.max-w-4xl{max-width:56rem}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-blue-200{--tw-border-opacity: 1;border-color:rgb(191 219 254 / var(--tw-border-opacity, 1))}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity, 1))}.border-green-300{--tw-border-opacity: 1;border-color:rgb(134 239 172 / var(--tw-border-opacity, 1))}.border-green-500{--tw-border-opacity: 1;border-color:rgb(34 197 94 / var(--tw-border-opacity, 1))}.border-green-600{--tw-border-opacity: 1;border-color:rgb(22 163 74 / var(--tw-border-opacity, 1))}.border-yellow-200{--tw-border-opacity: 1;border-color:rgb(254 240 138 / var(--tw-border-opacity, 1))}.bg-black\/0{background-color:#0000}.bg-black\/50{background-color:#00000080}.bg-blue-100{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity, 1))}.bg-blue-50{--tw-bg-opacity: 1;background-color:rgb(239 246 255 / var(--tw-bg-opacity, 1))}.bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity, 1))}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.bg-gray-200{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.bg-gray-300{--tw-bg-opacity: 1;background-color:rgb(209 213 219 / var(--tw-bg-opacity, 1))}.bg-gray-400{--tw-bg-opacity: 1;background-color:rgb(156 163 175 / var(--tw-bg-opacity, 1))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity, 1))}.bg-green-50{--tw-bg-opacity: 1;background-color:rgb(240 253 244 / var(--tw-bg-opacity, 1))}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-green-600{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity, 1))}.bg-purple-500{--tw-bg-opacity: 1;background-color:rgb(168 85 247 / var(--tw-bg-opacity, 1))}.bg-red-100{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity, 1))}.bg-red-50{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.bg-red-600{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-yellow-100{--tw-bg-opacity: 1;background-color:rgb(254 249 195 / var(--tw-bg-opacity, 1))}.bg-yellow-50{--tw-bg-opacity: 1;background-color:rgb(254 252 232 / var(--tw-bg-opacity, 1))}.bg-yellow-500{--tw-bg-opacity: 1;background-color:rgb(234 179 8 / var(--tw-bg-opacity, 1))}.bg-gradient-to-t{background-image:linear-gradient(to top,var(--tw-gradient-stops))}.from-black\/60{--tw-gradient-from: rgb(0 0 0 / .6) var(--tw-gradient-from-position);--tw-gradient-to: rgb(0 0 0 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.to-transparent{--tw-gradient-to: transparent var(--tw-gradient-to-position)}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-2{padding-bottom:.5rem}.pl-10{padding-left:2.5rem}.pr-10{padding-right:2.5rem}.pr-4{padding-right:1rem}.pt-4{padding-top:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.text-2xl{font-size:1.5rem;line-height:2rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.text-blue-500{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity, 1))}.text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity, 1))}.text-blue-700{--tw-text-opacity: 1;color:rgb(29 78 216 / var(--tw-text-opacity, 1))}.text-blue-800{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-green-600{--tw-text-opacity: 1;color:rgb(22 163 74 / var(--tw-text-opacity, 1))}.text-green-700{--tw-text-opacity: 1;color:rgb(21 128 61 / var(--tw-text-opacity, 1))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.text-red-700{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-500{--tw-text-opacity: 1;color:rgb(234 179 8 / var(--tw-text-opacity, 1))}.text-yellow-600{--tw-text-opacity: 1;color:rgb(202 138 4 / var(--tw-text-opacity, 1))}.text-yellow-700{--tw-text-opacity: 1;color:rgb(161 98 7 / var(--tw-text-opacity, 1))}.text-yellow-800{--tw-text-opacity: 1;color:rgb(133 77 14 / var(--tw-text-opacity, 1))}.opacity-0{opacity:0}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.ring-2{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-green-500{--tw-ring-opacity: 1;--tw-ring-color: rgb(34 197 94 / var(--tw-ring-opacity, 1))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}body{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity, 1))}.checked\:opacity-100:checked{opacity:1}.hover\:bg-blue-50:hover{--tw-bg-opacity: 1;background-color:rgb(239 246 255 / var(--tw-bg-opacity, 1))}.hover\:bg-blue-700:hover{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-100:hover{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-200:hover{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-50:hover{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.hover\:bg-green-700:hover{--tw-bg-opacity: 1;background-color:rgb(21 128 61 / var(--tw-bg-opacity, 1))}.hover\:bg-red-50:hover{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.hover\:bg-red-700:hover{--tw-bg-opacity: 1;background-color:rgb(185 28 28 / var(--tw-bg-opacity, 1))}.hover\:bg-white:hover{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.hover\:text-gray-700:hover{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:bg-black\/20{background-color:#0003}.group:hover .group-hover\:opacity-100{opacity:1}@media (min-width: 640px){.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width: 768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}}@media (min-width: 1024px){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-8{grid-template-columns:repeat(8,minmax(0,1fr))}} diff --git a/frontend/dist/index.html b/frontend/dist/index.html new file mode 100644 index 0000000..bda2ae9 --- /dev/null +++ b/frontend/dist/index.html @@ -0,0 +1,14 @@ + + + + + + + PlantGuideScraper + + + + +
+ + diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..57f562b --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + PlantGuideScraper + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..257b45f --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,31 @@ +{ + "name": "plant-scraper-frontend", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.21.0", + "@tanstack/react-query": "^5.17.0", + "axios": "^1.6.0", + "lucide-react": "^0.303.0", + "recharts": "^2.10.0", + "clsx": "^2.1.0" + }, + "devDependencies": { + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@vitejs/plugin-react": "^4.2.0", + "autoprefixer": "^10.4.16", + "postcss": "^8.4.32", + "tailwindcss": "^3.4.0", + "typescript": "^5.3.0", + "vite": "^5.0.0" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..bf21d79 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,81 @@ +import { BrowserRouter, Routes, Route, NavLink } from 'react-router-dom' +import { + LayoutDashboard, + Leaf, + Image, + Play, + Download, + Settings, +} from 'lucide-react' +import { clsx } from 'clsx' + +import Dashboard from './pages/Dashboard' +import Species from './pages/Species' +import Images from './pages/Images' +import Jobs from './pages/Jobs' +import Export from './pages/Export' +import SettingsPage from './pages/Settings' + +const navItems = [ + { to: '/', icon: LayoutDashboard, label: 'Dashboard' }, + { to: '/species', icon: Leaf, label: 'Species' }, + { to: '/images', icon: Image, label: 'Images' }, + { to: '/jobs', icon: Play, label: 'Jobs' }, + { to: '/export', icon: Download, label: 'Export' }, + { to: '/settings', icon: Settings, label: 'Settings' }, +] + +function Sidebar() { + return ( + + ) +} + +export default function App() { + return ( + +
+ +
+ + } /> + } /> + } /> + } /> + } /> + } /> + +
+
+
+ ) +} diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..ba22ad5 --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,275 @@ +import axios from 'axios' + +const API_URL = import.meta.env.VITE_API_URL || '' + +export const api = axios.create({ + baseURL: `${API_URL}/api`, + headers: { + 'Content-Type': 'application/json', + }, +}) + +// Types +export interface Species { + id: number + scientific_name: string + common_name: string | null + genus: string | null + family: string | null + created_at: string + image_count: number +} + +export interface SpeciesListResponse { + items: Species[] + total: number + page: number + page_size: number + pages: number +} + +export interface Image { + id: number + species_id: number + species_name: string | null + source: string + source_id: string | null + url: string + local_path: string | null + license: string + attribution: string | null + width: number | null + height: number | null + quality_score: number | null + status: string + created_at: string +} + +export interface ImageListResponse { + items: Image[] + total: number + page: number + page_size: number + pages: number +} + +export interface Job { + id: number + name: string + source: string + species_filter: string | null + status: string + progress_current: number + progress_total: number + images_downloaded: number + images_rejected: number + started_at: string | null + completed_at: string | null + error_message: string | null + created_at: string +} + +export interface JobListResponse { + items: Job[] + total: number +} + +export interface JobProgress { + status: string + progress_current: number + progress_total: number + current_species?: string +} + +export interface Export { + id: number + name: string + filter_criteria: string | null + train_split: number + status: string + file_path: string | null + file_size: number | null + species_count: number | null + image_count: number | null + created_at: string + completed_at: string | null + error_message: string | null +} + +export interface SourceConfig { + name: string + label: string + requires_secret: boolean + auth_type: 'none' | 'api_key' | 'api_key_secret' | 'oauth' + configured: boolean + enabled: boolean + api_key_masked: string | null + has_secret: boolean + has_access_token: boolean + rate_limit_per_sec: number + default_rate: number +} + +export interface Stats { + total_species: number + total_images: number + images_downloaded: number + images_pending: number + images_rejected: number + disk_usage_mb: number + sources: Array<{ + source: string + image_count: number + downloaded: number + pending: number + rejected: number + }> + licenses: Array<{ + license: string + count: number + }> + jobs: { + running: number + pending: number + completed: number + failed: number + } + top_species: Array<{ + id: number + scientific_name: string + common_name: string | null + image_count: number + }> + under_represented: Array<{ + id: number + scientific_name: string + common_name: string | null + image_count: number + }> +} + +// API functions +export const speciesApi = { + list: (params?: { page?: number; page_size?: number; search?: string; genus?: string; has_images?: boolean; max_images?: number; min_images?: number }) => + api.get('/species', { params }), + get: (id: number) => api.get(`/species/${id}`), + create: (data: { scientific_name: string; common_name?: string; genus?: string; family?: string }) => + api.post('/species', data), + update: (id: number, data: Partial) => api.put(`/species/${id}`, data), + delete: (id: number) => api.delete(`/species/${id}`), + import: (file: File) => { + const formData = new FormData() + formData.append('file', file) + return api.post('/species/import', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }) + }, + importJson: (file: File) => { + const formData = new FormData() + formData.append('file', file) + return api.post('/species/import-json', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }) + }, + genera: () => api.get('/species/genera/list'), +} + +export interface ImportScanResult { + available: boolean + message?: string + sources: Array<{ + name: string + species_count: number + image_count: number + }> + total_images: number + matched_species: number + unmatched_species: string[] +} + +export interface ImportResult { + imported: number + skipped: number + errors: string[] +} + +export const imagesApi = { + list: (params?: { + page?: number + page_size?: number + species_id?: number + source?: string + license?: string + status?: string + min_quality?: number + search?: string + }) => api.get('/images', { params }), + get: (id: number) => api.get(`/images/${id}`), + delete: (id: number) => api.delete(`/images/${id}`), + bulkDelete: (ids: number[]) => api.post('/images/bulk-delete', ids), + sources: () => api.get('/images/sources'), + licenses: () => api.get('/images/licenses'), + processPending: (source?: string) => + api.post<{ pending_count: number; task_id: string }>('/images/process-pending', null, { + params: source ? { source } : undefined, + }), + processPendingStatus: (taskId: string) => + api.get<{ task_id: string; state: string; queued?: number; total?: number }>( + `/images/process-pending/status/${taskId}` + ), + scanImports: () => api.get('/images/import/scan'), + runImport: (moveFiles: boolean = false) => + api.post('/images/import/run', null, { params: { move_files: moveFiles } }), +} + +export const jobsApi = { + list: (params?: { status?: string; source?: string; limit?: number }) => + api.get('/jobs', { params }), + get: (id: number) => api.get(`/jobs/${id}`), + create: (data: { name: string; source: string; species_ids?: number[]; only_without_images?: boolean; max_images?: number }) => + api.post('/jobs', data), + progress: (id: number) => api.get(`/jobs/${id}/progress`), + pause: (id: number) => api.post(`/jobs/${id}/pause`), + resume: (id: number) => api.post(`/jobs/${id}/resume`), + cancel: (id: number) => api.post(`/jobs/${id}/cancel`), +} + +export const exportsApi = { + list: (params?: { limit?: number }) => api.get('/exports', { params }), + get: (id: number) => api.get(`/exports/${id}`), + create: (data: { + name: string + filter_criteria: { + min_images_per_species: number + licenses?: string[] + min_quality?: number + species_ids?: number[] + } + train_split: number + }) => api.post('/exports', data), + preview: (data: any) => api.post('/exports/preview', data), + progress: (id: number) => api.get(`/exports/${id}/progress`), + download: (id: number) => `${API_URL}/api/exports/${id}/download`, + delete: (id: number) => api.delete(`/exports/${id}`), +} + +export const sourcesApi = { + list: () => api.get('/sources'), + get: (source: string) => api.get(`/sources/${source}`), + update: (source: string, data: { + api_key?: string + api_secret?: string + access_token?: string + rate_limit_per_sec?: number + enabled?: boolean + }) => api.put(`/sources/${source}`, { source, ...data }), + test: (source: string) => api.post(`/sources/${source}/test`), + delete: (source: string) => api.delete(`/sources/${source}`), +} + +export const statsApi = { + get: () => api.get('/stats'), + sources: () => api.get('/stats/sources'), + species: (params?: { min_count?: number; max_count?: number }) => + api.get('/stats/species', { params }), +} diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..c2f490f --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,7 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + @apply bg-gray-50 text-gray-900; +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..b7bc7e2 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,22 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import App from './App' +import './index.css' + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + retry: 1, + }, + }, +}) + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + , +) diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx new file mode 100644 index 0000000..2348fa9 --- /dev/null +++ b/frontend/src/pages/Dashboard.tsx @@ -0,0 +1,413 @@ +import { useState } from 'react' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { + Leaf, + Image, + HardDrive, + Clock, + CheckCircle, + XCircle, + AlertCircle, +} from 'lucide-react' +import { + BarChart, + Bar, + XAxis, + YAxis, + Tooltip, + ResponsiveContainer, + PieChart, + Pie, + Cell, +} from 'recharts' +import { statsApi, imagesApi } from '../api/client' + +const COLORS = ['#22c55e', '#3b82f6', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899'] + +function StatCard({ + title, + value, + icon: Icon, + color, +}: { + title: string + value: string | number + icon: React.ElementType + color: string +}) { + return ( +
+
+
+

{title}

+

{value}

+
+
+ +
+
+
+ ) +} + +export default function Dashboard() { + const queryClient = useQueryClient() + + const [processingTaskId, setProcessingTaskId] = useState(null) + + const processPendingMutation = useMutation({ + mutationFn: () => imagesApi.processPending(), + onSuccess: (res) => { + setProcessingTaskId(res.data.task_id) + }, + }) + + // Poll task status while processing + const { data: taskStatus } = useQuery({ + queryKey: ['process-pending-status', processingTaskId], + queryFn: async () => { + const res = await imagesApi.processPendingStatus(processingTaskId!) + if (res.data.state === 'SUCCESS' || res.data.state === 'FAILURE') { + // Task finished - clear tracking and refresh stats + setTimeout(() => { + setProcessingTaskId(null) + queryClient.invalidateQueries({ queryKey: ['stats'] }) + }, 0) + } + return res.data + }, + enabled: !!processingTaskId, + refetchInterval: (query) => { + const state = query.state.data?.state + if (state === 'SUCCESS' || state === 'FAILURE') return false + return 2000 + }, + }) + + const isProcessing = !!processingTaskId && taskStatus?.state !== 'SUCCESS' && taskStatus?.state !== 'FAILURE' + + const { data: stats, isLoading, error, failureCount, isFetching } = useQuery({ + queryKey: ['stats'], + queryFn: async () => { + const startTime = Date.now() + console.log('[Dashboard] Fetching stats...') + + // Create abort controller for timeout + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), 10000) // 10 second timeout + + try { + const res = await statsApi.get() + clearTimeout(timeoutId) + console.log(`[Dashboard] Stats loaded in ${Date.now() - startTime}ms`) + return res.data + } catch (err: any) { + clearTimeout(timeoutId) + if (err.name === 'AbortError' || err.code === 'ECONNABORTED') { + console.error('[Dashboard] Request timed out after 10 seconds') + throw new Error('Request timed out after 10 seconds - backend may be unresponsive') + } + console.error('[Dashboard] Stats fetch failed:', err) + console.error('[Dashboard] Error details:', { + message: err.message, + status: err.response?.status, + statusText: err.response?.statusText, + data: err.response?.data, + }) + throw err + } + }, + refetchInterval: 30000, // 30 seconds - matches backend cache + retry: 1, + staleTime: 25000, + }) + + // Debug panel to test backend + const { data: debugData, refetch: refetchDebug, isFetching: isDebugFetching } = useQuery({ + queryKey: ['debug'], + queryFn: async () => { + const res = await fetch('/api/debug') + return res.json() + }, + enabled: false, // Only fetch when manually triggered + }) + + if (isLoading) { + return ( +
+
+
+

Loading stats...

+
+
+ ) + } + + if (error) { + const err = error as any + return ( +
+
+

Failed to load dashboard

+
+

Error: {err.message}

+ {err.response && ( + <> +

Status: {err.response.status} {err.response.statusText}

+ {err.response.data && ( +

Response: {JSON.stringify(err.response.data)}

+ )} + + )} +

Retry count: {failureCount}

+
+
+ +
+

Debug Backend Connection

+ + {debugData && ( +
+              {JSON.stringify(debugData, null, 2)}
+            
+ )} +
+
+ ) + } + + if (!stats) { + return
Failed to load stats
+ } + + const sourceData = stats.sources.map((s) => ({ + name: s.source, + downloaded: s.downloaded, + pending: s.pending, + rejected: s.rejected, + })) + + const licenseData = stats.licenses.map((l, i) => ({ + name: l.license, + value: l.count, + color: COLORS[i % COLORS.length], + })) + + return ( +
+

Dashboard

+ + {/* Stats Grid */} +
+ + + + +
+ + {/* Process Pending Banner */} + {(stats.images_pending > 0 || isProcessing) && ( +
+
+

+ {isProcessing + ? `Processing pending images...` + : `${stats.images_pending.toLocaleString()} pending images`} +

+

+ {isProcessing && taskStatus?.queued != null && taskStatus?.total != null + ? `Queued ${taskStatus.queued.toLocaleString()} of ${taskStatus.total.toLocaleString()} for download` + : isProcessing + ? 'Queueing images for download...' + : 'These images have been scraped but not yet downloaded and processed.'} +

+
+ +
+ )} + + {/* Jobs Status */} +
+

Jobs Status

+
+
+
+ Running: {stats.jobs.running} +
+
+ + Pending: {stats.jobs.pending} +
+
+ + Completed: {stats.jobs.completed} +
+
+ + Failed: {stats.jobs.failed} +
+
+
+ + {/* Charts */} +
+ {/* Source Chart */} +
+

Images by Source

+ {sourceData.length > 0 ? ( + + + + + + + + + + + ) : ( +
+ No data yet +
+ )} +
+ + {/* License Chart */} +
+

Images by License

+ {licenseData.length > 0 ? ( + + + + `${name} (${(percent * 100).toFixed(0)}%)` + } + > + {licenseData.map((entry, index) => ( + + ))} + + + + + ) : ( +
+ No data yet +
+ )} +
+
+ + {/* Species Tables */} +
+ {/* Top Species */} +
+

Top Species

+ + + + + + + + + {stats.top_species.map((s) => ( + + + + + ))} + {stats.top_species.length === 0 && ( + + + + )} + +
SpeciesImages
+
{s.scientific_name}
+ {s.common_name && ( +
{s.common_name}
+ )} +
{s.image_count}
+ No species yet +
+
+ + {/* Under-represented Species */} +
+

+ + Under-represented Species +

+

Species with fewer than 100 images

+ + + + + + + + + {stats.under_represented.map((s) => ( + + + + + ))} + {stats.under_represented.length === 0 && ( + + + + )} + +
SpeciesImages
+
{s.scientific_name}
+ {s.common_name && ( +
{s.common_name}
+ )} +
{s.image_count}
+ All species have 100+ images +
+
+
+
+ ) +} diff --git a/frontend/src/pages/Export.tsx b/frontend/src/pages/Export.tsx new file mode 100644 index 0000000..2b1f43c --- /dev/null +++ b/frontend/src/pages/Export.tsx @@ -0,0 +1,346 @@ +import { useState } from 'react' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { + Download, + Trash2, + CheckCircle, + Clock, + AlertCircle, + Package, +} from 'lucide-react' +import { exportsApi, imagesApi, Export as ExportType } from '../api/client' + +export default function Export() { + const queryClient = useQueryClient() + const [showCreateModal, setShowCreateModal] = useState(false) + + const { data: exports, isLoading } = useQuery({ + queryKey: ['exports'], + queryFn: () => exportsApi.list({ limit: 50 }).then((res) => res.data), + refetchInterval: 5000, + }) + + const deleteMutation = useMutation({ + mutationFn: (id: number) => exportsApi.delete(id), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['exports'] }), + }) + + const getStatusIcon = (status: string) => { + switch (status) { + case 'generating': + return + case 'completed': + return + case 'failed': + return + default: + return + } + } + + const formatBytes = (bytes: number | null) => { + if (!bytes) return 'N/A' + if (bytes < 1024) return `${bytes} B` + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` + if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB` + return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB` + } + + return ( +
+
+

Export Dataset

+ +
+ + {/* Info Card */} +
+

Export Format

+

+ Exports are created in Create ML-compatible format with Training and Testing + folders. Each species has its own subfolder with images. +

+
+ + {/* Exports List */} + {isLoading ? ( +
+
+
+ ) : exports?.items.length === 0 ? ( +
+ +

No exports yet

+

+ Create an export to download your dataset for CoreML training +

+
+ ) : ( +
+ {exports?.items.map((exp: ExportType) => ( +
+
+
+
+ {getStatusIcon(exp.status)} +

{exp.name}

+
+
+
+ Species:{' '} + {exp.species_count ?? 'N/A'} +
+
+ Images:{' '} + {exp.image_count ?? 'N/A'} +
+
+ Size:{' '} + {formatBytes(exp.file_size)} +
+
+ Split:{' '} + {Math.round(exp.train_split * 100)}% / {Math.round((1 - exp.train_split) * 100)}% +
+
+ {exp.error_message && ( +
+ Error: {exp.error_message} +
+ )} +
+ Created: {new Date(exp.created_at).toLocaleString()} + {exp.completed_at && ( + + Completed: {new Date(exp.completed_at).toLocaleString()} + + )} +
+
+
+ {exp.status === 'completed' && ( + + + Download + + )} + +
+
+
+ ))} +
+ )} + + {/* Create Modal */} + {showCreateModal && ( + setShowCreateModal(false)} /> + )} +
+ ) +} + +function CreateExportModal({ onClose }: { onClose: () => void }) { + const queryClient = useQueryClient() + const [form, setForm] = useState({ + name: `Export ${new Date().toLocaleDateString()}`, + min_images: 100, + train_split: 0.8, + licenses: [] as string[], + min_quality: undefined as number | undefined, + }) + + const { data: licenses } = useQuery({ + queryKey: ['image-licenses'], + queryFn: () => imagesApi.licenses().then((res) => res.data), + }) + + const previewMutation = useMutation({ + mutationFn: () => + exportsApi.preview({ + name: form.name, + filter_criteria: { + min_images_per_species: form.min_images, + licenses: form.licenses.length > 0 ? form.licenses : undefined, + min_quality: form.min_quality, + }, + train_split: form.train_split, + }), + }) + + const createMutation = useMutation({ + mutationFn: () => + exportsApi.create({ + name: form.name, + filter_criteria: { + min_images_per_species: form.min_images, + licenses: form.licenses.length > 0 ? form.licenses : undefined, + min_quality: form.min_quality, + }, + train_split: form.train_split, + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['exports'] }) + onClose() + }, + }) + + const toggleLicense = (license: string) => { + setForm((f) => ({ + ...f, + licenses: f.licenses.includes(license) + ? f.licenses.filter((l) => l !== license) + : [...f.licenses, license], + })) + } + + return ( +
+
+

Create Export

+ +
+
+ + setForm({ ...form, name: e.target.value })} + className="w-full px-3 py-2 border rounded-lg" + /> +
+ +
+ + + setForm({ ...form, min_images: parseInt(e.target.value) || 0 }) + } + className="w-full px-3 py-2 border rounded-lg" + min={1} + /> +

+ Species with fewer images will be excluded +

+
+ +
+ +
+ + setForm({ ...form, train_split: parseFloat(e.target.value) }) + } + min={0.5} + max={0.95} + step={0.05} + className="flex-1" + /> + + {Math.round(form.train_split * 100)}% /{' '} + {Math.round((1 - form.train_split) * 100)}% + +
+
+ +
+ +
+ {licenses?.map((license) => ( + + ))} +
+ {form.licenses.length === 0 && ( +

+ All licenses will be included +

+ )} +
+ + {/* Preview */} + {previewMutation.data && ( +
+

Preview

+
+
+ Species:{' '} + {previewMutation.data.data.species_count} +
+
+ Images:{' '} + {previewMutation.data.data.image_count} +
+
+ Est. Size:{' '} + {previewMutation.data.data.estimated_size_mb.toFixed(0)} MB +
+
+
+ )} +
+ +
+ +
+ + +
+
+
+
+ ) +} diff --git a/frontend/src/pages/Images.tsx b/frontend/src/pages/Images.tsx new file mode 100644 index 0000000..58ef0db --- /dev/null +++ b/frontend/src/pages/Images.tsx @@ -0,0 +1,331 @@ +import { useState } from 'react' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { + Search, + Filter, + Trash2, + ChevronLeft, + ChevronRight, + X, + ExternalLink, +} from 'lucide-react' +import { imagesApi } from '../api/client' + +export default function Images() { + const queryClient = useQueryClient() + const [page, setPage] = useState(1) + const [search, setSearch] = useState('') + const [filters, setFilters] = useState({ + source: '', + license: '', + status: 'downloaded', + min_quality: undefined as number | undefined, + }) + const [selectedIds, setSelectedIds] = useState([]) + const [selectedImage, setSelectedImage] = useState(null) + + const { data, isLoading } = useQuery({ + queryKey: ['images', page, search, filters], + queryFn: () => + imagesApi + .list({ + page, + page_size: 48, + search: search || undefined, + source: filters.source || undefined, + license: filters.license || undefined, + status: filters.status || undefined, + min_quality: filters.min_quality, + }) + .then((res) => res.data), + }) + + const { data: sources } = useQuery({ + queryKey: ['image-sources'], + queryFn: () => imagesApi.sources().then((res) => res.data), + }) + + const { data: licenses } = useQuery({ + queryKey: ['image-licenses'], + queryFn: () => imagesApi.licenses().then((res) => res.data), + }) + + const { data: imageDetail } = useQuery({ + queryKey: ['image', selectedImage], + queryFn: () => imagesApi.get(selectedImage!).then((res) => res.data), + enabled: !!selectedImage, + }) + + const deleteMutation = useMutation({ + mutationFn: (id: number) => imagesApi.delete(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['images'] }) + setSelectedImage(null) + }, + }) + + const bulkDeleteMutation = useMutation({ + mutationFn: (ids: number[]) => imagesApi.bulkDelete(ids), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['images'] }) + setSelectedIds([]) + }, + }) + + const handleSelect = (id: number) => { + setSelectedIds((prev) => + prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id] + ) + } + + return ( +
+
+

Images

+ {selectedIds.length > 0 && ( + + )} +
+ + {/* Filters */} +
+
+ + { + setSearch(e.target.value) + setPage(1) + }} + className="pl-10 pr-4 py-2 border rounded-lg w-64" + /> +
+ + + + + + +
+ + {/* Image Grid */} + {isLoading ? ( +
+
+
+ ) : data?.items.length === 0 ? ( +
+ +

No images found

+
+ ) : ( +
+ {data?.items.map((image) => ( +
setSelectedImage(image.id)} + > + {image.local_path ? ( + {image.species_name + ) : ( +
+ Pending +
+ )} +
+
+ { + e.stopPropagation() + handleSelect(image.id) + }} + className="rounded opacity-0 group-hover:opacity-100 checked:opacity-100" + /> +
+
+

+ {image.species_name} +

+
+
+ ))} +
+ )} + + {/* Pagination */} + {data && data.pages > 1 && ( +
+ + {data.total} images + +
+ + + Page {page} of {data.pages} + + +
+
+ )} + + {/* Image Detail Modal */} + {selectedImage && imageDetail && ( +
+
+
+

Image Details

+ +
+
+
+ {imageDetail.local_path ? ( + {imageDetail.species_name + ) : ( +
+ Not downloaded +
+ )} +
+
+
+ +

{imageDetail.species_name}

+
+
+ +

{imageDetail.source}

+
+
+ +

{imageDetail.license}

+
+ {imageDetail.attribution && ( +
+ +

{imageDetail.attribution}

+
+ )} +
+
+ +

+ {imageDetail.width || '?'} x {imageDetail.height || '?'} +

+
+
+ +

{imageDetail.quality_score?.toFixed(1) || 'N/A'}

+
+
+
+ +

+ + {imageDetail.status} + +

+
+
+ + + View Original + + +
+
+
+
+
+ )} +
+ ) +} diff --git a/frontend/src/pages/Jobs.tsx b/frontend/src/pages/Jobs.tsx new file mode 100644 index 0000000..882b239 --- /dev/null +++ b/frontend/src/pages/Jobs.tsx @@ -0,0 +1,354 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { + Play, + Pause, + XCircle, + CheckCircle, + Clock, + AlertCircle, + RefreshCw, + Leaf, + Download, + XOctagon, +} from 'lucide-react' +import { jobsApi, Job } from '../api/client' + +export default function Jobs() { + const queryClient = useQueryClient() + + const { data, isLoading, refetch } = useQuery({ + queryKey: ['jobs'], + queryFn: () => jobsApi.list({ limit: 100 }).then((res) => res.data), + refetchInterval: 1000, // Faster refresh for live updates + }) + + const pauseMutation = useMutation({ + mutationFn: (id: number) => jobsApi.pause(id), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['jobs'] }), + }) + + const resumeMutation = useMutation({ + mutationFn: (id: number) => jobsApi.resume(id), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['jobs'] }), + }) + + const cancelMutation = useMutation({ + mutationFn: (id: number) => jobsApi.cancel(id), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['jobs'] }), + }) + + const getStatusIcon = (status: string) => { + switch (status) { + case 'running': + return + case 'pending': + return + case 'paused': + return + case 'completed': + return + case 'failed': + return + default: + return null + } + } + + const getStatusClass = (status: string) => { + switch (status) { + case 'running': + return 'bg-blue-100 text-blue-700' + case 'pending': + return 'bg-yellow-100 text-yellow-700' + case 'paused': + return 'bg-gray-100 text-gray-700' + case 'completed': + return 'bg-green-100 text-green-700' + case 'failed': + return 'bg-red-100 text-red-700' + default: + return 'bg-gray-100 text-gray-700' + } + } + + // Separate running jobs from others + const runningJobs = data?.items.filter((j) => j.status === 'running') || [] + const otherJobs = data?.items.filter((j) => j.status !== 'running') || [] + + return ( +
+
+

Jobs

+ +
+ + {isLoading ? ( +
+
+
+ ) : data?.items.length === 0 ? ( +
+ +

No jobs yet

+

+ Select species and start a scrape job to get started +

+
+ ) : ( +
+ {/* Running Jobs - More prominent display */} + {runningJobs.length > 0 && ( +
+

+ + Active Jobs ({runningJobs.length}) +

+ {runningJobs.map((job) => ( + pauseMutation.mutate(job.id)} + onCancel={() => cancelMutation.mutate(job.id)} + /> + ))} +
+ )} + + {/* Other Jobs */} + {otherJobs.length > 0 && ( +
+ {runningJobs.length > 0 && ( +

Other Jobs

+ )} + {otherJobs.map((job) => ( +
+
+
+
+ {getStatusIcon(job.status)} +

{job.name}

+ + {job.status} + +
+
+ Source: {job.source} + + Downloaded: {job.images_downloaded} + + Rejected: {job.images_rejected} +
+ + {/* Progress bar for paused jobs */} + {job.status === 'paused' && job.progress_total > 0 && ( +
+
+ + {job.progress_current} / {job.progress_total} species + + + {Math.round( + (job.progress_current / job.progress_total) * 100 + )} + % + +
+
+
+
+
+ )} + + {job.error_message && ( +
+ Error: {job.error_message} +
+ )} + +
+ {job.started_at && ( + + Started: {new Date(job.started_at).toLocaleString()} + + )} + {job.completed_at && ( + + Completed: {new Date(job.completed_at).toLocaleString()} + + )} +
+
+ + {/* Actions */} +
+ {job.status === 'paused' && ( + + )} + {(job.status === 'paused' || job.status === 'pending') && ( + + )} +
+
+
+ ))} +
+ )} +
+ )} +
+ ) +} + +function RunningJobCard({ + job, + onPause, + onCancel, +}: { + job: Job + onPause: () => void + onCancel: () => void +}) { + // Fetch real-time progress for this job + const { data: progress } = useQuery({ + queryKey: ['job-progress', job.id], + queryFn: () => jobsApi.progress(job.id).then((res) => res.data), + refetchInterval: 500, // Very fast updates for live feel + enabled: job.status === 'running', + }) + + const currentSpecies = progress?.current_species || '' + const progressCurrent = progress?.progress_current ?? job.progress_current + const progressTotal = progress?.progress_total ?? job.progress_total + const percentage = progressTotal > 0 ? Math.round((progressCurrent / progressTotal) * 100) : 0 + + return ( +
+
+
+
+ +

{job.name}

+ + running + +
+ + {/* Live Stats */} +
+
+
+ + Species Progress +
+
+ {progressCurrent} / {progressTotal} +
+
+
+
+ + Downloaded +
+
+ {job.images_downloaded} +
+
+
+
+ + Rejected +
+
+ {job.images_rejected} +
+
+
+ + {/* Current Species */} + {currentSpecies && ( +
+
Currently scraping:
+
+ + + + + {currentSpecies} +
+
+ )} + + {/* Progress bar */} + {progressTotal > 0 && ( +
+
+ Progress + {percentage}% +
+
+
+
+
+ )} + +
+ Source: {job.source} • Started: {job.started_at ? new Date(job.started_at).toLocaleString() : 'N/A'} +
+
+ + {/* Actions */} +
+ + +
+
+
+ ) +} diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx new file mode 100644 index 0000000..9c570ce --- /dev/null +++ b/frontend/src/pages/Settings.tsx @@ -0,0 +1,543 @@ +import { useState } from 'react' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { + Key, + CheckCircle, + XCircle, + Eye, + EyeOff, + RefreshCw, + FolderInput, + AlertTriangle, +} from 'lucide-react' +import { sourcesApi, imagesApi, SourceConfig, ImportScanResult } from '../api/client' + +export default function Settings() { + const [editingSource, setEditingSource] = useState(null) + + const { data: sources, isLoading, error } = useQuery({ + queryKey: ['sources'], + queryFn: () => sourcesApi.list().then((res) => res.data), + }) + + return ( +
+

Settings

+ + {/* API Keys Section */} +
+
+

+ + API Keys +

+

+ Configure API keys for each data source +

+
+ + {isLoading ? ( +
+ +
+ ) : error ? ( +
+ Error loading sources: {(error as Error).message} +
+ ) : !sources || sources.length === 0 ? ( +
+ No sources available +
+ ) : ( +
+ {sources.map((source) => ( + setEditingSource(source.name)} + onClose={() => setEditingSource(null)} + /> + ))} +
+ )} +
+ + {/* Import Scanner Section */} + + + {/* Rate Limits Info */} +
+

Rate Limits (recommended settings)

+
    +
  • GBIF: 1 req/sec safe (free, no authentication required)
  • +
  • iNaturalist: 1 req/sec max (60/min limit), 10k/day, 5GB/hr media
  • +
  • Flickr: 0.5 req/sec recommended (3600/hr limit shared across all users)
  • +
  • Wikimedia: 1 req/sec safe (requires OAuth credentials)
  • +
  • Trefle: 1 req/sec safe (120/min limit)
  • +
+
+
+ ) +} + +function SourceRow({ + source, + isEditing, + onEdit, + onClose, +}: { + source: SourceConfig + isEditing: boolean + onEdit: () => void + onClose: () => void +}) { + const queryClient = useQueryClient() + const [showKey, setShowKey] = useState(false) + const [form, setForm] = useState({ + api_key: '', + api_secret: '', + access_token: '', + rate_limit_per_sec: source.configured ? source.rate_limit_per_sec : (source.default_rate || 1.0), + enabled: source.enabled, + }) + + // Get field labels based on auth type + const isNoAuth = source.auth_type === 'none' + const isOAuth = source.auth_type === 'oauth' + const keyLabel = isOAuth ? 'Client ID' : 'API Key' + const secretLabel = isOAuth ? 'Client Secret' : 'API Secret' + const [testResult, setTestResult] = useState<{ + status: 'success' | 'error' + message: string + } | null>(null) + + const updateMutation = useMutation({ + mutationFn: () => + sourcesApi.update(source.name, { + api_key: isNoAuth ? undefined : form.api_key || undefined, + api_secret: form.api_secret || undefined, + access_token: form.access_token || undefined, + rate_limit_per_sec: form.rate_limit_per_sec, + enabled: form.enabled, + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['sources'] }) + onClose() + }, + }) + + const testMutation = useMutation({ + mutationFn: () => sourcesApi.test(source.name), + onSuccess: (res) => { + setTestResult({ status: res.data.status, message: res.data.message }) + }, + onError: (err: any) => { + setTestResult({ + status: 'error', + message: err.response?.data?.message || 'Connection failed', + }) + }, + }) + + if (isEditing) { + return ( +
+
+

{source.label}

+ +
+ +
+ {isNoAuth ? ( +
+ This source doesn't require authentication. Just enable it to start scraping. +
+ ) : ( + <> +
+ +
+ setForm({ ...form, api_key: e.target.value })} + placeholder={source.api_key_masked || `Enter ${keyLabel}`} + className="w-full px-3 py-2 border rounded-lg pr-10" + /> + +
+
+ + {source.requires_secret && ( +
+ + + setForm({ ...form, api_secret: e.target.value }) + } + placeholder={source.has_secret ? '••••••••' : `Enter ${secretLabel}`} + className="w-full px-3 py-2 border rounded-lg" + /> +
+ )} + + {isOAuth && ( +
+ + + setForm({ ...form, access_token: e.target.value }) + } + placeholder={source.has_access_token ? '••••••••' : 'Enter Access Token'} + className="w-full px-3 py-2 border rounded-lg" + /> +
+ )} + + )} + +
+ + + setForm({ + ...form, + rate_limit_per_sec: parseFloat(e.target.value) || 1, + }) + } + className="w-full px-3 py-2 border rounded-lg" + min={0.1} + max={10} + step={0.1} + /> +
+ +
+ setForm({ ...form, enabled: e.target.checked })} + className="rounded" + /> + +
+ + {testResult && ( +
+ {testResult.message} +
+ )} + +
+ {source.configured && ( + + )} + +
+
+
+ ) + } + + const isNoAuthRow = source.auth_type === 'none' + + return ( +
+
+
+
+

{source.label}

+

+ {isNoAuthRow + ? 'No authentication required' + : source.configured + ? `Key: ${source.api_key_masked}` + : 'Not configured'} +

+
+
+
+ {(isNoAuthRow || source.configured) && ( + + {source.enabled ? ( + <> + + Enabled + + ) : ( + <> + + Disabled + + )} + + )} + +
+
+ ) +} + +function ImportScanner() { + const [scanResult, setScanResult] = useState(null) + const [moveFiles, setMoveFiles] = useState(false) + const [importResult, setImportResult] = useState<{ + imported: number + skipped: number + errors: string[] + } | null>(null) + + const scanMutation = useMutation({ + mutationFn: () => imagesApi.scanImports().then((res) => res.data), + onSuccess: (data) => { + setScanResult(data) + setImportResult(null) + }, + }) + + const importMutation = useMutation({ + mutationFn: () => imagesApi.runImport(moveFiles).then((res) => res.data), + onSuccess: (data) => { + setImportResult(data) + setScanResult(null) + }, + }) + + return ( +
+
+

+ + Import Images +

+

+ Bulk import images from the imports folder +

+
+ +
+
+

Expected folder structure:

+ + imports/{'{source}'}/{'{species_name}'}/*.jpg + +

+ Example: imports/inaturalist/Monstera_deliciosa/image1.jpg +

+
+ +
+ +
+ + {scanMutation.isError && ( +
+ Error scanning: {(scanMutation.error as Error).message} +
+ )} + + {scanResult && ( +
+ {!scanResult.available ? ( +
+

{scanResult.message}

+
+ ) : scanResult.total_images === 0 ? ( +
+

No images found in the imports folder.

+
+ ) : ( + <> +
+

Scan Results

+
+
+ Total Images: + {scanResult.total_images} +
+
+ Matched Species: + {scanResult.matched_species} +
+
+ + {scanResult.sources.length > 0 && ( +
+

Sources Found:

+
+ {scanResult.sources.map((source) => ( +
+ {source.name} + + {source.species_count} species, {source.image_count} images + +
+ ))} +
+
+ )} +
+ + {scanResult.unmatched_species.length > 0 && ( +
+

+ + Unmatched Species ({scanResult.unmatched_species.length}) +

+

+ These species folders don't match any species in the database and will be skipped: +

+
+ {scanResult.unmatched_species.slice(0, 20).map((name) => ( +
{name}
+ ))} + {scanResult.unmatched_species.length > 20 && ( +
+ ...and {scanResult.unmatched_species.length - 20} more +
+ )} +
+
+ )} + +
+
+ +
+ + +
+ + )} +
+ )} + + {importResult && ( +
+

Import Complete

+
+
+ Imported: + {importResult.imported} +
+
+ Skipped (already exists): + {importResult.skipped} +
+ {importResult.errors.length > 0 && ( +
+ Errors ({importResult.errors.length}): +
+ {importResult.errors.map((err, i) => ( +
{err}
+ ))} +
+
+ )} +
+
+ )} +
+
+ ) +} diff --git a/frontend/src/pages/Species.tsx b/frontend/src/pages/Species.tsx new file mode 100644 index 0000000..97bbf51 --- /dev/null +++ b/frontend/src/pages/Species.tsx @@ -0,0 +1,997 @@ +import { useState, useRef } from 'react' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { + Plus, + Upload, + Search, + Trash2, + Play, + ChevronLeft, + ChevronRight, + Filter, + X, + Image as ImageIcon, + ExternalLink, +} from 'lucide-react' +import { speciesApi, jobsApi, imagesApi, Species as SpeciesType } from '../api/client' + +export default function Species() { + const queryClient = useQueryClient() + const csvInputRef = useRef(null) + const jsonInputRef = useRef(null) + + const [page, setPage] = useState(1) + const [search, setSearch] = useState('') + const [genus, setGenus] = useState('') + const [hasImages, setHasImages] = useState('') + const [maxImages, setMaxImages] = useState('') + const [selectedIds, setSelectedIds] = useState([]) + const [showAddModal, setShowAddModal] = useState(false) + const [showScrapeModal, setShowScrapeModal] = useState(false) + const [showScrapeAllModal, setShowScrapeAllModal] = useState(false) + const [showScrapeFilteredModal, setShowScrapeFilteredModal] = useState(false) + const [viewSpecies, setViewSpecies] = useState(null) + + const { data: genera } = useQuery({ + queryKey: ['genera'], + queryFn: () => speciesApi.genera().then((res) => res.data), + }) + + const { data, isLoading } = useQuery({ + queryKey: ['species', page, search, genus, hasImages, maxImages], + queryFn: () => + speciesApi.list({ + page, + page_size: 50, + search: search || undefined, + genus: genus || undefined, + has_images: hasImages === '' ? undefined : hasImages === 'true', + max_images: maxImages ? parseInt(maxImages) : undefined, + }).then((res) => res.data), + }) + + const importCsvMutation = useMutation({ + mutationFn: (file: File) => speciesApi.import(file), + onSuccess: (res) => { + queryClient.invalidateQueries({ queryKey: ['species'] }) + queryClient.invalidateQueries({ queryKey: ['genera'] }) + alert(`Imported ${res.data.imported} species, skipped ${res.data.skipped}`) + }, + }) + + const importJsonMutation = useMutation({ + mutationFn: (file: File) => speciesApi.importJson(file), + onSuccess: (res) => { + queryClient.invalidateQueries({ queryKey: ['species'] }) + queryClient.invalidateQueries({ queryKey: ['genera'] }) + alert(`Imported ${res.data.imported} species, skipped ${res.data.skipped}`) + }, + }) + + const deleteMutation = useMutation({ + mutationFn: (id: number) => speciesApi.delete(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['species'] }) + }, + }) + + const createJobMutation = useMutation({ + mutationFn: (data: { name: string; source: string; species_ids?: number[] }) => + jobsApi.create(data), + onSuccess: () => { + setShowScrapeModal(false) + setSelectedIds([]) + alert('Scrape job created!') + }, + }) + + const handleCsvImport = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (file) { + importCsvMutation.mutate(file) + e.target.value = '' + } + } + + const handleJsonImport = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (file) { + importJsonMutation.mutate(file) + e.target.value = '' + } + } + + const handleSelectAll = () => { + if (!data) return + if (selectedIds.length === data.items.length) { + setSelectedIds([]) + } else { + setSelectedIds(data.items.map((s) => s.id)) + } + } + + const handleSelect = (id: number) => { + setSelectedIds((prev) => + prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id] + ) + } + + return ( +
+
+

Species

+
+ + + + + +
+
+ + {/* Search and Filters */} +
+
+ + { + setSearch(e.target.value) + setPage(1) + }} + className="pl-10 pr-4 py-2 border rounded-lg w-64" + /> +
+ +
+ + + + + + + + {(genus || hasImages || maxImages) && ( + + )} +
+ +
+ {maxImages && data && data.total > 0 && ( + + )} + + {selectedIds.length > 0 && ( +
+ + {selectedIds.length} selected + + +
+ )} +
+
+ + {/* Table */} +
+ + + + + + + + + + + + + {isLoading ? ( + + + + ) : data?.items.length === 0 ? ( + + + + ) : ( + data?.items.map((species) => ( + setViewSpecies(species)} + > + + + + + + + + )) + )} + +
+ 0 && selectedIds.length === (data?.items?.length ?? 0)} + onChange={handleSelectAll} + className="rounded" + /> + + Scientific Name + + Common Name + + Genus + + Images + + Actions +
+ Loading... +
+ No species found. Import a CSV to get started. +
e.stopPropagation()}> + handleSelect(species.id)} + className="rounded" + /> + {species.scientific_name} + {species.common_name || '-'} + {species.genus || '-'} + = 100 + ? 'bg-green-100 text-green-700' + : species.image_count > 0 + ? 'bg-yellow-100 text-yellow-700' + : 'bg-gray-100 text-gray-600' + }`} + > + {species.image_count} + + e.stopPropagation()}> + +
+
+ + {/* Pagination */} + {data && data.pages > 1 && ( +
+ + Showing {(page - 1) * 50 + 1} to {Math.min(page * 50, data.total)} of{' '} + {data.total} + +
+ + + Page {page} of {data.pages} + + +
+
+ )} + + {/* Add Species Modal */} + {showAddModal && ( + setShowAddModal(false)} /> + )} + + {/* Scrape Modal */} + {showScrapeModal && ( + setShowScrapeModal(false)} + onSubmit={(source) => { + createJobMutation.mutate({ + name: `Scrape ${selectedIds.length} species from ${source}`, + source, + species_ids: selectedIds, + }) + }} + /> + )} + + {/* Species Detail Modal */} + {viewSpecies && ( + setViewSpecies(null)} + /> + )} + + {/* Scrape All Without Images Modal */} + {showScrapeAllModal && ( + setShowScrapeAllModal(false)} + /> + )} + + {/* Scrape All Filtered Modal */} + {showScrapeFilteredModal && ( + setShowScrapeFilteredModal(false)} + /> + )} +
+ ) +} + +function AddSpeciesModal({ onClose }: { onClose: () => void }) { + const queryClient = useQueryClient() + const [form, setForm] = useState({ + scientific_name: '', + common_name: '', + genus: '', + family: '', + }) + + const mutation = useMutation({ + mutationFn: () => speciesApi.create(form), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['species'] }) + onClose() + }, + }) + + return ( +
+
+

Add Species

+
+
+ + + setForm({ ...form, scientific_name: e.target.value }) + } + className="w-full px-3 py-2 border rounded-lg" + placeholder="e.g. Monstera deliciosa" + /> +
+
+ + setForm({ ...form, common_name: e.target.value })} + className="w-full px-3 py-2 border rounded-lg" + placeholder="e.g. Swiss Cheese Plant" + /> +
+
+
+ + setForm({ ...form, genus: e.target.value })} + className="w-full px-3 py-2 border rounded-lg" + placeholder="e.g. Monstera" + /> +
+
+ + setForm({ ...form, family: e.target.value })} + className="w-full px-3 py-2 border rounded-lg" + placeholder="e.g. Araceae" + /> +
+
+
+
+ + +
+
+
+ ) +} + +function ScrapeModal({ + selectedIds, + onClose, + onSubmit, +}: { + selectedIds: number[] + onClose: () => void + onSubmit: (source: string) => void +}) { + const [source, setSource] = useState('inaturalist') + + const sources = [ + { value: 'gbif', label: 'GBIF' }, + { value: 'inaturalist', label: 'iNaturalist' }, + { value: 'flickr', label: 'Flickr' }, + { value: 'wikimedia', label: 'Wikimedia Commons' }, + { value: 'trefle', label: 'Trefle.io' }, + { value: 'duckduckgo', label: 'DuckDuckGo' }, + { value: 'bing', label: 'Bing Image Search' }, + ] + + return ( +
+
+

Start Scrape Job

+

+ Scrape images for {selectedIds.length} selected species +

+
+ +
+ {sources.map((s) => ( + + ))} +
+
+
+ + +
+
+
+ ) +} + +function SpeciesDetailModal({ + species, + onClose, +}: { + species: SpeciesType + onClose: () => void +}) { + const [page, setPage] = useState(1) + const pageSize = 20 + + const { data, isLoading } = useQuery({ + queryKey: ['species-images', species.id, page], + queryFn: () => + imagesApi.list({ + species_id: species.id, + status: 'downloaded', + page, + page_size: pageSize, + }).then((res) => res.data), + }) + + return ( +
+
+ {/* Header */} +
+
+

{species.scientific_name}

+ {species.common_name && ( +

{species.common_name}

+ )} +
+ {species.genus && Genus: {species.genus}} + {species.family && Family: {species.family}} + {species.image_count} images +
+
+ +
+ + {/* Images Grid */} +
+ {isLoading ? ( +
+
+
+ ) : !data || data.items.length === 0 ? ( +
+ +

No images yet

+

+ Start a scrape job to download images for this species +

+
+ ) : ( +
+ {data.items.map((image) => ( +
+ {image.local_path ? ( + {species.scientific_name} + ) : ( +
+ +
+ )} + {/* Overlay with info */} +
+
+
+ + {image.source} + + + {image.license} + +
+ {image.width && image.height && ( +
+ {image.width} × {image.height} +
+ )} +
+ {image.url && ( + e.stopPropagation()} + > + + + )} +
+
+ ))} +
+ )} +
+ + {/* Pagination */} + {data && data.pages > 1 && ( +
+ + Showing {(page - 1) * pageSize + 1} to{' '} + {Math.min(page * pageSize, data.total)} of {data.total} + +
+ + + Page {page} of {data.pages} + + +
+
+ )} +
+
+ ) +} + +function ScrapeAllModal({ onClose }: { onClose: () => void }) { + const [selectedSources, setSelectedSources] = useState([]) + const [isSubmitting, setIsSubmitting] = useState(false) + + // Fetch count of species without images + const { data: speciesData, isLoading } = useQuery({ + queryKey: ['species-no-images'], + queryFn: () => + speciesApi.list({ + page: 1, + page_size: 1, + has_images: false, + }).then((res) => res.data), + }) + + const sources = [ + { value: 'gbif', label: 'GBIF', description: 'Free biodiversity database, no API key needed' }, + { value: 'inaturalist', label: 'iNaturalist', description: 'Research-grade observations with CC licenses' }, + { value: 'wikimedia', label: 'Wikimedia Commons', description: 'Free media repository, requires OAuth' }, + { value: 'flickr', label: 'Flickr', description: 'Requires API key, CC-licensed photos' }, + { value: 'trefle', label: 'Trefle.io', description: 'Plant database, requires API key' }, + { value: 'duckduckgo', label: 'DuckDuckGo', description: 'Web image search, no API key needed' }, + { value: 'bing', label: 'Bing Image Search', description: 'Azure Cognitive Services, requires API key' }, + ] + + const toggleSource = (source: string) => { + setSelectedSources((prev) => + prev.includes(source) + ? prev.filter((s) => s !== source) + : [...prev, source] + ) + } + + const handleSubmit = async () => { + if (selectedSources.length === 0) return + + setIsSubmitting(true) + try { + // Create a job for each selected source + for (const source of selectedSources) { + await jobsApi.create({ + name: `Scrape all species without images from ${source}`, + source, + only_without_images: true, + }) + } + alert(`Created ${selectedSources.length} scrape job(s)!`) + onClose() + } catch (error) { + alert('Failed to create jobs') + } finally { + setIsSubmitting(false) + } + } + + const speciesCount = speciesData?.total ?? 0 + + return ( +
+
+

Scrape All Species Without Images

+ {isLoading ? ( +

Loading...

+ ) : ( +

+ {speciesCount === 0 ? ( + 'All species already have images!' + ) : ( + <> + {speciesCount} species + don't have any images yet. Select sources to scrape from: + + )} +

+ )} + + {speciesCount > 0 && ( + <> +
+ {sources.map((s) => ( + + ))} +
+ + {selectedSources.length > 1 && ( +
+ {selectedSources.length} jobs will be created and run in parallel, + one for each selected source. +
+ )} + + )} + +
+ + {speciesCount > 0 && ( + + )} +
+
+
+ ) +} + +function ScrapeFilteredModal({ + maxImages, + speciesCount, + onClose, +}: { + maxImages: number + speciesCount: number + onClose: () => void +}) { + const [selectedSources, setSelectedSources] = useState([]) + const [isSubmitting, setIsSubmitting] = useState(false) + + const sources = [ + { value: 'gbif', label: 'GBIF', description: 'Free biodiversity database, no API key needed' }, + { value: 'inaturalist', label: 'iNaturalist', description: 'Research-grade observations with CC licenses' }, + { value: 'wikimedia', label: 'Wikimedia Commons', description: 'Free media repository, requires OAuth' }, + { value: 'flickr', label: 'Flickr', description: 'Requires API key, CC-licensed photos' }, + { value: 'trefle', label: 'Trefle.io', description: 'Plant database, requires API key' }, + { value: 'duckduckgo', label: 'DuckDuckGo', description: 'Web image search, no API key needed' }, + { value: 'bing', label: 'Bing Image Search', description: 'Azure Cognitive Services, requires API key' }, + ] + + const toggleSource = (source: string) => { + setSelectedSources((prev) => + prev.includes(source) + ? prev.filter((s) => s !== source) + : [...prev, source] + ) + } + + const handleSubmit = async () => { + if (selectedSources.length === 0) return + + setIsSubmitting(true) + try { + for (const source of selectedSources) { + await jobsApi.create({ + name: `Scrape species with <${maxImages} images from ${source}`, + source, + max_images: maxImages, + }) + } + alert(`Created ${selectedSources.length} scrape job(s)!`) + onClose() + } catch (error) { + alert('Failed to create jobs') + } finally { + setIsSubmitting(false) + } + } + + return ( +
+
+

Scrape All Filtered Species

+

+ {speciesCount} species + have fewer than {maxImages} images. + Select sources to scrape from: +

+ +
+ {sources.map((s) => ( + + ))} +
+ + {selectedSources.length > 1 && ( +
+ {selectedSources.length} jobs will be created and run in parallel, + one for each selected source. +
+ )} + +
+ + +
+
+
+ ) +} diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..b54b4c9 --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +interface ImportMetaEnv { + readonly VITE_API_URL: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..dca8ba0 --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,11 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + theme: { + extend: {}, + }, + plugins: [], +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..3934b8f --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..42872c5 --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..57b3737 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + server: { + port: 3000, + host: true, + proxy: { + '/api': { + target: 'http://backend:8000', + changeOrigin: true, + }, + }, + // Disable HMR - not useful in Docker deployments + hmr: false, + }, +}) diff --git a/houseplants_list.json b/houseplants_list.json new file mode 100755 index 0000000..963d7f3 --- /dev/null +++ b/houseplants_list.json @@ -0,0 +1,18874 @@ +{ + "source_date": "2026-01-22", + "total_plants": 2278, + "sources": [ + "World Flora Online (wfoplantlist.org)", + "Missouri Botanical Garden Plant Finder", + "Royal Horticultural Society (RHS)", + "Wikipedia - List of Houseplants", + "House Plants Expert (houseplantsexpert.com)", + "Epic Gardening", + "Costa Farms", + "Gardener's Path", + "Plantura Garden", + "Ohio Tropics", + "Smart Garden Guide", + "Balcony Garden Web", + "USDA Plants Database" + ], + "plants": [ + { + "scientific_name": "Philodendron hederaceum", + "common_names": [ + "Heartleaf Philodendron", + "Sweetheart Plant" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron hederaceum 'Brasil'", + "common_names": [ + "Brasil Philodendron", + "Philodendron Brasil" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron hederaceum 'Micans'", + "common_names": [ + "Velvet Leaf Philodendron", + "Philodendron Micans" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron hederaceum 'Lemon Lime'", + "common_names": [ + "Lemon Lime Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron hederaceum 'Cream Splash'", + "common_names": [ + "Cream Splash Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron hederaceum 'Rio'", + "common_names": [ + "Philodendron Rio" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron erubescens", + "common_names": [ + "Blushing Philodendron", + "Red-leaf Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron erubescens 'Pink Princess'", + "common_names": [ + "Pink Princess Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron erubescens 'White Princess'", + "common_names": [ + "White Princess Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron erubescens 'White Knight'", + "common_names": [ + "White Knight Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron erubescens 'White Wizard'", + "common_names": [ + "White Wizard Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron erubescens 'Prince of Orange'", + "common_names": [ + "Prince of Orange Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron erubescens 'McColley's Finale'", + "common_names": [ + "McColley's Finale Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron erubescens 'Imperial Red'", + "common_names": [ + "Imperial Red Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron erubescens 'Imperial Green'", + "common_names": [ + "Imperial Green Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron erubescens 'Red Emerald'", + "common_names": [ + "Red Emerald Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron erubescens 'Burgundy'", + "common_names": [ + "Burgundy Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron 'Birkin'", + "common_names": [ + "Birkin Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron bipinnatifidum", + "common_names": [ + "Tree Philodendron", + "Lacy Tree Philodendron", + "Split-leaf Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron 'Xanadu'", + "common_names": [ + "Xanadu Philodendron", + "Winterbourn" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron 'Hope'", + "common_names": [ + "Hope Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron gloriosum", + "common_names": [ + "Gloriosum Philodendron", + "Velvet Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron gloriosum 'Dark Form'", + "common_names": [ + "Dark Form Gloriosum" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron gloriosum 'Zebra'", + "common_names": [ + "Zebra Gloriosum" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron gloriosum 'Pink Back'", + "common_names": [ + "Pink Back Gloriosum" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron melanochrysum", + "common_names": [ + "Black Gold Philodendron", + "Melano" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron verrucosum", + "common_names": [ + "Ecuador Philodendron", + "Velvet Leaf Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron plowmanii", + "common_names": [ + "Plowman's Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron rugosum", + "common_names": [ + "Pigskin Philodendron", + "Rugosum Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron mexicanum", + "common_names": [ + "Mexican Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron squamiferum", + "common_names": [ + "Hairy Philodendron", + "Red Bristle Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron pedatum", + "common_names": [ + "Oak Leaf Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron 'Florida'", + "common_names": [ + "Florida Philodendron", + "Florida Green" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron 'Florida Ghost'", + "common_names": [ + "Florida Ghost Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron 'Florida Beauty'", + "common_names": [ + "Florida Beauty Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron bernardopazii", + "common_names": [ + "Bernardopazii Philodendron", + "Santa Leopoldina" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron hastatum", + "common_names": [ + "Silver Sword Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron giganteum", + "common_names": [ + "Giant Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron brandtianum", + "common_names": [ + "Silver Leaf Philodendron", + "Brandt's Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron mamei", + "common_names": [ + "Silver Cloud Philodendron", + "Quilted Silver Leaf" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron sodiroi", + "common_names": [ + "Sodiroi Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron pastazanum", + "common_names": [ + "Pasta Philodendron", + "My Pasta" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron billietiae", + "common_names": [ + "Billietiae Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron atabapoense", + "common_names": [ + "Atabapoense Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron tortum", + "common_names": [ + "Tortum Philodendron", + "Fernleaf Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron warszewiczii", + "common_names": [ + "Warszewiczii Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron sharoniae", + "common_names": [ + "Sharoniae Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron nangaritense", + "common_names": [ + "Nangaritense Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron mayoi", + "common_names": [ + "Mayoi Philodendron", + "Palm Leaf Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron 'Splendid'", + "common_names": [ + "Splendid Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Epipremnum aureum", + "common_names": [ + "Golden Pothos", + "Devil's Ivy", + "Money Plant" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Epipremnum aureum 'Marble Queen'", + "common_names": [ + "Marble Queen Pothos" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Epipremnum aureum 'Jade'", + "common_names": [ + "Jade Pothos" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Epipremnum aureum 'Neon'", + "common_names": [ + "Neon Pothos" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Epipremnum aureum 'N'Joy'", + "common_names": [ + "N'Joy Pothos", + "Njoy Pothos" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Epipremnum aureum 'Pearls and Jade'", + "common_names": [ + "Pearls and Jade Pothos" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Epipremnum aureum 'Manjula'", + "common_names": [ + "Manjula Pothos", + "Happy Leaf Pothos" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Epipremnum aureum 'Global Green'", + "common_names": [ + "Global Green Pothos" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Epipremnum aureum 'Jessenia'", + "common_names": [ + "Jessenia Pothos" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Epipremnum aureum 'Snow Queen'", + "common_names": [ + "Snow Queen Pothos" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Epipremnum aureum 'Harlequin'", + "common_names": [ + "Harlequin Pothos" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Epipremnum pinnatum", + "common_names": [ + "Dragon Tail Pothos", + "Tibatib" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Epipremnum pinnatum 'Cebu Blue'", + "common_names": [ + "Cebu Blue Pothos" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Epipremnum pinnatum 'Baltic Blue'", + "common_names": [ + "Baltic Blue Pothos" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Epipremnum pinnatum 'Skeleton Key'", + "common_names": [ + "Skeleton Key Pothos" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Epipremnum pinnatum 'Marble'", + "common_names": [ + "Marble King Pothos", + "Epipremnum Pinnatum Marble" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Epipremnum pinnatum 'Silver Streak'", + "common_names": [ + "Silver Streak Pothos" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Epipremnum amplissimum", + "common_names": [ + "Amplifolia Pothos" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Scindapsus pictus", + "common_names": [ + "Satin Pothos", + "Silver Pothos" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Scindapsus pictus 'Argyraeus'", + "common_names": [ + "Satin Pothos Argyraeus", + "Silver Satin Pothos" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Scindapsus pictus 'Exotica'", + "common_names": [ + "Exotica Pothos", + "Satin Pothos Exotica" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Scindapsus pictus 'Silvery Ann'", + "common_names": [ + "Silvery Ann Pothos" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Scindapsus treubii", + "common_names": [ + "Trebi Pothos", + "Sterling Silver Scindapsus" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Scindapsus treubii 'Moonlight'", + "common_names": [ + "Moonlight Scindapsus" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Scindapsus treubii 'Dark Form'", + "common_names": [ + "Dark Form Scindapsus" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Monstera deliciosa", + "common_names": [ + "Swiss Cheese Plant", + "Split-leaf Philodendron", + "Fruit Salad Plant" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Monstera deliciosa 'Thai Constellation'", + "common_names": [ + "Thai Constellation Monstera" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Monstera deliciosa 'Albo Variegata'", + "common_names": [ + "Albo Monstera", + "Variegated Monstera" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Monstera deliciosa 'Aurea'", + "common_names": [ + "Aurea Monstera", + "Yellow Variegated Monstera" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Monstera borsigiana", + "common_names": [ + "Borsigiana Monstera", + "Small Form Monstera" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Monstera adansonii", + "common_names": [ + "Swiss Cheese Vine", + "Monkey Mask", + "Adanson's Monstera" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Monstera adansonii 'Aurea'", + "common_names": [ + "Aurea Adansonii" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Monstera adansonii 'Variegata'", + "common_names": [ + "Variegated Adansonii" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Monstera obliqua", + "common_names": [ + "Obliqua Monstera", + "Unicorn Plant" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Monstera siltepecana", + "common_names": [ + "Silver Monstera", + "El Salvador" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Monstera peru", + "common_names": [ + "Monstera Peru", + "Monstera karstenianum" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Monstera pinnatipartita", + "common_names": [ + "Pinnatipartita Monstera" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Monstera dubia", + "common_names": [ + "Dubia Monstera", + "Shingle Plant" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Monstera esqueleto", + "common_names": [ + "Esqueleto Monstera", + "Skeleton Monstera" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Monstera subpinnata", + "common_names": [ + "Subpinnata Monstera", + "Palm Monstera" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Monstera acuminata", + "common_names": [ + "Acuminata Monstera", + "Shingle Monstera" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Monstera lechleriana", + "common_names": [ + "Lechleriana Monstera" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Monstera 'Aussie Dream'", + "common_names": [ + "Aussie Dream Monstera" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Monstera standleyana", + "common_names": [ + "Standleyana Monstera", + "Five Holes Plant" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Monstera standleyana 'Albo'", + "common_names": [ + "Albo Standleyana" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus lyrata", + "common_names": [ + "Fiddle Leaf Fig", + "Banjo Fig" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus lyrata 'Bambino'", + "common_names": [ + "Dwarf Fiddle Leaf Fig" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus elastica", + "common_names": [ + "Rubber Plant", + "Rubber Tree", + "Rubber Fig" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus elastica 'Burgundy'", + "common_names": [ + "Burgundy Rubber Plant" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus elastica 'Tineke'", + "common_names": [ + "Tineke Rubber Plant", + "Variegated Rubber Plant" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus elastica 'Ruby'", + "common_names": [ + "Ruby Rubber Plant", + "Red Ruby" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus elastica 'Robusta'", + "common_names": [ + "Robusta Rubber Plant" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus elastica 'Black Prince'", + "common_names": [ + "Black Prince Rubber Plant" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus elastica 'Shivereana'", + "common_names": [ + "Shivereana Rubber Plant", + "Moonshine Ficus" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus benjamina", + "common_names": [ + "Weeping Fig", + "Benjamin Fig", + "Ficus Tree" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus benjamina 'Variegata'", + "common_names": [ + "Variegated Weeping Fig" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus benjamina 'Starlight'", + "common_names": [ + "Starlight Ficus" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus benghalensis", + "common_names": [ + "Ficus Audrey", + "Banyan Tree", + "Indian Banyan" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus microcarpa", + "common_names": [ + "Ginseng Ficus", + "Indian Laurel", + "Chinese Banyan" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus pumila", + "common_names": [ + "Creeping Fig", + "Climbing Fig" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus pumila 'Variegata'", + "common_names": [ + "Variegated Creeping Fig" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus triangularis", + "common_names": [ + "Triangle Ficus", + "Sweetheart Tree" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus triangularis 'Variegata'", + "common_names": [ + "Variegated Triangle Ficus" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus umbellata", + "common_names": [ + "Umbellata Ficus", + "Ficus Umbellata" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus altissima", + "common_names": [ + "Council Tree", + "Lofty Fig" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus altissima 'Yellow Gem'", + "common_names": [ + "Yellow Gem Ficus" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus petiolaris", + "common_names": [ + "Rock Fig", + "Blue Ficus" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia amazonica", + "common_names": [ + "Amazonian Elephant Ear", + "African Mask Plant" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia amazonica 'Polly'", + "common_names": [ + "Alocasia Polly", + "African Mask Polly" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia baginda 'Dragon Scale'", + "common_names": [ + "Dragon Scale Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia baginda 'Silver Dragon'", + "common_names": [ + "Silver Dragon Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia reginula 'Black Velvet'", + "common_names": [ + "Black Velvet Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia sanderiana", + "common_names": [ + "Kris Plant", + "Sander's Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia zebrina", + "common_names": [ + "Zebra Alocasia", + "Tiger Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia stingray", + "common_names": [ + "Stingray Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia macrorrhiza", + "common_names": [ + "Giant Taro", + "Upright Elephant Ear" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia macrorrhiza 'Stingray'", + "common_names": [ + "Stingray Elephant Ear" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia cuprea", + "common_names": [ + "Mirror Plant", + "Jewel Alocasia", + "Red Secret" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia wentii", + "common_names": [ + "Hardy Elephant Ear", + "New Guinea Shield" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia rugosa 'Melo'", + "common_names": [ + "Alocasia Melo", + "Rugose Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia lauterbachiana", + "common_names": [ + "Purple Sword Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia odora", + "common_names": [ + "Night-scented Lily", + "Asian Taro" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia frydek", + "common_names": [ + "Alocasia Frydek", + "Green Velvet Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia frydek 'Variegata'", + "common_names": [ + "Variegated Alocasia Frydek" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia 'Ivory Coast'", + "common_names": [ + "Ivory Coast Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia 'Nairobi Nights'", + "common_names": [ + "Nairobi Nights Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia 'Black Magic'", + "common_names": [ + "Black Magic Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia 'Yucatan Princess'", + "common_names": [ + "Yucatan Princess Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia portei", + "common_names": [ + "Portei Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia longiloba", + "common_names": [ + "Tiger Taro", + "Longiloba Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia cucullata", + "common_names": [ + "Buddha's Hand", + "Chinese Taro" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Calathea ornata", + "common_names": [ + "Pinstripe Calathea", + "Pinstripe Plant" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Calathea ornata 'Sanderiana'", + "common_names": [ + "Sanderiana Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Calathea orbifolia", + "common_names": [ + "Orbifolia Calathea", + "Round Leaf Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Calathea lancifolia", + "common_names": [ + "Rattlesnake Plant", + "Rattlesnake Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Calathea makoyana", + "common_names": [ + "Peacock Plant", + "Cathedral Windows" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Calathea roseopicta", + "common_names": [ + "Rose Painted Calathea", + "Medallion Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Calathea roseopicta 'Dottie'", + "common_names": [ + "Dottie Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Calathea roseopicta 'Rosy'", + "common_names": [ + "Rosy Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Calathea roseopicta 'Corona'", + "common_names": [ + "Corona Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Calathea roseopicta 'Medallion'", + "common_names": [ + "Medallion Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Calathea zebrina", + "common_names": [ + "Zebra Plant", + "Zebra Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Calathea musaica", + "common_names": [ + "Network Calathea", + "Mosaic Plant" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Calathea rufibarba", + "common_names": [ + "Furry Feather Calathea", + "Velvet Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Calathea warscewiczii", + "common_names": [ + "Jungle Velvet Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Calathea 'White Fusion'", + "common_names": [ + "White Fusion Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Calathea 'Freddie'", + "common_names": [ + "Freddie Calathea", + "Concinna Freddie" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Calathea 'Beauty Star'", + "common_names": [ + "Beauty Star Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Calathea vittata", + "common_names": [ + "Vittata Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Calathea setosa", + "common_names": [ + "Setosa Calathea", + "Never Never Plant" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Calathea leitzii 'Fusion White'", + "common_names": [ + "Fusion White Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Maranta leuconeura", + "common_names": [ + "Prayer Plant", + "Red Prayer Plant" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Maranta leuconeura var. erythroneura", + "common_names": [ + "Red Veined Prayer Plant", + "Herringbone Plant" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Maranta leuconeura var. kerchoveana", + "common_names": [ + "Rabbit's Foot Prayer Plant", + "Green Prayer Plant" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Maranta leuconeura var. leuconeura", + "common_names": [ + "Black Prayer Plant" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Maranta leuconeura 'Lemon Lime'", + "common_names": [ + "Lemon Lime Prayer Plant" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Maranta leuconeura 'Kim'", + "common_names": [ + "Kim Prayer Plant" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Stromanthe sanguinea", + "common_names": [ + "Stromanthe Triostar", + "Never Never Plant" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Stromanthe sanguinea 'Triostar'", + "common_names": [ + "Triostar Stromanthe", + "Magenta Triostar" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Stromanthe sanguinea 'Magic Star'", + "common_names": [ + "Magic Star Stromanthe" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ctenanthe oppenheimiana", + "common_names": [ + "Never Never Plant", + "Giant Bamburanta" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ctenanthe burle-marxii", + "common_names": [ + "Fishbone Prayer Plant", + "Ctenanthe Amagris" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ctenanthe setosa", + "common_names": [ + "Grey Star Ctenanthe" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ctenanthe lubbersiana", + "common_names": [ + "Bamburanta", + "Never Never Plant" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya carnosa", + "common_names": [ + "Wax Plant", + "Porcelain Flower" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya carnosa 'Tricolor'", + "common_names": [ + "Tricolor Hoya", + "Krimson Princess" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya carnosa 'Compacta'", + "common_names": [ + "Hindu Rope Plant", + "Krinkle Kurl" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya carnosa 'Krimson Queen'", + "common_names": [ + "Krimson Queen Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya carnosa 'Chelsea'", + "common_names": [ + "Chelsea Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya kerrii", + "common_names": [ + "Sweetheart Hoya", + "Valentine Hoya", + "Heart Leaf Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya kerrii 'Variegata'", + "common_names": [ + "Variegated Sweetheart Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya pubicalyx", + "common_names": [ + "Pink Silver Hoya", + "Pubicalyx" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya pubicalyx 'Splash'", + "common_names": [ + "Splash Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya pubicalyx 'Royal Hawaiian Purple'", + "common_names": [ + "Royal Hawaiian Purple Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya australis", + "common_names": [ + "Waxvine", + "Australian Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya australis 'Lisa'", + "common_names": [ + "Lisa Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya obovata", + "common_names": [ + "Obovata Hoya", + "Wax Leaf Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya obovata 'Variegata'", + "common_names": [ + "Variegated Obovata" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya linearis", + "common_names": [ + "Linearis Hoya", + "Wax Plant" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya curtisii", + "common_names": [ + "Curtisii Hoya", + "Tiny Leaf Porcelain Flower" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya wayetii", + "common_names": [ + "Wayetii Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya retusa", + "common_names": [ + "Retusa Hoya", + "Grass Leafed Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya shepherdii", + "common_names": [ + "String Bean Hoya", + "Shepherdii" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya macgillivrayi", + "common_names": [ + "Macgillivray's Wax Flower" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya serpens", + "common_names": [ + "Serpens Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya callistophylla", + "common_names": [ + "Callistophylla Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya bella", + "common_names": [ + "Beautiful Hoya", + "Miniature Wax Plant" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya multiflora", + "common_names": [ + "Shooting Star Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya lacunosa", + "common_names": [ + "Cinnamon Scented Wax Plant" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya mathilde", + "common_names": [ + "Mathilde Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya krohniana", + "common_names": [ + "Krohniana Hoya", + "Heart Leaf Lacunosa" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya kentiana", + "common_names": [ + "Kentiana Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya memoria", + "common_names": [ + "Memoria Hoya", + "Gracilis Memoria" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya spartioides", + "common_names": [ + "Spartioides Hoya", + "Leafless Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia obtusifolia", + "common_names": [ + "Baby Rubber Plant", + "American Rubber Plant" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia obtusifolia 'Variegata'", + "common_names": [ + "Variegated Baby Rubber Plant" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia caperata", + "common_names": [ + "Emerald Ripple Peperomia", + "Radiator Plant" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia caperata 'Rosso'", + "common_names": [ + "Rosso Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia caperata 'Red Luna'", + "common_names": [ + "Red Luna Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia caperata 'Frost'", + "common_names": [ + "Frost Peperomia", + "Silver Frost" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia argyreia", + "common_names": [ + "Watermelon Peperomia", + "Watermelon Begonia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia polybotrya", + "common_names": [ + "Raindrop Peperomia", + "Coin Leaf Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia prostrata", + "common_names": [ + "String of Turtles" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia rotundifolia", + "common_names": [ + "Trailing Jade", + "Round Leaf Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia clusiifolia", + "common_names": [ + "Red Edge Peperomia", + "Jelly Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia clusiifolia 'Ginny'", + "common_names": [ + "Ginny Peperomia", + "Rainbow Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia 'Hope'", + "common_names": [ + "Hope Peperomia", + "Trailing Jade" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia graveolens", + "common_names": [ + "Ruby Glow Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia nivalis", + "common_names": [ + "Nivalis Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia ferreyrae", + "common_names": [ + "Pincushion Peperomia", + "Happy Bean" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia columella", + "common_names": [ + "Columella Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia puteolata", + "common_names": [ + "Parallel Peperomia", + "Stilt Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia verticillata", + "common_names": [ + "Red Log Peperomia", + "Belly Button Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia albovittata", + "common_names": [ + "Piccolo Banda Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia tetraphylla", + "common_names": [ + "Acorn Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia quadrangularis", + "common_names": [ + "Beetle Peperomia", + "Angulata" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia incana", + "common_names": [ + "Felted Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia serpens", + "common_names": [ + "Vining Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Begonia rex", + "common_names": [ + "Rex Begonia", + "Painted Leaf Begonia", + "Fancy Leaf Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia rex 'Escargot'", + "common_names": [ + "Escargot Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia rex 'Fireworks'", + "common_names": [ + "Fireworks Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia rex 'Silver Dollar'", + "common_names": [ + "Silver Dollar Rex Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia maculata", + "common_names": [ + "Polka Dot Begonia", + "Spotted Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia maculata 'Wightii'", + "common_names": [ + "Wightii Begonia", + "Angel Wing Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia 'Angel Wing'", + "common_names": [ + "Angel Wing Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia 'Dragon Wing'", + "common_names": [ + "Dragon Wing Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia semperflorens", + "common_names": [ + "Wax Begonia", + "Bedding Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia tuberhybrida", + "common_names": [ + "Tuberous Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia erythrophylla", + "common_names": [ + "Beefsteak Begonia", + "Kidney Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia bowerae", + "common_names": [ + "Eyelash Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia bowerae 'Tiger'", + "common_names": [ + "Tiger Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia pavonina", + "common_names": [ + "Peacock Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia amphioxus", + "common_names": [ + "Spotted Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia listada", + "common_names": [ + "Striped Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia 'Marmaduke'", + "common_names": [ + "Marmaduke Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia 'Caribbean Night'", + "common_names": [ + "Caribbean Night Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia 'Amber Love'", + "common_names": [ + "Amber Love Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Dracaena trifasciata", + "common_names": [ + "Snake Plant", + "Mother-in-law's Tongue", + "Sansevieria" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena trifasciata 'Laurentii'", + "common_names": [ + "Laurentii Snake Plant", + "Variegated Snake Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena trifasciata 'Black Gold'", + "common_names": [ + "Black Gold Snake Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena trifasciata 'Black Coral'", + "common_names": [ + "Black Coral Snake Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena trifasciata 'Moonshine'", + "common_names": [ + "Moonshine Snake Plant", + "Silver Snake Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena trifasciata 'Hahnii'", + "common_names": [ + "Bird's Nest Snake Plant", + "Golden Hahnii" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena trifasciata 'Twisted Sister'", + "common_names": [ + "Twisted Sister Snake Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena masoniana", + "common_names": [ + "Whale Fin Snake Plant", + "Mason's Congo" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena cylindrica", + "common_names": [ + "Cylindrical Snake Plant", + "African Spear" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena 'Sayuri'", + "common_names": [ + "Sayuri Snake Plant", + "Metallica" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena kirkii", + "common_names": [ + "Star Sansevieria" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena fragrans", + "common_names": [ + "Corn Plant", + "Cornstalk Dracaena" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena fragrans 'Massangeana'", + "common_names": [ + "Mass Cane", + "Corn Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena fragrans 'Janet Craig'", + "common_names": [ + "Janet Craig Dracaena" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena fragrans 'Lemon Lime'", + "common_names": [ + "Lemon Lime Dracaena" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena fragrans 'Warneckii'", + "common_names": [ + "Warneckii Dracaena", + "Striped Dracaena" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena fragrans 'Lindenii'", + "common_names": [ + "Lindenii Dracaena" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena fragrans 'Victoria'", + "common_names": [ + "Victoria Dracaena" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena fragrans 'Hawaiian Sunshine'", + "common_names": [ + "Hawaiian Sunshine Dracaena" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena reflexa", + "common_names": [ + "Song of India", + "Pleomele" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena reflexa 'Song of Jamaica'", + "common_names": [ + "Song of Jamaica" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena marginata", + "common_names": [ + "Madagascar Dragon Tree", + "Dragon Tree" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena marginata 'Tricolor'", + "common_names": [ + "Tricolor Dragon Tree" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena marginata 'Colorama'", + "common_names": [ + "Colorama Dracaena" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena marginata 'Kiwi'", + "common_names": [ + "Kiwi Dracaena" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena sanderiana", + "common_names": [ + "Lucky Bamboo", + "Ribbon Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena surculosa", + "common_names": [ + "Gold Dust Dracaena", + "Spotted Dracaena" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena draco", + "common_names": [ + "Dragon Tree", + "Canary Islands Dragon Tree" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena goldieana", + "common_names": [ + "Zebra Striped Dragon Tree", + "Queen of Dracaenas" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena compacta", + "common_names": [ + "Compacta Dracaena", + "Janet Craig Compacta" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum", + "common_names": [ + "Arrowhead Plant", + "Arrowhead Vine", + "Goosefoot Plant" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum 'White Butterfly'", + "common_names": [ + "White Butterfly Syngonium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum 'Pink Allusion'", + "common_names": [ + "Pink Allusion Syngonium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum 'Neon Robusta'", + "common_names": [ + "Neon Robusta Syngonium", + "Pink Syngonium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum 'Strawberry Cream'", + "common_names": [ + "Strawberry Cream Syngonium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum 'Berry Allusion'", + "common_names": [ + "Berry Allusion Syngonium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum 'Bold Allusion'", + "common_names": [ + "Bold Allusion Syngonium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum 'Cream Allusion'", + "common_names": [ + "Cream Allusion Syngonium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum 'Golden Allusion'", + "common_names": [ + "Golden Allusion Syngonium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum 'Maria Allusion'", + "common_names": [ + "Maria Allusion Syngonium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum 'Batik'", + "common_names": [ + "Batik Syngonium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum 'Holly'", + "common_names": [ + "Holly Syngonium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum 'Mini Pixie'", + "common_names": [ + "Mini Pixie Syngonium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum 'Albo Variegata'", + "common_names": [ + "Albo Variegata Syngonium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum 'Regina Red'", + "common_names": [ + "Regina Red Syngonium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum 'Bronze Maria'", + "common_names": [ + "Bronze Maria Syngonium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum 'Emerald Gem'", + "common_names": [ + "Emerald Gem Syngonium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum 'Confetti'", + "common_names": [ + "Confetti Syngonium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium auritum", + "common_names": [ + "American Evergreen", + "Five Fingers" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema commutatum", + "common_names": [ + "Chinese Evergreen", + "Philippine Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Silver Bay'", + "common_names": [ + "Silver Bay Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Silver Queen'", + "common_names": [ + "Silver Queen Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Maria'", + "common_names": [ + "Maria Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Emerald Beauty'", + "common_names": [ + "Emerald Beauty Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Golden Bay'", + "common_names": [ + "Golden Bay Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Prestige'", + "common_names": [ + "Prestige Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Lady Valentine'", + "common_names": [ + "Lady Valentine Chinese Evergreen", + "Pink Aglaonema" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Pink Dalmatian'", + "common_names": [ + "Pink Dalmatian Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Red Star'", + "common_names": [ + "Red Star Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Red Zircon'", + "common_names": [ + "Red Zircon Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Chocolate'", + "common_names": [ + "Chocolate Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Butterfly'", + "common_names": [ + "Butterfly Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Cutlass'", + "common_names": [ + "Cutlass Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'White Lance'", + "common_names": [ + "White Lance Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Slim Jim'", + "common_names": [ + "Slim Jim Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema pictum 'Tricolor'", + "common_names": [ + "Camouflage Plant", + "Tricolor Aglaonema" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema nitidum", + "common_names": [ + "Silver Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Cecilia'", + "common_names": [ + "Cecilia Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Spathiphyllum wallisii", + "common_names": [ + "Peace Lily", + "White Sails", + "Spathe Flower" + ], + "family": "Araceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Spathiphyllum 'Sensation'", + "common_names": [ + "Sensation Peace Lily", + "Giant Peace Lily" + ], + "family": "Araceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Spathiphyllum 'Domino'", + "common_names": [ + "Domino Peace Lily", + "Variegated Peace Lily" + ], + "family": "Araceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Spathiphyllum 'Mauna Loa'", + "common_names": [ + "Mauna Loa Peace Lily" + ], + "family": "Araceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Spathiphyllum 'Petite'", + "common_names": [ + "Petite Peace Lily", + "Dwarf Peace Lily" + ], + "family": "Araceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Spathiphyllum 'Little Angel'", + "common_names": [ + "Little Angel Peace Lily" + ], + "family": "Araceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Spathiphyllum 'Power Petite'", + "common_names": [ + "Power Petite Peace Lily" + ], + "family": "Araceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Spathiphyllum 'Piccolino'", + "common_names": [ + "Piccolino Peace Lily" + ], + "family": "Araceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Spathiphyllum 'Clevelandii'", + "common_names": [ + "Clevelandii Peace Lily" + ], + "family": "Araceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Spathiphyllum 'Mojo Lime'", + "common_names": [ + "Mojo Lime Peace Lily" + ], + "family": "Araceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Spathiphyllum 'Gemini'", + "common_names": [ + "Gemini Peace Lily" + ], + "family": "Araceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Spathiphyllum 'Jet Diamond'", + "common_names": [ + "Jet Diamond Peace Lily" + ], + "family": "Araceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Spathiphyllum 'Pearl Cupido'", + "common_names": [ + "Pearl Cupido Peace Lily" + ], + "family": "Araceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Spathiphyllum 'Sweet Paco'", + "common_names": [ + "Sweet Paco Peace Lily", + "Fragrant Peace Lily" + ], + "family": "Araceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Spathiphyllum 'Platinum Mist'", + "common_names": [ + "Platinum Mist Peace Lily" + ], + "family": "Araceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Anthurium andraeanum", + "common_names": [ + "Flamingo Flower", + "Laceleaf", + "Tailflower" + ], + "family": "Araceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Anthurium scherzerianum", + "common_names": [ + "Pigtail Anthurium", + "Flamingo Lily" + ], + "family": "Araceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Anthurium crystallinum", + "common_names": [ + "Crystal Anthurium", + "Velvet Cardboard Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium clarinervium", + "common_names": [ + "Velvet Cardboard Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium warocqueanum", + "common_names": [ + "Queen Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium veitchii", + "common_names": [ + "King Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium superbum", + "common_names": [ + "Superbum Anthurium", + "Bird's Nest Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium forgetii", + "common_names": [ + "Forget's Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium bonplandii", + "common_names": [ + "Bonplandii Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium magnificum", + "common_names": [ + "Magnificum Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium 'Ace of Spades'", + "common_names": [ + "Ace of Spades Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium 'Cheers'", + "common_names": [ + "Cheers Anthurium" + ], + "family": "Araceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Anthurium 'Small Talk'", + "common_names": [ + "Small Talk Anthurium" + ], + "family": "Araceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Anthurium pedatum", + "common_names": [ + "Pedatum Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium radicans", + "common_names": [ + "Radicans Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium vittarifolium", + "common_names": [ + "Strap Leaf Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium regale", + "common_names": [ + "Regale Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Zamioculcas zamiifolia", + "common_names": [ + "ZZ Plant", + "Zanzibar Gem", + "Eternity Plant" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Zamioculcas zamiifolia 'Raven'", + "common_names": [ + "Raven ZZ Plant", + "Black ZZ Plant" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Zamioculcas zamiifolia 'Zenzi'", + "common_names": [ + "Zenzi ZZ Plant", + "Dwarf ZZ Plant" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Zamioculcas zamiifolia 'Variegata'", + "common_names": [ + "Variegated ZZ Plant" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Zamioculcas zamiifolia 'Chameleon'", + "common_names": [ + "Chameleon ZZ Plant" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dieffenbachia seguine", + "common_names": [ + "Dumb Cane", + "Leopard Lily" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dieffenbachia 'Camille'", + "common_names": [ + "Camille Dieffenbachia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dieffenbachia 'Tropic Snow'", + "common_names": [ + "Tropic Snow Dieffenbachia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dieffenbachia 'Compacta'", + "common_names": [ + "Compacta Dieffenbachia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dieffenbachia 'Sparkles'", + "common_names": [ + "Sparkles Dieffenbachia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dieffenbachia 'Star Bright'", + "common_names": [ + "Star Bright Dieffenbachia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dieffenbachia 'Mary'", + "common_names": [ + "Mary Dieffenbachia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dieffenbachia maculata", + "common_names": [ + "Spotted Dumb Cane" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dieffenbachia 'Reflector'", + "common_names": [ + "Reflector Dieffenbachia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Nephrolepis exaltata", + "common_names": [ + "Boston Fern", + "Sword Fern" + ], + "family": "Lomariopsidaceae", + "category": "Fern" + }, + { + "scientific_name": "Nephrolepis exaltata 'Bostoniensis'", + "common_names": [ + "Boston Fern" + ], + "family": "Lomariopsidaceae", + "category": "Fern" + }, + { + "scientific_name": "Nephrolepis exaltata 'Fluffy Ruffles'", + "common_names": [ + "Fluffy Ruffles Fern" + ], + "family": "Lomariopsidaceae", + "category": "Fern" + }, + { + "scientific_name": "Nephrolepis exaltata 'Tiger Fern'", + "common_names": [ + "Tiger Fern", + "Variegated Boston Fern" + ], + "family": "Lomariopsidaceae", + "category": "Fern" + }, + { + "scientific_name": "Nephrolepis obliterata", + "common_names": [ + "Kimberley Queen Fern", + "Australian Sword Fern" + ], + "family": "Lomariopsidaceae", + "category": "Fern" + }, + { + "scientific_name": "Asplenium nidus", + "common_names": [ + "Bird's Nest Fern" + ], + "family": "Aspleniaceae", + "category": "Fern" + }, + { + "scientific_name": "Asplenium nidus 'Crispy Wave'", + "common_names": [ + "Crispy Wave Fern" + ], + "family": "Aspleniaceae", + "category": "Fern" + }, + { + "scientific_name": "Asplenium nidus 'Osaka'", + "common_names": [ + "Osaka Fern" + ], + "family": "Aspleniaceae", + "category": "Fern" + }, + { + "scientific_name": "Asplenium antiquum", + "common_names": [ + "Japanese Bird's Nest Fern" + ], + "family": "Aspleniaceae", + "category": "Fern" + }, + { + "scientific_name": "Adiantum raddianum", + "common_names": [ + "Maidenhair Fern", + "Delta Maidenhair" + ], + "family": "Pteridaceae", + "category": "Fern" + }, + { + "scientific_name": "Adiantum capillus-veneris", + "common_names": [ + "Southern Maidenhair Fern", + "Venus Hair Fern" + ], + "family": "Pteridaceae", + "category": "Fern" + }, + { + "scientific_name": "Adiantum pedatum", + "common_names": [ + "Northern Maidenhair Fern" + ], + "family": "Pteridaceae", + "category": "Fern" + }, + { + "scientific_name": "Platycerium bifurcatum", + "common_names": [ + "Staghorn Fern", + "Elkhorn Fern" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Platycerium superbum", + "common_names": [ + "Giant Staghorn Fern" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Davallia fejeensis", + "common_names": [ + "Rabbit's Foot Fern" + ], + "family": "Davalliaceae", + "category": "Fern" + }, + { + "scientific_name": "Davallia canariensis", + "common_names": [ + "Deer Foot Fern", + "Hare's Foot Fern" + ], + "family": "Davalliaceae", + "category": "Fern" + }, + { + "scientific_name": "Humata tyermannii", + "common_names": [ + "Bear's Paw Fern" + ], + "family": "Davalliaceae", + "category": "Fern" + }, + { + "scientific_name": "Microsorum diversifolium", + "common_names": [ + "Kangaroo Paw Fern" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Microsorum musifolium 'Crocodyllus'", + "common_names": [ + "Crocodile Fern" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Pteris cretica", + "common_names": [ + "Cretan Brake Fern", + "Ribbon Fern" + ], + "family": "Pteridaceae", + "category": "Fern" + }, + { + "scientific_name": "Pteris ensiformis", + "common_names": [ + "Silver Lace Fern", + "Slender Brake Fern" + ], + "family": "Pteridaceae", + "category": "Fern" + }, + { + "scientific_name": "Pellaea rotundifolia", + "common_names": [ + "Button Fern" + ], + "family": "Pteridaceae", + "category": "Fern" + }, + { + "scientific_name": "Pellaea falcata", + "common_names": [ + "Sickle Fern" + ], + "family": "Pteridaceae", + "category": "Fern" + }, + { + "scientific_name": "Phlebodium aureum", + "common_names": [ + "Blue Star Fern", + "Golden Polypody" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Phlebodium aureum 'Blue Star'", + "common_names": [ + "Blue Star Fern" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Cyrtomium falcatum", + "common_names": [ + "Holly Fern", + "Japanese Holly Fern" + ], + "family": "Dryopteridaceae", + "category": "Fern" + }, + { + "scientific_name": "Blechnum gibbum", + "common_names": [ + "Silver Lady Fern", + "Dwarf Tree Fern" + ], + "family": "Blechnaceae", + "category": "Fern" + }, + { + "scientific_name": "Selaginella kraussiana", + "common_names": [ + "Krauss's Spikemoss", + "Spreading Clubmoss" + ], + "family": "Selaginellaceae", + "category": "Fern" + }, + { + "scientific_name": "Selaginella martensii", + "common_names": [ + "Martens's Spikemoss" + ], + "family": "Selaginellaceae", + "category": "Fern" + }, + { + "scientific_name": "Asparagus setaceus", + "common_names": [ + "Asparagus Fern", + "Lace Fern" + ], + "family": "Asparagaceae", + "category": "Fern" + }, + { + "scientific_name": "Asparagus densiflorus", + "common_names": [ + "Foxtail Fern", + "Asparagus Fern" + ], + "family": "Asparagaceae", + "category": "Fern" + }, + { + "scientific_name": "Asparagus densiflorus 'Sprengeri'", + "common_names": [ + "Sprenger's Asparagus Fern" + ], + "family": "Asparagaceae", + "category": "Fern" + }, + { + "scientific_name": "Asparagus densiflorus 'Myersii'", + "common_names": [ + "Foxtail Fern", + "Myers Fern" + ], + "family": "Asparagaceae", + "category": "Fern" + }, + { + "scientific_name": "Dypsis lutescens", + "common_names": [ + "Areca Palm", + "Butterfly Palm", + "Golden Cane Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Howea forsteriana", + "common_names": [ + "Kentia Palm", + "Paradise Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Howea belmoreana", + "common_names": [ + "Sentry Palm", + "Curly Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Chamaedorea elegans", + "common_names": [ + "Parlor Palm", + "Neanthe Bella Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Chamaedorea seifrizii", + "common_names": [ + "Bamboo Palm", + "Reed Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Chamaedorea cataractarum", + "common_names": [ + "Cat Palm", + "Cascade Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Chamaedorea metallica", + "common_names": [ + "Metallic Palm", + "Miniature Fishtail Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Rhapis excelsa", + "common_names": [ + "Lady Palm", + "Bamboo Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Ravenea rivularis", + "common_names": [ + "Majesty Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Livistona chinensis", + "common_names": [ + "Chinese Fan Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Chamaerops humilis", + "common_names": [ + "European Fan Palm", + "Mediterranean Fan Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Phoenix roebelenii", + "common_names": [ + "Pygmy Date Palm", + "Miniature Date Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Phoenix canariensis", + "common_names": [ + "Canary Island Date Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Caryota mitis", + "common_names": [ + "Fishtail Palm", + "Clustering Fishtail Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Cyrtostachys renda", + "common_names": [ + "Lipstick Palm", + "Red Sealing Wax Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Beaucarnea recurvata", + "common_names": [ + "Ponytail Palm", + "Elephant's Foot" + ], + "family": "Asparagaceae", + "category": "Palm" + }, + { + "scientific_name": "Cycas revoluta", + "common_names": [ + "Sago Palm", + "Japanese Sago Palm" + ], + "family": "Cycadaceae", + "category": "Palm" + }, + { + "scientific_name": "Echeveria elegans", + "common_names": [ + "Mexican Snowball", + "White Mexican Rose" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Lola'", + "common_names": [ + "Lola Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Perle von Nurnberg'", + "common_names": [ + "Perle von Nurnberg" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Black Prince'", + "common_names": [ + "Black Prince Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria agavoides", + "common_names": [ + "Lipstick Echeveria", + "Molded Wax Agave" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria agavoides 'Lipstick'", + "common_names": [ + "Lipstick Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria pulvinata", + "common_names": [ + "Chenille Plant", + "Plush Plant" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria setosa", + "common_names": [ + "Mexican Firecracker", + "Firecracker Plant" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Cubic Frost'", + "common_names": [ + "Cubic Frost Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Raindrops'", + "common_names": [ + "Raindrops Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Tippy'", + "common_names": [ + "Tippy Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Dusty Rose'", + "common_names": [ + "Dusty Rose Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Chroma'", + "common_names": [ + "Chroma Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Melaco'", + "common_names": [ + "Melaco Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Neon Breakers'", + "common_names": [ + "Neon Breakers Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Topsy Turvy'", + "common_names": [ + "Topsy Turvy Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria imbricata", + "common_names": [ + "Blue Rose Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Doris Taylor'", + "common_names": [ + "Woolly Rose" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria harmsii", + "common_names": [ + "Ruby Slippers", + "Red Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria gibbiflora", + "common_names": [ + "Gibbiflora Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula ovata", + "common_names": [ + "Jade Plant", + "Money Plant", + "Lucky Plant" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula ovata 'Gollum'", + "common_names": [ + "Gollum Jade", + "Ogre Ears" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula ovata 'Hobbit'", + "common_names": [ + "Hobbit Jade", + "Finger Jade" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula ovata 'Hummel's Sunset'", + "common_names": [ + "Sunset Jade", + "Golden Jade" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula ovata 'Variegata'", + "common_names": [ + "Variegated Jade Plant", + "Tricolor Jade" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula perforata", + "common_names": [ + "String of Buttons" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula arborescens", + "common_names": [ + "Silver Dollar Plant", + "Blue Bird" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula muscosa", + "common_names": [ + "Watch Chain", + "Princess Pine", + "Zipper Plant" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula capitella", + "common_names": [ + "Campfire Plant", + "Red Pagoda" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula 'Baby's Necklace'", + "common_names": [ + "Baby's Necklace" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula pellucida", + "common_names": [ + "Calico Kitten", + "Variegated Crassula" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula falcata", + "common_names": [ + "Propeller Plant", + "Airplane Plant" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula rupestris", + "common_names": [ + "Baby's Necklace", + "Rosary Vine" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum morganianum", + "common_names": [ + "Burro's Tail", + "Donkey Tail" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum burrito", + "common_names": [ + "Baby Burro's Tail", + "Burrito Sedum" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum rubrotinctum", + "common_names": [ + "Jelly Bean Plant", + "Pork and Beans" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum nussbaumerianum", + "common_names": [ + "Coppertone Sedum", + "Golden Sedum" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum adolphii", + "common_names": [ + "Golden Glow Sedum", + "Golden Sedum" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum pachyphyllum", + "common_names": [ + "Jelly Bean Sedum", + "Blue Jelly Bean" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum dasyphyllum", + "common_names": [ + "Corsican Stonecrop", + "Blue Tears Sedum" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum spurium", + "common_names": [ + "Two-Row Stonecrop", + "Dragon's Blood Sedum" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum makinoi", + "common_names": [ + "Golden Japanese Sedum" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum tectorum", + "common_names": [ + "Hens and Chicks", + "Common Houseleek" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum arachnoideum", + "common_names": [ + "Cobweb Houseleek", + "Spider Web Hens and Chicks" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum 'Ruby Heart'", + "common_names": [ + "Ruby Heart Sempervivum" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum 'Black'", + "common_names": [ + "Black Sempervivum" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aeonium arboreum", + "common_names": [ + "Tree Aeonium", + "Tree Houseleek" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aeonium arboreum 'Zwartkop'", + "common_names": [ + "Black Rose", + "Black Tree Aeonium" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aeonium 'Sunburst'", + "common_names": [ + "Sunburst Aeonium" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aeonium 'Kiwi'", + "common_names": [ + "Kiwi Aeonium", + "Dream Color" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aeonium haworthii", + "common_names": [ + "Pinwheel Aeonium" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Graptoveria 'Fred Ives'", + "common_names": [ + "Fred Ives" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Graptoveria 'Opalina'", + "common_names": [ + "Opalina Graptoveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Graptoveria 'Debbie'", + "common_names": [ + "Debbie Graptoveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Graptopetalum paraguayense", + "common_names": [ + "Ghost Plant", + "Mother of Pearl Plant" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Pachyphytum oviferum", + "common_names": [ + "Moonstones", + "Sugar Almond Plant" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Pachyphytum compactum", + "common_names": [ + "Thick Plant" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Kalanchoe blossfeldiana", + "common_names": [ + "Flaming Katy", + "Christmas Kalanchoe" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Kalanchoe tomentosa", + "common_names": [ + "Panda Plant", + "Chocolate Soldier" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Kalanchoe daigremontiana", + "common_names": [ + "Mother of Thousands", + "Alligator Plant" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Kalanchoe fedtschenkoi", + "common_names": [ + "Lavender Scallops" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Kalanchoe luciae", + "common_names": [ + "Paddle Plant", + "Flapjack Plant" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Kalanchoe thyrsiflora", + "common_names": [ + "Flapjack Kalanchoe", + "Desert Cabbage" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Kalanchoe pinnata", + "common_names": [ + "Cathedral Bells", + "Air Plant" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Kalanchoe marnieriana", + "common_names": [ + "Marnier's Kalanchoe" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Kalanchoe beharensis", + "common_names": [ + "Elephant's Ear Kalanchoe", + "Felt Bush" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Kalanchoe 'Pink Butterflies'", + "common_names": [ + "Pink Butterflies" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Kalanchoe longiflora", + "common_names": [ + "Long Flower Kalanchoe", + "Tugela Cliff Kalanchoe" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia fasciata", + "common_names": [ + "Zebra Plant", + "Zebra Haworthia" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia attenuata", + "common_names": [ + "Zebra Cactus", + "Zebra Plant" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia cooperi", + "common_names": [ + "Cooper's Haworthia", + "Transparent Haworthia" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia truncata", + "common_names": [ + "Horse's Teeth", + "Truncata Haworthia" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia retusa", + "common_names": [ + "Star Cactus", + "Window Haworthia" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia cymbiformis", + "common_names": [ + "Cathedral Window Haworthia" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia obtusa", + "common_names": [ + "Obtusa Haworthia" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia reinwardtii", + "common_names": [ + "Zebra Wart" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Gasteria gracilis", + "common_names": [ + "Ox Tongue", + "Lawyer's Tongue" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Gasteria carinata", + "common_names": [ + "Keeled Gasteria" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Gasteria 'Little Warty'", + "common_names": [ + "Little Warty Gasteria" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe vera", + "common_names": [ + "Aloe Vera", + "Medicinal Aloe", + "True Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe aristata", + "common_names": [ + "Lace Aloe", + "Torch Plant" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe variegata", + "common_names": [ + "Tiger Aloe", + "Partridge Breast Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe nobilis", + "common_names": [ + "Gold Tooth Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe brevifolia", + "common_names": [ + "Short-leaved Aloe", + "Crocodile Jaws" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe juvenna", + "common_names": [ + "Tiger Tooth Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe humilis", + "common_names": [ + "Spider Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe striata", + "common_names": [ + "Coral Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe cameronii", + "common_names": [ + "Red Aloe", + "Cameron's Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe arborescens", + "common_names": [ + "Candelabra Aloe", + "Krantz Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe plicatilis", + "common_names": [ + "Fan Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe rauhii", + "common_names": [ + "Snowflake Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe 'Crosby's Prolific'", + "common_names": [ + "Crosby's Prolific Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Senecio rowleyanus", + "common_names": [ + "String of Pearls" + ], + "family": "Asteraceae", + "category": "Succulent" + }, + { + "scientific_name": "Senecio herreianus", + "common_names": [ + "String of Watermelons", + "String of Beads" + ], + "family": "Asteraceae", + "category": "Succulent" + }, + { + "scientific_name": "Senecio radicans", + "common_names": [ + "String of Bananas", + "Fishhook Senecio" + ], + "family": "Asteraceae", + "category": "Succulent" + }, + { + "scientific_name": "Senecio peregrinus", + "common_names": [ + "String of Dolphins" + ], + "family": "Asteraceae", + "category": "Succulent" + }, + { + "scientific_name": "Senecio serpens", + "common_names": [ + "Blue Chalksticks" + ], + "family": "Asteraceae", + "category": "Succulent" + }, + { + "scientific_name": "Senecio mandraliscae", + "common_names": [ + "Blue Finger", + "Blue Chalk Sticks" + ], + "family": "Asteraceae", + "category": "Succulent" + }, + { + "scientific_name": "Curio ficoides", + "common_names": [ + "Mount Everest Senecio", + "Blue Pickle Plant" + ], + "family": "Asteraceae", + "category": "Succulent" + }, + { + "scientific_name": "Lithops species", + "common_names": [ + "Living Stones", + "Pebble Plants" + ], + "family": "Aizoaceae", + "category": "Succulent" + }, + { + "scientific_name": "Lithops lesliei", + "common_names": [ + "Leslie's Living Stone" + ], + "family": "Aizoaceae", + "category": "Succulent" + }, + { + "scientific_name": "Lithops karasmontana", + "common_names": [ + "Karas Mountains Living Stone" + ], + "family": "Aizoaceae", + "category": "Succulent" + }, + { + "scientific_name": "Lithops aucampiae", + "common_names": [ + "Aucamp's Living Stone" + ], + "family": "Aizoaceae", + "category": "Succulent" + }, + { + "scientific_name": "Fenestraria rhopalophylla", + "common_names": [ + "Baby Toes" + ], + "family": "Aizoaceae", + "category": "Succulent" + }, + { + "scientific_name": "Faucaria tigrina", + "common_names": [ + "Tiger Jaws" + ], + "family": "Aizoaceae", + "category": "Succulent" + }, + { + "scientific_name": "Pleiospilos nelii", + "common_names": [ + "Split Rock", + "Cleft Stone" + ], + "family": "Aizoaceae", + "category": "Succulent" + }, + { + "scientific_name": "Cotyledon tomentosa", + "common_names": [ + "Bear Paws" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Cotyledon orbiculata", + "common_names": [ + "Pig's Ear", + "Round-leafed Navel-wort" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Portulacaria afra", + "common_names": [ + "Elephant Bush", + "Dwarf Jade", + "Spekboom" + ], + "family": "Didiereaceae", + "category": "Succulent" + }, + { + "scientific_name": "Portulacaria afra 'Variegata'", + "common_names": [ + "Rainbow Bush", + "Variegated Elephant Bush" + ], + "family": "Didiereaceae", + "category": "Succulent" + }, + { + "scientific_name": "Opuntia microdasys", + "common_names": [ + "Bunny Ears Cactus", + "Angel Wings" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Opuntia microdasys var. albata", + "common_names": [ + "White Bunny Ears" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Opuntia microdasys var. rufida", + "common_names": [ + "Cinnamon Bunny Ears" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Opuntia ficus-indica", + "common_names": [ + "Prickly Pear Cactus", + "Indian Fig" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria species", + "common_names": [ + "Pincushion Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria hahniana", + "common_names": [ + "Old Lady Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria plumosa", + "common_names": [ + "Feather Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria elongata", + "common_names": [ + "Ladyfinger Cactus", + "Gold Lace Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria spinosissima", + "common_names": [ + "Spiny Pincushion Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria bocasana", + "common_names": [ + "Powder Puff Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria gracilis", + "common_names": [ + "Thimble Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Echinocactus grusonii", + "common_names": [ + "Golden Barrel Cactus", + "Mother-in-law's Cushion" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Ferocactus species", + "common_names": [ + "Barrel Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Gymnocalycium mihanovichii", + "common_names": [ + "Moon Cactus", + "Ruby Ball Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Schlumbergera truncata", + "common_names": [ + "Thanksgiving Cactus", + "Crab Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Schlumbergera x buckleyi", + "common_names": [ + "Christmas Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Hatiora gaertneri", + "common_names": [ + "Easter Cactus", + "Spring Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Rhipsalis baccifera", + "common_names": [ + "Mistletoe Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Rhipsalis cereuscula", + "common_names": [ + "Coral Cactus", + "Rice Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Rhipsalis cassutha", + "common_names": [ + "Chain Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Rhipsalis paradoxa", + "common_names": [ + "Chain Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Epiphyllum oxypetalum", + "common_names": [ + "Queen of the Night", + "Orchid Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Epiphyllum anguliger", + "common_names": [ + "Fishbone Cactus", + "Zig Zag Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Cereus peruvianus", + "common_names": [ + "Peruvian Apple Cactus", + "Column Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Cereus repandus", + "common_names": [ + "Peruvian Apple", + "Giant Club Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Cereus jamacaru", + "common_names": [ + "Blue Candle Cactus", + "Mandacaru" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Espostoa lanata", + "common_names": [ + "Old Man of the Andes", + "Peruvian Old Man" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Cephalocereus senilis", + "common_names": [ + "Old Man Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Pilosocereus azureus", + "common_names": [ + "Blue Torch Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Astrophytum asterias", + "common_names": [ + "Sand Dollar Cactus", + "Sea Urchin Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Astrophytum myriostigma", + "common_names": [ + "Bishop's Cap Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Astrophytum ornatum", + "common_names": [ + "Star Cactus", + "Monk's Hood" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Echinopsis species", + "common_names": [ + "Sea Urchin Cactus", + "Easter Lily Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Echinopsis chamaecereus", + "common_names": [ + "Peanut Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Cleistocactus strausii", + "common_names": [ + "Silver Torch Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Rebutia minuscula", + "common_names": [ + "Crown Cactus", + "Red Crown Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Notocactus species", + "common_names": [ + "Ball Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Myrtillocactus geometrizans", + "common_names": [ + "Blue Candle", + "Blue Myrtle Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Euphorbia trigona", + "common_names": [ + "African Milk Tree", + "Cathedral Cactus" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia trigona 'Rubra'", + "common_names": [ + "Red African Milk Tree" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia tirucalli", + "common_names": [ + "Pencil Cactus", + "Firesticks" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia tirucalli 'Sticks on Fire'", + "common_names": [ + "Sticks on Fire", + "Fire Sticks" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia lactea", + "common_names": [ + "Dragon Bones", + "Mottled Spurge" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia lactea 'Cristata'", + "common_names": [ + "Coral Cactus", + "Crested Euphorbia" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia ingens", + "common_names": [ + "Candelabra Tree", + "Cowboy Cactus" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia mammillaris", + "common_names": [ + "Corn Cob Cactus", + "Indian Corn Cob" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia obesa", + "common_names": [ + "Baseball Plant", + "Living Baseball" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia milii", + "common_names": [ + "Crown of Thorns" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia pulcherrima", + "common_names": [ + "Poinsettia", + "Christmas Star" + ], + "family": "Euphorbiaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Tillandsia ionantha", + "common_names": [ + "Sky Plant", + "Ionantha Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia xerographica", + "common_names": [ + "King of Tillandsias", + "Xerographica Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia caput-medusae", + "common_names": [ + "Medusa's Head", + "Medusa Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia streptophylla", + "common_names": [ + "Shirley Temple", + "Curly Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia tectorum", + "common_names": [ + "Snowball Air Plant", + "Fuzzy Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia aeranthos", + "common_names": [ + "Aeranthos Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia bulbosa", + "common_names": [ + "Bulbosa Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia stricta", + "common_names": [ + "Stricta Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia capitata", + "common_names": [ + "Capitata Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia brachycaulos", + "common_names": [ + "Brachycaulos Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia usneoides", + "common_names": [ + "Spanish Moss" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia recurvata", + "common_names": [ + "Ball Moss" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia juncea", + "common_names": [ + "Rush Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia bergeri", + "common_names": [ + "Bergeri Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia crocata", + "common_names": [ + "Saffron Air Plant", + "Fragrant Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia purpurea", + "common_names": [ + "Purple Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia fuchsii", + "common_names": [ + "Fuchsii Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia harrisii", + "common_names": [ + "Harris's Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia kolbii", + "common_names": [ + "Kolbii Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia velutina", + "common_names": [ + "Velutina Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Guzmania lingulata", + "common_names": [ + "Scarlet Star", + "Guzmania Bromeliad" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Guzmania 'Tempo'", + "common_names": [ + "Tempo Guzmania" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Guzmania 'Tala'", + "common_names": [ + "Tala Guzmania" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Guzmania 'Luna'", + "common_names": [ + "Luna Guzmania" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Guzmania 'Grand Prix'", + "common_names": [ + "Grand Prix Guzmania" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Vriesea splendens", + "common_names": [ + "Flaming Sword", + "Painted Feather" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Vriesea 'Christiane'", + "common_names": [ + "Christiane Vriesea" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Vriesea 'Era'", + "common_names": [ + "Era Vriesea" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Vriesea hieroglyphica", + "common_names": [ + "King of the Bromeliads" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Neoregelia carolinae", + "common_names": [ + "Blushing Bromeliad" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Neoregelia spectabilis", + "common_names": [ + "Painted Fingernail Plant" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Neoregelia 'Fireball'", + "common_names": [ + "Fireball Bromeliad" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Neoregelia 'Domino'", + "common_names": [ + "Domino Bromeliad" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Aechmea fasciata", + "common_names": [ + "Silver Vase Plant", + "Urn Plant" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Aechmea 'Blue Rain'", + "common_names": [ + "Blue Rain Aechmea" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Billbergia nutans", + "common_names": [ + "Queen's Tears", + "Friendship Plant" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Cryptanthus bivittatus", + "common_names": [ + "Earth Star", + "Starfish Plant" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Cryptanthus zonatus", + "common_names": [ + "Zebra Plant" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Ananas comosus", + "common_names": [ + "Pineapple Plant", + "Ornamental Pineapple" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Phalaenopsis species", + "common_names": [ + "Moth Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Phalaenopsis amabilis", + "common_names": [ + "Moon Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Phalaenopsis equestris", + "common_names": [ + "Horse Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Phalaenopsis schilleriana", + "common_names": [ + "Schiller's Phalaenopsis" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Phalaenopsis stuartiana", + "common_names": [ + "Stuart's Phalaenopsis" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Dendrobium nobile", + "common_names": [ + "Noble Dendrobium", + "Nobile Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Dendrobium phalaenopsis", + "common_names": [ + "Cooktown Orchid", + "Den-Phal" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Dendrobium kingianum", + "common_names": [ + "Pink Rock Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Dendrobium anosmum", + "common_names": [ + "Unscented Dendrobium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Oncidium species", + "common_names": [ + "Dancing Lady Orchid", + "Golden Shower Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Oncidium 'Sharry Baby'", + "common_names": [ + "Chocolate Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Oncidium 'Twinkle'", + "common_names": [ + "Twinkle Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Cymbidium species", + "common_names": [ + "Boat Orchid", + "Cymbidium Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Cattleya species", + "common_names": [ + "Corsage Orchid", + "Cattleya Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Paphiopedilum species", + "common_names": [ + "Slipper Orchid", + "Venus Slipper" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Miltoniopsis species", + "common_names": [ + "Pansy Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Vanda species", + "common_names": [ + "Vanda Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Cambria species", + "common_names": [ + "Cambria Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Zygopetalum species", + "common_names": [ + "Zygo Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Ludisia discolor", + "common_names": [ + "Jewel Orchid", + "Black Jewel Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Macodes petola", + "common_names": [ + "Lightning Orchid", + "Gold Veined Jewel Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Streptocarpus ionanthus", + "common_names": [ + "African Violet", + "Saintpaulia" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Streptocarpus ionanthus 'Blue Boy'", + "common_names": [ + "Blue Boy African Violet" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Streptocarpus ionanthus 'Rob's Antique Rose'", + "common_names": [ + "Rob's Antique Rose" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Streptocarpus ionanthus 'Rhapsodie Lisa'", + "common_names": [ + "Rhapsodie Lisa" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Streptocarpus ionanthus 'Little Delight'", + "common_names": [ + "Little Delight" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Streptocarpus saxorum", + "common_names": [ + "Cape Primrose", + "False African Violet" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Episcia cupreata", + "common_names": [ + "Flame Violet", + "Chocolate Soldier" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Columnea species", + "common_names": [ + "Goldfish Plant", + "Flying Goldfish Plant" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Nematanthus gregarius", + "common_names": [ + "Goldfish Plant", + "Guppy Plant" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Aeschynanthus radicans", + "common_names": [ + "Lipstick Plant" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Aeschynanthus 'Mona Lisa'", + "common_names": [ + "Mona Lisa Lipstick Plant" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Aeschynanthus 'Rasta'", + "common_names": [ + "Rasta Lipstick Plant", + "Curly Lipstick Plant" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Saintpaulia grotei", + "common_names": [ + "Trailing African Violet" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Sinningia speciosa", + "common_names": [ + "Gloxinia", + "Florist's Gloxinia" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Kohleria species", + "common_names": [ + "Kohleria", + "Tree Gloxinia" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Clivia miniata", + "common_names": [ + "Kaffir Lily", + "Bush Lily", + "Fire Lily" + ], + "family": "Amaryllidaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Hippeastrum species", + "common_names": [ + "Amaryllis", + "Knight's Star Lily" + ], + "family": "Amaryllidaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Hippeastrum 'Red Lion'", + "common_names": [ + "Red Lion Amaryllis" + ], + "family": "Amaryllidaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Hippeastrum 'Apple Blossom'", + "common_names": [ + "Apple Blossom Amaryllis" + ], + "family": "Amaryllidaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Cyclamen persicum", + "common_names": [ + "Florist's Cyclamen", + "Persian Cyclamen" + ], + "family": "Primulaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Primula obconica", + "common_names": [ + "Fairy Primrose", + "Poison Primrose" + ], + "family": "Primulaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Primula vulgaris", + "common_names": [ + "Common Primrose", + "English Primrose" + ], + "family": "Primulaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Impatiens walleriana", + "common_names": [ + "Busy Lizzie", + "Impatiens" + ], + "family": "Balsaminaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Impatiens hawkeri", + "common_names": [ + "New Guinea Impatiens" + ], + "family": "Balsaminaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Hibiscus rosa-sinensis", + "common_names": [ + "Chinese Hibiscus", + "Tropical Hibiscus" + ], + "family": "Malvaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Hibiscus syriacus", + "common_names": [ + "Rose of Sharon" + ], + "family": "Malvaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Gardenia jasminoides", + "common_names": [ + "Gardenia", + "Cape Jasmine" + ], + "family": "Rubiaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Jasminum sambac", + "common_names": [ + "Arabian Jasmine", + "Sampaguita" + ], + "family": "Oleaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Jasminum polyanthum", + "common_names": [ + "Pink Jasmine", + "Winter Jasmine" + ], + "family": "Oleaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Stephanotis floribunda", + "common_names": [ + "Madagascar Jasmine", + "Bridal Wreath" + ], + "family": "Apocynaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Mandevilla sanderi", + "common_names": [ + "Brazilian Jasmine", + "Dipladenia" + ], + "family": "Apocynaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Oxalis triangularis", + "common_names": [ + "Purple Shamrock", + "Love Plant" + ], + "family": "Oxalidaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Oxalis triangularis 'Fanny'", + "common_names": [ + "Green Shamrock" + ], + "family": "Oxalidaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Oxalis regnellii", + "common_names": [ + "Lucky Shamrock" + ], + "family": "Oxalidaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Oxalis deppei", + "common_names": [ + "Iron Cross Oxalis", + "Good Luck Plant" + ], + "family": "Oxalidaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Pelargonium species", + "common_names": [ + "Geranium", + "Pelargonium" + ], + "family": "Geraniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Pelargonium x hortorum", + "common_names": [ + "Zonal Geranium", + "Garden Geranium" + ], + "family": "Geraniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Pelargonium peltatum", + "common_names": [ + "Ivy Geranium", + "Trailing Geranium" + ], + "family": "Geraniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Pelargonium graveolens", + "common_names": [ + "Rose Geranium", + "Scented Geranium" + ], + "family": "Geraniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Pelargonium x domesticum", + "common_names": [ + "Regal Geranium", + "Martha Washington Geranium" + ], + "family": "Geraniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Hedera helix", + "common_names": [ + "English Ivy", + "Common Ivy" + ], + "family": "Araliaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Hedera helix 'Glacier'", + "common_names": [ + "Glacier Ivy" + ], + "family": "Araliaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Hedera helix 'Gold Child'", + "common_names": [ + "Gold Child Ivy" + ], + "family": "Araliaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Hedera helix 'Needlepoint'", + "common_names": [ + "Needlepoint Ivy" + ], + "family": "Araliaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Hedera helix 'Duckfoot'", + "common_names": [ + "Duckfoot Ivy" + ], + "family": "Araliaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Hedera helix 'Ivalace'", + "common_names": [ + "Ivalace Ivy" + ], + "family": "Araliaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Hedera helix 'Anne Marie'", + "common_names": [ + "Anne Marie Ivy" + ], + "family": "Araliaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Hedera helix 'Little Diamond'", + "common_names": [ + "Little Diamond Ivy" + ], + "family": "Araliaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Hedera helix 'Midas Touch'", + "common_names": [ + "Midas Touch Ivy" + ], + "family": "Araliaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Hedera algeriensis", + "common_names": [ + "Algerian Ivy" + ], + "family": "Araliaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Hedera canariensis", + "common_names": [ + "Canary Island Ivy" + ], + "family": "Araliaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Tradescantia zebrina", + "common_names": [ + "Wandering Jew", + "Inch Plant", + "Silver Inch Plant" + ], + "family": "Commelinaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Tradescantia zebrina 'Burgundy'", + "common_names": [ + "Burgundy Wandering Jew" + ], + "family": "Commelinaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Tradescantia fluminensis", + "common_names": [ + "Small-Leaf Spiderwort", + "Wandering Jew" + ], + "family": "Commelinaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Tradescantia fluminensis 'Quicksilver'", + "common_names": [ + "Quicksilver Tradescantia" + ], + "family": "Commelinaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Tradescantia pallida", + "common_names": [ + "Purple Heart", + "Purple Queen" + ], + "family": "Commelinaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Tradescantia spathacea", + "common_names": [ + "Moses in the Cradle", + "Oyster Plant", + "Boat Lily" + ], + "family": "Commelinaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Tradescantia sillamontana", + "common_names": [ + "White Velvet", + "Cobweb Spiderwort" + ], + "family": "Commelinaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Tradescantia 'Nanouk'", + "common_names": [ + "Nanouk Tradescantia", + "Fantasy Venice" + ], + "family": "Commelinaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Callisia repens", + "common_names": [ + "Creeping Inch Plant", + "Turtle Vine" + ], + "family": "Commelinaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Callisia repens 'Pink Lady'", + "common_names": [ + "Pink Lady Callisia" + ], + "family": "Commelinaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Ceropegia woodii", + "common_names": [ + "String of Hearts", + "Rosary Vine", + "Chain of Hearts" + ], + "family": "Apocynaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Ceropegia woodii 'Variegata'", + "common_names": [ + "Variegated String of Hearts" + ], + "family": "Apocynaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Ceropegia linearis", + "common_names": [ + "String of Needles" + ], + "family": "Apocynaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Dischidia nummularia", + "common_names": [ + "String of Nickels", + "Button Orchid" + ], + "family": "Apocynaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Dischidia ovata", + "common_names": [ + "Watermelon Dischidia" + ], + "family": "Apocynaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Dischidia ruscifolia", + "common_names": [ + "Million Hearts" + ], + "family": "Apocynaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Pilea peperomioides", + "common_names": [ + "Chinese Money Plant", + "Pancake Plant", + "UFO Plant" + ], + "family": "Urticaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Pilea peperomioides 'Mojito'", + "common_names": [ + "Mojito Pilea" + ], + "family": "Urticaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Pilea peperomioides 'Sugar'", + "common_names": [ + "Sugar Pilea" + ], + "family": "Urticaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Pilea cadierei", + "common_names": [ + "Aluminum Plant", + "Watermelon Pilea" + ], + "family": "Urticaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Pilea involucrata", + "common_names": [ + "Friendship Plant", + "Panamiga" + ], + "family": "Urticaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Pilea mollis", + "common_names": [ + "Moon Valley Pilea" + ], + "family": "Urticaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Pilea microphylla", + "common_names": [ + "Artillery Plant", + "Gunpowder Plant" + ], + "family": "Urticaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Pilea glauca", + "common_names": [ + "Silver Sparkle Pilea", + "Aquamarine" + ], + "family": "Urticaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Pilea nummulariifolia", + "common_names": [ + "Creeping Charlie" + ], + "family": "Urticaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Codiaeum variegatum", + "common_names": [ + "Croton", + "Garden Croton", + "Joseph's Coat" + ], + "family": "Euphorbiaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Codiaeum variegatum 'Petra'", + "common_names": [ + "Petra Croton" + ], + "family": "Euphorbiaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Codiaeum variegatum 'Mammy'", + "common_names": [ + "Mammy Croton", + "Mamey Croton" + ], + "family": "Euphorbiaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Codiaeum variegatum 'Gold Dust'", + "common_names": [ + "Gold Dust Croton" + ], + "family": "Euphorbiaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Codiaeum variegatum 'Oakleaf'", + "common_names": [ + "Oakleaf Croton" + ], + "family": "Euphorbiaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Codiaeum variegatum 'Zanzibar'", + "common_names": [ + "Zanzibar Croton" + ], + "family": "Euphorbiaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Codiaeum variegatum 'Mrs. Iceton'", + "common_names": [ + "Mrs. Iceton Croton" + ], + "family": "Euphorbiaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Codiaeum variegatum 'Eleanor Roosevelt'", + "common_names": [ + "Eleanor Roosevelt Croton" + ], + "family": "Euphorbiaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Codiaeum variegatum 'Picasso's Paintbrush'", + "common_names": [ + "Picasso's Paintbrush Croton" + ], + "family": "Euphorbiaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Codiaeum variegatum 'Sunny Star'", + "common_names": [ + "Sunny Star Croton" + ], + "family": "Euphorbiaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Schefflera arboricola", + "common_names": [ + "Dwarf Umbrella Tree", + "Umbrella Plant" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Schefflera arboricola 'Gold Capella'", + "common_names": [ + "Gold Capella Schefflera" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Schefflera arboricola 'Trinette'", + "common_names": [ + "Trinette Schefflera" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Schefflera actinophylla", + "common_names": [ + "Umbrella Tree", + "Octopus Tree" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Schefflera elegantissima", + "common_names": [ + "False Aralia", + "Finger Aralia" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Fatsia japonica", + "common_names": [ + "Japanese Aralia", + "Fatsi" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "x Fatshedera lizei", + "common_names": [ + "Tree Ivy", + "Aralia Ivy" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Polyscias fruticosa", + "common_names": [ + "Ming Aralia" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Polyscias scutellaria", + "common_names": [ + "Shield Aralia", + "Dinner Plate Aralia" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Polyscias guilfoylei", + "common_names": [ + "Geranium Aralia", + "Wild Coffee" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aspidistra elatior", + "common_names": [ + "Cast Iron Plant", + "Bar Room Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aspidistra elatior 'Variegata'", + "common_names": [ + "Variegated Cast Iron Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aspidistra elatior 'Milky Way'", + "common_names": [ + "Milky Way Cast Iron Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Chlorophytum comosum", + "common_names": [ + "Spider Plant", + "Airplane Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Chlorophytum comosum 'Vittatum'", + "common_names": [ + "Variegated Spider Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Chlorophytum comosum 'Bonnie'", + "common_names": [ + "Bonnie Spider Plant", + "Curly Spider Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Chlorophytum comosum 'Reverse'", + "common_names": [ + "Reverse Spider Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Chlorophytum laxum 'Bichetii'", + "common_names": [ + "Siam Lily", + "Bichetii Grass" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Cordyline fruticosa", + "common_names": [ + "Ti Plant", + "Good Luck Plant", + "Hawaiian Ti" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Cordyline fruticosa 'Red Sister'", + "common_names": [ + "Red Sister Ti Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Cordyline fruticosa 'Pink Diamond'", + "common_names": [ + "Pink Diamond Ti Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Cordyline fruticosa 'Chocolate Queen'", + "common_names": [ + "Chocolate Queen Ti Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Cordyline australis", + "common_names": [ + "Cabbage Tree", + "Cabbage Palm" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Yucca elephantipes", + "common_names": [ + "Spineless Yucca", + "Stick Yucca" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Yucca aloifolia", + "common_names": [ + "Spanish Bayonet", + "Dagger Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Yucca filamentosa", + "common_names": [ + "Adam's Needle", + "Common Yucca" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ocimum basilicum", + "common_names": [ + "Sweet Basil", + "Common Basil" + ], + "family": "Lamiaceae", + "category": "Herb" + }, + { + "scientific_name": "Ocimum basilicum 'Genovese'", + "common_names": [ + "Genovese Basil" + ], + "family": "Lamiaceae", + "category": "Herb" + }, + { + "scientific_name": "Ocimum basilicum 'Thai'", + "common_names": [ + "Thai Basil" + ], + "family": "Lamiaceae", + "category": "Herb" + }, + { + "scientific_name": "Ocimum basilicum var. purpurascens", + "common_names": [ + "Purple Basil", + "Dark Opal Basil" + ], + "family": "Lamiaceae", + "category": "Herb" + }, + { + "scientific_name": "Rosmarinus officinalis", + "common_names": [ + "Rosemary" + ], + "family": "Lamiaceae", + "category": "Herb" + }, + { + "scientific_name": "Thymus vulgaris", + "common_names": [ + "Common Thyme", + "Garden Thyme" + ], + "family": "Lamiaceae", + "category": "Herb" + }, + { + "scientific_name": "Origanum vulgare", + "common_names": [ + "Oregano", + "Wild Marjoram" + ], + "family": "Lamiaceae", + "category": "Herb" + }, + { + "scientific_name": "Salvia officinalis", + "common_names": [ + "Common Sage", + "Garden Sage" + ], + "family": "Lamiaceae", + "category": "Herb" + }, + { + "scientific_name": "Mentha spicata", + "common_names": [ + "Spearmint" + ], + "family": "Lamiaceae", + "category": "Herb" + }, + { + "scientific_name": "Mentha x piperita", + "common_names": [ + "Peppermint" + ], + "family": "Lamiaceae", + "category": "Herb" + }, + { + "scientific_name": "Allium schoenoprasum", + "common_names": [ + "Chives" + ], + "family": "Amaryllidaceae", + "category": "Herb" + }, + { + "scientific_name": "Petroselinum crispum", + "common_names": [ + "Parsley" + ], + "family": "Apiaceae", + "category": "Herb" + }, + { + "scientific_name": "Coriandrum sativum", + "common_names": [ + "Cilantro", + "Coriander" + ], + "family": "Apiaceae", + "category": "Herb" + }, + { + "scientific_name": "Anethum graveolens", + "common_names": [ + "Dill" + ], + "family": "Apiaceae", + "category": "Herb" + }, + { + "scientific_name": "Lavandula angustifolia", + "common_names": [ + "English Lavender", + "True Lavender" + ], + "family": "Lamiaceae", + "category": "Herb" + }, + { + "scientific_name": "Laurus nobilis", + "common_names": [ + "Bay Laurel", + "Sweet Bay" + ], + "family": "Lauraceae", + "category": "Herb" + }, + { + "scientific_name": "Cymbopogon citratus", + "common_names": [ + "Lemongrass" + ], + "family": "Poaceae", + "category": "Herb" + }, + { + "scientific_name": "Melissa officinalis", + "common_names": [ + "Lemon Balm" + ], + "family": "Lamiaceae", + "category": "Herb" + }, + { + "scientific_name": "Origanum majorana", + "common_names": [ + "Sweet Marjoram", + "Marjoram" + ], + "family": "Lamiaceae", + "category": "Herb" + }, + { + "scientific_name": "Artemisia dracunculus", + "common_names": [ + "Tarragon", + "French Tarragon" + ], + "family": "Asteraceae", + "category": "Herb" + }, + { + "scientific_name": "Aloysia citrodora", + "common_names": [ + "Lemon Verbena" + ], + "family": "Verbenaceae", + "category": "Herb" + }, + { + "scientific_name": "Anthriscus cerefolium", + "common_names": [ + "Chervil", + "French Parsley" + ], + "family": "Apiaceae", + "category": "Herb" + }, + { + "scientific_name": "Satureja hortensis", + "common_names": [ + "Summer Savory" + ], + "family": "Lamiaceae", + "category": "Herb" + }, + { + "scientific_name": "Satureja montana", + "common_names": [ + "Winter Savory" + ], + "family": "Lamiaceae", + "category": "Herb" + }, + { + "scientific_name": "Fittonia albivenis", + "common_names": [ + "Nerve Plant", + "Mosaic Plant" + ], + "family": "Acanthaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Fittonia albivenis 'Pink Angel'", + "common_names": [ + "Pink Angel Fittonia" + ], + "family": "Acanthaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Fittonia albivenis 'White Anne'", + "common_names": [ + "White Anne Fittonia" + ], + "family": "Acanthaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Fittonia albivenis 'Red Anne'", + "common_names": [ + "Red Anne Fittonia" + ], + "family": "Acanthaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Fittonia albivenis 'Frankie'", + "common_names": [ + "Frankie Fittonia" + ], + "family": "Acanthaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hypoestes phyllostachya", + "common_names": [ + "Polka Dot Plant", + "Flamingo Plant" + ], + "family": "Acanthaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hypoestes phyllostachya 'Splash Select Pink'", + "common_names": [ + "Pink Polka Dot Plant" + ], + "family": "Acanthaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hypoestes phyllostachya 'Splash Select Red'", + "common_names": [ + "Red Polka Dot Plant" + ], + "family": "Acanthaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hypoestes phyllostachya 'Splash Select White'", + "common_names": [ + "White Polka Dot Plant" + ], + "family": "Acanthaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Iresine herbstii", + "common_names": [ + "Bloodleaf", + "Beefsteak Plant", + "Chicken Gizzard" + ], + "family": "Amaranthaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Iresine herbstii 'Aureoreticulata'", + "common_names": [ + "Yellow Bloodleaf" + ], + "family": "Amaranthaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Plectranthus scutellarioides", + "common_names": [ + "Coleus", + "Painted Nettle" + ], + "family": "Lamiaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Plectranthus scutellarioides 'Kong Rose'", + "common_names": [ + "Kong Rose Coleus" + ], + "family": "Lamiaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Plectranthus scutellarioides 'Wizard Mix'", + "common_names": [ + "Wizard Coleus" + ], + "family": "Lamiaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Plectranthus verticillatus", + "common_names": [ + "Swedish Ivy", + "Swedish Begonia" + ], + "family": "Lamiaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Plectranthus amboinicus", + "common_names": [ + "Cuban Oregano", + "Spanish Thyme" + ], + "family": "Lamiaceae", + "category": "Herb" + }, + { + "scientific_name": "Rhipsalidopsis gaertneri", + "common_names": [ + "Easter Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Aporocactus flagelliformis", + "common_names": [ + "Rat Tail Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Lepismium bolivianum", + "common_names": [ + "Bolivian Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Selenicereus chrysocardium", + "common_names": [ + "Fernleaf Cactus", + "Golden Heart" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Opuntia santa-rita", + "common_names": [ + "Santa Rita Prickly Pear", + "Purple Prickly Pear" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Gymnocalycium baldianum", + "common_names": [ + "Dwarf Chin Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Lobivia species", + "common_names": [ + "Cob Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Parodia magnifica", + "common_names": [ + "Balloon Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Stenocactus multicostatus", + "common_names": [ + "Brain Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Thelocactus species", + "common_names": [ + "Glory of Texas" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Copiapoa cinerea", + "common_names": [ + "Copiapoa" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Sulcorebutia species", + "common_names": [ + "Sulcorebutia" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Frailea species", + "common_names": [ + "Frailea Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Melocactus species", + "common_names": [ + "Turk's Cap Cactus", + "Melon Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Ariocarpus species", + "common_names": [ + "Living Rock Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Lophophora williamsii", + "common_names": [ + "Peyote" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Carnegiea gigantea", + "common_names": [ + "Saguaro Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Trichocereus pachanoi", + "common_names": [ + "San Pedro Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Pachycereus marginatus", + "common_names": [ + "Mexican Fence Post Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Cephalocereus palmeri", + "common_names": [ + "Woolly Torch Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Stenocereus thurberi", + "common_names": [ + "Organ Pipe Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Pachypodium lamerei", + "common_names": [ + "Madagascar Palm" + ], + "family": "Apocynaceae", + "category": "Succulent" + }, + { + "scientific_name": "Adenium obesum", + "common_names": [ + "Desert Rose" + ], + "family": "Apocynaceae", + "category": "Succulent" + }, + { + "scientific_name": "Agave americana", + "common_names": [ + "Century Plant", + "American Aloe" + ], + "family": "Asparagaceae", + "category": "Succulent" + }, + { + "scientific_name": "Agave americana 'Marginata'", + "common_names": [ + "Variegated Century Plant" + ], + "family": "Asparagaceae", + "category": "Succulent" + }, + { + "scientific_name": "Agave attenuata", + "common_names": [ + "Fox Tail Agave", + "Lion's Tail Agave" + ], + "family": "Asparagaceae", + "category": "Succulent" + }, + { + "scientific_name": "Agave victoriae-reginae", + "common_names": [ + "Queen Victoria Agave", + "Royal Agave" + ], + "family": "Asparagaceae", + "category": "Succulent" + }, + { + "scientific_name": "Agave parryi", + "common_names": [ + "Parry's Agave", + "Artichoke Agave" + ], + "family": "Asparagaceae", + "category": "Succulent" + }, + { + "scientific_name": "Agave desmettiana", + "common_names": [ + "Smooth Agave" + ], + "family": "Asparagaceae", + "category": "Succulent" + }, + { + "scientific_name": "Agave titanota", + "common_names": [ + "Chalk Agave" + ], + "family": "Asparagaceae", + "category": "Succulent" + }, + { + "scientific_name": "Agave potatorum", + "common_names": [ + "Butterfly Agave" + ], + "family": "Asparagaceae", + "category": "Succulent" + }, + { + "scientific_name": "Dudleya species", + "common_names": [ + "Liveforever", + "Chalk Dudleya" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Adromischus species", + "common_names": [ + "Crinkle-leaf Plant" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Adromischus cristatus", + "common_names": [ + "Crinkle-leaf Plant", + "Key Lime Pie" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Senecio vitalis", + "common_names": [ + "Blue Chalk Fingers", + "Narrow-leaf Chalksticks" + ], + "family": "Asteraceae", + "category": "Succulent" + }, + { + "scientific_name": "Monilaria obconica", + "common_names": [ + "Bunny Ears Succulent", + "Bunny Succulent" + ], + "family": "Aizoaceae", + "category": "Succulent" + }, + { + "scientific_name": "Delosperma cooperi", + "common_names": [ + "Ice Plant", + "Trailing Ice Plant" + ], + "family": "Aizoaceae", + "category": "Succulent" + }, + { + "scientific_name": "Lampranthus species", + "common_names": [ + "Ice Plant" + ], + "family": "Aizoaceae", + "category": "Succulent" + }, + { + "scientific_name": "Carpobrotus edulis", + "common_names": [ + "Hottentot Fig", + "Ice Plant" + ], + "family": "Aizoaceae", + "category": "Succulent" + }, + { + "scientific_name": "Oscularia deltoides", + "common_names": [ + "Pink Ice Plant" + ], + "family": "Aizoaceae", + "category": "Succulent" + }, + { + "scientific_name": "Stapelia grandiflora", + "common_names": [ + "Starfish Flower", + "Carrion Flower" + ], + "family": "Apocynaceae", + "category": "Succulent" + }, + { + "scientific_name": "Stapelia gigantea", + "common_names": [ + "Zulu Giant", + "Giant Starfish Flower" + ], + "family": "Apocynaceae", + "category": "Succulent" + }, + { + "scientific_name": "Huernia zebrina", + "common_names": [ + "Lifesaver Plant", + "Owl Eyes" + ], + "family": "Apocynaceae", + "category": "Succulent" + }, + { + "scientific_name": "Orbea variegata", + "common_names": [ + "Starfish Cactus", + "Toad Plant" + ], + "family": "Apocynaceae", + "category": "Succulent" + }, + { + "scientific_name": "Hoodia gordonii", + "common_names": [ + "Hoodia" + ], + "family": "Apocynaceae", + "category": "Succulent" + }, + { + "scientific_name": "Rhipsalis teres", + "common_names": [ + "Pencil Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Rhipsalis ewaldiana", + "common_names": [ + "Ewald's Rhipsalis" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Rhipsalis campos-portoana", + "common_names": [ + "Drunkard's Dream" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Rhipsalis pilocarpa", + "common_names": [ + "Hairy Stemmed Rhipsalis" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Pseudorhipsalis ramulosa", + "common_names": [ + "Red Rhipsalis" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Cryptocereus anthonyanus", + "common_names": [ + "Ric Rac Cactus", + "Fishbone Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Epiphyllum hybrid", + "common_names": [ + "Orchid Cactus Hybrid" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Gymnocalycium denudatum", + "common_names": [ + "Spider Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Acanthocereus tetragonus", + "common_names": [ + "Fairy Castle Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Pereskia aculeata", + "common_names": [ + "Barbados Gooseberry", + "Leaf Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Harrisia species", + "common_names": [ + "Moonlight Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Hylocereus undatus", + "common_names": [ + "Dragon Fruit", + "Pitaya" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria carmenae", + "common_names": [ + "Carmen Pincushion" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria bombycina", + "common_names": [ + "Silken Pincushion" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria haageana", + "common_names": [ + "Mexican Pincushion" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria mystax", + "common_names": [ + "Mustache Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria nejapensis", + "common_names": [ + "Nejapa Pincushion" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria prolifera", + "common_names": [ + "Texas Nipple Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria rhodantha", + "common_names": [ + "Rainbow Pincushion" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria zeilmanniana", + "common_names": [ + "Rose Pincushion Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria matudae", + "common_names": [ + "Thumb Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria vetula", + "common_names": [ + "Frosty Pincushion" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Opuntia engelmannii", + "common_names": [ + "Engelmann's Prickly Pear" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Opuntia basilaris", + "common_names": [ + "Beavertail Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Opuntia humifusa", + "common_names": [ + "Eastern Prickly Pear" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Opuntia leucotricha", + "common_names": [ + "Aaron's Beard Prickly Pear" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Opuntia monacantha", + "common_names": [ + "Drooping Prickly Pear" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Cylindropuntia bigelovii", + "common_names": [ + "Teddy Bear Cholla" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Cylindropuntia fulgida", + "common_names": [ + "Chain Fruit Cholla", + "Jumping Cholla" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Austrocylindropuntia subulata", + "common_names": [ + "Eve's Needle Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Echinocereus rigidissimus", + "common_names": [ + "Rainbow Hedgehog Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Echinocereus engelmannii", + "common_names": [ + "Strawberry Hedgehog" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Echinocereus triglochidiatus", + "common_names": [ + "Claret Cup Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Echinocereus pentalophus", + "common_names": [ + "Lady Finger Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Ferocactus cylindraceus", + "common_names": [ + "California Barrel Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Ferocactus wislizeni", + "common_names": [ + "Fishhook Barrel Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Ferocactus latispinus", + "common_names": [ + "Devil's Tongue Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Ferocactus glaucescens", + "common_names": [ + "Blue Barrel Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Ferocactus pilosus", + "common_names": [ + "Mexican Fire Barrel" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Echinocactus texensis", + "common_names": [ + "Horse Crippler" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Echinocactus horizonthalonius", + "common_names": [ + "Eagle Claws Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Echeveria runyonii", + "common_names": [ + "Topsy Turvy" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Atlantis'", + "common_names": [ + "Atlantis Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Blue Bird'", + "common_names": [ + "Blue Bird Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Blue Prince'", + "common_names": [ + "Blue Prince Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Blue Sky'", + "common_names": [ + "Blue Sky Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Blue Waves'", + "common_names": [ + "Blue Waves Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Compton Carousel'", + "common_names": [ + "Compton Carousel Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Dark Moon'", + "common_names": [ + "Dark Moon Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Devotion'", + "common_names": [ + "Devotion Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Etna'", + "common_names": [ + "Etna Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Fiona'", + "common_names": [ + "Fiona Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Fleur Blanc'", + "common_names": [ + "Fleur Blanc Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Galaxy Blue'", + "common_names": [ + "Galaxy Blue Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Lilacina'", + "common_names": [ + "Ghost Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Lolita'", + "common_names": [ + "Lolita Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Lovely Rose'", + "common_names": [ + "Lovely Rose Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Mexican Giant'", + "common_names": [ + "Mexican Giant Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Mexican Snowball'", + "common_names": [ + "Mexican Snowball" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Minima'", + "common_names": [ + "Minima Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Monroe'", + "common_names": [ + "Monroe Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Morning Beauty'", + "common_names": [ + "Morning Beauty Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Orion'", + "common_names": [ + "Orion Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Peacockii'", + "common_names": [ + "Peacock Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Pollux'", + "common_names": [ + "Pollux Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Powder Blue'", + "common_names": [ + "Powder Blue Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Rainbow'", + "common_names": [ + "Rainbow Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Romeo'", + "common_names": [ + "Romeo Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Silver Queen'", + "common_names": [ + "Silver Queen Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Suyon'", + "common_names": [ + "Suyon Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Violet Queen'", + "common_names": [ + "Violet Queen Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria colorata", + "common_names": [ + "Colorata Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria derenbergii", + "common_names": [ + "Painted Lady Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria laui", + "common_names": [ + "Laui Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria nodulosa", + "common_names": [ + "Painted Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria pallida", + "common_names": [ + "Argentine Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria purpusorum", + "common_names": [ + "Urbinia Purpusii" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria secunda", + "common_names": [ + "Blue Echeveria", + "Hen and Chicks" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria shaviana", + "common_names": [ + "Mexican Hens" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria subrigida", + "common_names": [ + "Fire and Ice" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum hispanicum", + "common_names": [ + "Spanish Stonecrop" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum reflexum", + "common_names": [ + "Blue Spruce Stonecrop", + "Jenny's Stonecrop" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum spathulifolium", + "common_names": [ + "Broadleaf Stonecrop", + "Cape Blanco" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum spectabile", + "common_names": [ + "Showy Stonecrop", + "Ice Plant" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum telephium", + "common_names": [ + "Witch's Moneybags", + "Orpine" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum 'Autumn Joy'", + "common_names": [ + "Autumn Joy Sedum" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum acre", + "common_names": [ + "Goldmoss Stonecrop", + "Biting Stonecrop" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum album", + "common_names": [ + "White Stonecrop" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum clavatum", + "common_names": [ + "Tiscalatengo Gorge Sedum" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum hernandezii", + "common_names": [ + "Hernandez Sedum" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum hintonii", + "common_names": [ + "Hinton's Sedum" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum kamtschaticum", + "common_names": [ + "Orange Stonecrop" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum lineare", + "common_names": [ + "Carpet Sedum", + "Needle Stonecrop" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum mexicanum", + "common_names": [ + "Mexican Stonecrop" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum multiceps", + "common_names": [ + "Miniature Joshua Tree" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum oreganum", + "common_names": [ + "Oregon Stonecrop" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum praealtum", + "common_names": [ + "Green Cockscomb" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum sieboldii", + "common_names": [ + "October Daphne", + "Siebold's Stonecrop" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum stahlii", + "common_names": [ + "Coral Bells" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum tetractinum", + "common_names": [ + "Chinese Sedum" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum treleasei", + "common_names": [ + "Trelease's Sedum", + "Silver Sedum" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum versadense", + "common_names": [ + "Fuzzy Stonecrop" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula 'Buddha's Temple'", + "common_names": [ + "Buddha's Temple" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula 'Ivory Pagoda'", + "common_names": [ + "Ivory Pagoda" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula 'Moonglow'", + "common_names": [ + "Moonglow Crassula" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula 'Springtime'", + "common_names": [ + "Springtime Crassula" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula 'Tom Thumb'", + "common_names": [ + "Tom Thumb Crassula" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula alstonii", + "common_names": [ + "Alston's Crassula" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula brevifolia", + "common_names": [ + "Short-leaved Crassula" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula columnaris", + "common_names": [ + "Column Crassula" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula conjuncta", + "common_names": [ + "Silver Buttons" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula congesta", + "common_names": [ + "Congesta Crassula" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula deceptor", + "common_names": [ + "Deceptor Crassula" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula erosula", + "common_names": [ + "Campfire Crassula" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula hemisphaerica", + "common_names": [ + "Half Globe Crassula" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula lactea", + "common_names": [ + "Taylor's Parches" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula mesembryanthemoides", + "common_names": [ + "Fuzzy Crassula" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula multicava", + "common_names": [ + "Fairy Crassula" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula nudicaulis", + "common_names": [ + "Naked-stemmed Crassula" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula orbicularis", + "common_names": [ + "Round-leaved Crassula" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula pyramidalis", + "common_names": [ + "Pagoda Mini Jade" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula sarmentosa", + "common_names": [ + "Showy Crassula" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula tetragona", + "common_names": [ + "Miniature Pine Tree" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula volkensii", + "common_names": [ + "Volkens' Crassula" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Philodendron andreanum", + "common_names": [ + "Velvet Leaf Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron camposportoanum", + "common_names": [ + "Campos Porto Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron cordatum", + "common_names": [ + "Heart Leaf Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron deflexum", + "common_names": [ + "Deflexum Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron domesticum", + "common_names": [ + "Spade Leaf Philodendron", + "Burgundy Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron elegans", + "common_names": [ + "Elegant Philodendron", + "Skeleton Key Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron esmeraldense", + "common_names": [ + "Esmeralda Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron fibrosum", + "common_names": [ + "Fibrous Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron gigas", + "common_names": [ + "Giant Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron grazielae", + "common_names": [ + "Graziela Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron imbe", + "common_names": [ + "Imbe Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron jacquinii", + "common_names": [ + "Jacquin's Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron joepii", + "common_names": [ + "Joep's Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron lacerum", + "common_names": [ + "Torn Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron lupinum", + "common_names": [ + "Wolf Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron martianum", + "common_names": [ + "Flask Philodendron", + "Fat Boy Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron microstictum", + "common_names": [ + "Microstictum Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron oxycardium", + "common_names": [ + "Oxycardium Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron patriciae", + "common_names": [ + "Patricia's Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron pinnatifidum", + "common_names": [ + "Fernleaf Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron radiatum", + "common_names": [ + "Radiate Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron renauxii", + "common_names": [ + "Renaux's Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron sagittifolium", + "common_names": [ + "Arrow Leaf Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron scandens", + "common_names": [ + "Climbing Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron spiritus-sancti", + "common_names": [ + "Santa Leopoldina Philodendron", + "Holy Spirit Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron stenolobum", + "common_names": [ + "Narrow Lobe Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron subhastatum", + "common_names": [ + "Subhastatum Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron tenue", + "common_names": [ + "Tenue Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron tripartitum", + "common_names": [ + "Three-lobed Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron undulatum", + "common_names": [ + "Wavy Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron warscewiczii", + "common_names": [ + "Warscewicz's Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron wendlandii", + "common_names": [ + "Wendland's Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron xanadu", + "common_names": [ + "Xanadu" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron 'Bob Cee'", + "common_names": [ + "Bob Cee Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron 'Burle Marx'", + "common_names": [ + "Burle Marx Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron 'Burle Marx Fantasy'", + "common_names": [ + "Burle Marx Fantasy Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron 'Caramel Marble'", + "common_names": [ + "Caramel Marble Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Philodendron 'Congo Rojo'", + "common_names": [ + "Congo Rojo Philodendron", + "Red Congo Philodendron" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya acuta", + "common_names": [ + "Acuta Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya affinis", + "common_names": [ + "Affinis Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya aldrichii", + "common_names": [ + "Aldrich's Hoya", + "Christmas Island Waxflower" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya archboldiana", + "common_names": [ + "Archbold's Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya arnottiana", + "common_names": [ + "Arnott's Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya australis 'Tenuipes'", + "common_names": [ + "Tenuipes Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya benguetensis", + "common_names": [ + "Benguet Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya bilobata", + "common_names": [ + "Two-lobed Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya brevialata", + "common_names": [ + "Short-winged Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya buotii", + "common_names": [ + "Buot's Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya burtoniae", + "common_names": [ + "Burton's Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya camphorifolia", + "common_names": [ + "Camphor-leaved Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya carnosa 'Grey Ghost'", + "common_names": [ + "Grey Ghost Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya carnosa 'Krinkle 8'", + "common_names": [ + "Krinkle 8 Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya carnosa 'Rubra'", + "common_names": [ + "Rubra Hoya", + "Krimson Princess" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya caudata", + "common_names": [ + "Tailed Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya chlorantha", + "common_names": [ + "Green-flowered Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya cinnamomifolia", + "common_names": [ + "Cinnamon-leaved Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya citrina", + "common_names": [ + "Lemon Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya clandestina", + "common_names": [ + "Hidden Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya collina", + "common_names": [ + "Hill Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya cominsii", + "common_names": [ + "Comins' Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya compacta 'Regalis'", + "common_names": [ + "Variegated Hindu Rope", + "Regalis Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya coronaria", + "common_names": [ + "Crown Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya cumingiana", + "common_names": [ + "Cuming's Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya davidcumingii", + "common_names": [ + "David Cuming's Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya densifolia", + "common_names": [ + "Dense-leaved Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya dickasoniana", + "common_names": [ + "Dickason's Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya diptera", + "common_names": [ + "Two-winged Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya diversifolia", + "common_names": [ + "Diverse-leaved Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya elliptica", + "common_names": [ + "Elliptical Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya engleriana", + "common_names": [ + "Engler's Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya erythrina", + "common_names": [ + "Red Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya erythrostemma", + "common_names": [ + "Red Crown Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya excavata", + "common_names": [ + "Hollowed Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya finlaysonii", + "common_names": [ + "Finlayson's Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya flagellata", + "common_names": [ + "Whip Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya fraterna", + "common_names": [ + "Fraternal Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya fungii", + "common_names": [ + "Fung's Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya fusca", + "common_names": [ + "Brown Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya globulifera", + "common_names": [ + "Globe-bearing Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya gnaphalioides", + "common_names": [ + "Cudweed-like Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya gracilis", + "common_names": [ + "Graceful Hoya", + "Hoya Memoria" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya greenii", + "common_names": [ + "Green's Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Hoya griffithii", + "common_names": [ + "Griffith's Hoya" + ], + "family": "Apocynaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Echeveria 'Abalone'", + "common_names": [ + "Abalone Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Afterglow'", + "common_names": [ + "Afterglow Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Black Knight'", + "common_names": [ + "Black Knight Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Blue Atoll'", + "common_names": [ + "Blue Atoll Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Blue Frills'", + "common_names": [ + "Blue Frills Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Blue Heron'", + "common_names": [ + "Blue Heron Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Blue Metal'", + "common_names": [ + "Blue Metal Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Bumps'", + "common_names": [ + "Bumps Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Cante'", + "common_names": [ + "Cante Echeveria", + "White Cloud Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Circus'", + "common_names": [ + "Circus Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Dondo'", + "common_names": [ + "Dondo Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Esther'", + "common_names": [ + "Esther Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Gilva'", + "common_names": [ + "Gilva Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Glauca'", + "common_names": [ + "Blue Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Hercules'", + "common_names": [ + "Hercules Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Imbricata'", + "common_names": [ + "Blue Rose Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Irish Mint'", + "common_names": [ + "Irish Mint Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Echeveria 'Joan Daniel'", + "common_names": [ + "Joan Daniel Echeveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula arborescens 'Blue Bird'", + "common_names": [ + "Blue Bird Jade" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula arborescens 'Undulatifolia'", + "common_names": [ + "Ripple Jade" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula atropurpurea", + "common_names": [ + "Purple Crassula" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula barklyi", + "common_names": [ + "Rattlesnake Tail" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula capitella 'Campfire'", + "common_names": [ + "Campfire Plant" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula coccinea", + "common_names": [ + "Red Crassula" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula commutata", + "common_names": [ + "Interchanged Crassula" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula cooperi", + "common_names": [ + "Cooper's Crassula" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula cordata", + "common_names": [ + "Heart-shaped Crassula" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula cornuta", + "common_names": [ + "Horned Crassula" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula cotyledonis", + "common_names": [ + "Bear Paw Crassula" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula deltoidea", + "common_names": [ + "Silver Beads" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula ericoides", + "common_names": [ + "Heather-like Crassula" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula ernestii", + "common_names": [ + "Ernest's Crassula" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula exilis", + "common_names": [ + "Slender Crassula" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Crassula helmsii", + "common_names": [ + "Swamp Stonecrop" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe aculeata", + "common_names": [ + "Prickly Aloe", + "Red Hot Poker Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe africana", + "common_names": [ + "African Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe bakeri", + "common_names": [ + "Baker's Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe broomii", + "common_names": [ + "Mountain Aloe", + "Snake Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe castanea", + "common_names": [ + "Cat's Tail Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe ciliaris", + "common_names": [ + "Climbing Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe claviflora", + "common_names": [ + "Kraal Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe comosa", + "common_names": [ + "Clanwilliam Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe descoingsii", + "common_names": [ + "Descoings' Aloe", + "Miniature Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe dichotoma", + "common_names": [ + "Quiver Tree", + "Kokerboom" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe distans", + "common_names": [ + "Jeweled Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe dorotheae", + "common_names": [ + "Sunset Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe ecklonis", + "common_names": [ + "Grass Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe ferox", + "common_names": [ + "Cape Aloe", + "Bitter Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe glauca", + "common_names": [ + "Blue Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe haworthioides", + "common_names": [ + "Haworthia-like Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe hereroensis", + "common_names": [ + "Sand Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe krapohliana", + "common_names": [ + "Krapohl's Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe lineata", + "common_names": [ + "Lined Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe maculata", + "common_names": [ + "Soap Aloe", + "Zebra Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe marlothii", + "common_names": [ + "Mountain Aloe", + "Marloth's Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe mitriformis", + "common_names": [ + "Mitre Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Aloe parvula", + "common_names": [ + "Tiny Aloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia angustifolia", + "common_names": [ + "Narrow-leaved Haworthia" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia arachnoidea", + "common_names": [ + "Cobweb Haworthia" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia bayeri", + "common_names": [ + "Bayer's Haworthia" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia bolusii", + "common_names": [ + "Bolus' Haworthia" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia chloracantha", + "common_names": [ + "Green-flowered Haworthia" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia coarctata", + "common_names": [ + "Compressed Haworthia" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia comptoniana", + "common_names": [ + "Compton's Haworthia" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia concolor", + "common_names": [ + "Same-colored Haworthia" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia correcta", + "common_names": [ + "Corrected Haworthia" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia decipiens", + "common_names": [ + "Deceiving Haworthia" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia emelyae", + "common_names": [ + "Emely's Haworthia" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia fasciata 'Big Band'", + "common_names": [ + "Big Band Zebra Plant" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia fasciata 'Super White'", + "common_names": [ + "Super White Zebra Plant" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia glauca", + "common_names": [ + "Blue-grey Haworthia" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia gracilis", + "common_names": [ + "Slender Haworthia" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia herbacea", + "common_names": [ + "Herbaceous Haworthia" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia lockwoodii", + "common_names": [ + "Lockwood's Haworthia" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia magnifica", + "common_names": [ + "Magnificent Haworthia" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia maughanii", + "common_names": [ + "Maughan's Haworthia" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia minima", + "common_names": [ + "Miniature Haworthia" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia mirabilis", + "common_names": [ + "Wonderful Haworthia" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia monticola", + "common_names": [ + "Mountain Haworthia" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia nortieri", + "common_names": [ + "Nortier's Haworthia" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia parksiana", + "common_names": [ + "Parks' Haworthia" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia picta", + "common_names": [ + "Painted Haworthia" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia pygmaea", + "common_names": [ + "Pygmy Haworthia" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Haworthia reticulata", + "common_names": [ + "Net Haworthia" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Ariocarpus fissuratus", + "common_names": [ + "Living Rock Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Astrophytum capricorne", + "common_names": [ + "Goat's Horn Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Astrophytum coahuilense", + "common_names": [ + "Coahuila Astrophytum" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Browningia hertlingiana", + "common_names": [ + "Blue Cereus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Cereus hildmannianus", + "common_names": [ + "Hedge Cactus", + "Queen of the Night" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Cereus repandus 'Monstrosus'", + "common_names": [ + "Monstrose Apple Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Cleistocactus winteri", + "common_names": [ + "Golden Rat Tail" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Copiapoa humilis", + "common_names": [ + "Humble Copiapoa" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Coryphantha elephantidens", + "common_names": [ + "Elephant Tooth Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Discocactus horstii", + "common_names": [ + "Horst's Discocactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Echinocactus grusonii 'Albino'", + "common_names": [ + "Albino Golden Barrel" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Echinocactus platyacanthus", + "common_names": [ + "Giant Barrel Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Echinocereus cinerascens", + "common_names": [ + "Ash-grey Hedgehog" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Echinocereus coccineus", + "common_names": [ + "Scarlet Hedgehog Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Echinocereus dasyacanthus", + "common_names": [ + "Texas Rainbow Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Echinocereus fendleri", + "common_names": [ + "Fendler's Hedgehog" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Echinocereus pectinatus", + "common_names": [ + "Rainbow Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Echinocereus reichenbachii", + "common_names": [ + "Lace Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Echinocereus viridiflorus", + "common_names": [ + "Green-flowered Hedgehog" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Echinopsis ancistrophora", + "common_names": [ + "Hooked Echinopsis" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Echinopsis aurea", + "common_names": [ + "Golden Easter Lily Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Echinopsis calochlora", + "common_names": [ + "Beautiful Green Echinopsis" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Aerangis biloba", + "common_names": [ + "Two-lobed Aerangis" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Aerangis citrata", + "common_names": [ + "Lemon-scented Aerangis" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Aerangis luteoalba var. rhodosticta", + "common_names": [ + "Red-spotted Aerangis" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Aerides odorata", + "common_names": [ + "Fragrant Aerides", + "Cat's Tail Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Angraecum didieri", + "common_names": [ + "Didier's Angraecum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Angraecum eburneum", + "common_names": [ + "Comet Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Angraecum leonis", + "common_names": [ + "Lion's Angraecum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Angraecum magdalenae", + "common_names": [ + "Magdalena's Angraecum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Angraecum sesquipedale", + "common_names": [ + "Darwin's Orchid", + "Christmas Orchid", + "Star of Bethlehem Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Barkeria spectabilis", + "common_names": [ + "Showy Barkeria" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Brassia caudata", + "common_names": [ + "Tailed Spider Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Brassia gireoudiana", + "common_names": [ + "Gireoud's Spider Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Brassia maculata", + "common_names": [ + "Spotted Spider Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Brassia verrucosa", + "common_names": [ + "Warty Spider Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Bulbophyllum beccarii", + "common_names": [ + "Giant Bulbophyllum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Bulbophyllum fletcherianum", + "common_names": [ + "Fletcher's Bulbophyllum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Bulbophyllum lobbii", + "common_names": [ + "Lobb's Bulbophyllum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Bulbophyllum medusae", + "common_names": [ + "Medusa Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Bulbophyllum phalaenopsis", + "common_names": [ + "Moth Orchid Bulbophyllum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Calanthe discolor", + "common_names": [ + "Hardy Calanthe" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Calanthe triplicata", + "common_names": [ + "Christmas Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Catasetum fimbriatum", + "common_names": [ + "Fringed Catasetum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Catasetum macrocarpum", + "common_names": [ + "Large-fruited Catasetum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Catasetum pileatum", + "common_names": [ + "Cap Catasetum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Cattleya aclandiae", + "common_names": [ + "Acland's Cattleya" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Cattleya amethystoglossa", + "common_names": [ + "Amethyst-lipped Cattleya" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Cattleya aurantiaca", + "common_names": [ + "Orange Cattleya" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Cattleya bicolor", + "common_names": [ + "Two-colored Cattleya" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Cattleya bowringiana", + "common_names": [ + "Bowring's Cattleya", + "Cluster Cattleya" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Cattleya dowiana", + "common_names": [ + "Dow's Cattleya" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Tillandsia abdita", + "common_names": [ + "Hidden Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia achyrostachys", + "common_names": [ + "Silver Spike Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia albida", + "common_names": [ + "White Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia andreana", + "common_names": [ + "Andrea's Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia araujei", + "common_names": [ + "Araujo's Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia argentea", + "common_names": [ + "Silver Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia baileyi", + "common_names": [ + "Bailey's Ball Moss" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia bandensis", + "common_names": [ + "Banda Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia bartramii", + "common_names": [ + "Bartram's Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia brachyphylla", + "common_names": [ + "Short-leaved Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia bulbosa 'Giant Form'", + "common_names": [ + "Giant Bulbous Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia cacticola", + "common_names": [ + "Cactus-dwelling Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia caerulea", + "common_names": [ + "Blue Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia caliginosa", + "common_names": [ + "Dark Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia califani", + "common_names": [ + "Califan's Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia capitata 'Maroon'", + "common_names": [ + "Maroon Capitata Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia capitata 'Peach'", + "common_names": [ + "Peach Capitata Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia capitata 'Yellow'", + "common_names": [ + "Yellow Capitata Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia carlsoniae", + "common_names": [ + "Carlson's Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia chiapensis", + "common_names": [ + "Chiapas Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia circinnatoides", + "common_names": [ + "Curly Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia clavigera", + "common_names": [ + "Club-bearing Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia complanata", + "common_names": [ + "Flattened Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia concolor", + "common_names": [ + "Same-colored Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia cryptantha", + "common_names": [ + "Hidden Flower Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia decomposita", + "common_names": [ + "Decomposed Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia didisticha", + "common_names": [ + "Two-ranked Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Tillandsia disticha", + "common_names": [ + "Distichous Air Plant" + ], + "family": "Bromeliaceae", + "category": "Air Plant" + }, + { + "scientific_name": "Adiantum capillus-veneris 'Imbricatum'", + "common_names": [ + "Overlapping Maidenhair Fern" + ], + "family": "Pteridaceae", + "category": "Fern" + }, + { + "scientific_name": "Adiantum caudatum", + "common_names": [ + "Walking Maidenhair Fern", + "Tailed Maidenhair" + ], + "family": "Pteridaceae", + "category": "Fern" + }, + { + "scientific_name": "Adiantum concinnum", + "common_names": [ + "Brittle Maidenhair Fern" + ], + "family": "Pteridaceae", + "category": "Fern" + }, + { + "scientific_name": "Adiantum formosum", + "common_names": [ + "Giant Maidenhair Fern", + "Black Stem Maidenhair" + ], + "family": "Pteridaceae", + "category": "Fern" + }, + { + "scientific_name": "Adiantum hispidulum", + "common_names": [ + "Rough Maidenhair Fern", + "Five-finger Fern" + ], + "family": "Pteridaceae", + "category": "Fern" + }, + { + "scientific_name": "Adiantum macrophyllum", + "common_names": [ + "Large-leaved Maidenhair" + ], + "family": "Pteridaceae", + "category": "Fern" + }, + { + "scientific_name": "Adiantum peruvianum", + "common_names": [ + "Silver Dollar Maidenhair" + ], + "family": "Pteridaceae", + "category": "Fern" + }, + { + "scientific_name": "Adiantum tenerum 'Farleyense'", + "common_names": [ + "Farley's Fern", + "Barbados Fern" + ], + "family": "Pteridaceae", + "category": "Fern" + }, + { + "scientific_name": "Adiantum trapeziforme", + "common_names": [ + "Diamond Maidenhair Fern" + ], + "family": "Pteridaceae", + "category": "Fern" + }, + { + "scientific_name": "Asplenium australasicum", + "common_names": [ + "Australian Bird's Nest Fern" + ], + "family": "Aspleniaceae", + "category": "Fern" + }, + { + "scientific_name": "Asplenium bulbiferum", + "common_names": [ + "Mother Fern", + "Hen and Chickens Fern" + ], + "family": "Aspleniaceae", + "category": "Fern" + }, + { + "scientific_name": "Asplenium ceterach", + "common_names": [ + "Rustyback Fern" + ], + "family": "Aspleniaceae", + "category": "Fern" + }, + { + "scientific_name": "Asplenium dimorphum", + "common_names": [ + "Norfolk Island Asplenium" + ], + "family": "Aspleniaceae", + "category": "Fern" + }, + { + "scientific_name": "Asplenium nidus 'Antiquum'", + "common_names": [ + "Wavy Bird's Nest Fern" + ], + "family": "Aspleniaceae", + "category": "Fern" + }, + { + "scientific_name": "Asplenium nidus 'Fimbriatum'", + "common_names": [ + "Fringed Bird's Nest Fern" + ], + "family": "Aspleniaceae", + "category": "Fern" + }, + { + "scientific_name": "Asplenium scolopendrium", + "common_names": [ + "Hart's Tongue Fern" + ], + "family": "Aspleniaceae", + "category": "Fern" + }, + { + "scientific_name": "Asplenium scolopendrium 'Crispum'", + "common_names": [ + "Curly Hart's Tongue Fern" + ], + "family": "Aspleniaceae", + "category": "Fern" + }, + { + "scientific_name": "Athyrium filix-femina", + "common_names": [ + "Lady Fern" + ], + "family": "Athyriaceae", + "category": "Fern" + }, + { + "scientific_name": "Athyrium niponicum 'Pictum'", + "common_names": [ + "Japanese Painted Fern" + ], + "family": "Athyriaceae", + "category": "Fern" + }, + { + "scientific_name": "Athyrium niponicum 'Burgundy Lace'", + "common_names": [ + "Burgundy Lace Fern" + ], + "family": "Athyriaceae", + "category": "Fern" + }, + { + "scientific_name": "Athyrium niponicum 'Ghost'", + "common_names": [ + "Ghost Fern" + ], + "family": "Athyriaceae", + "category": "Fern" + }, + { + "scientific_name": "Athyrium niponicum 'Ursula's Red'", + "common_names": [ + "Ursula's Red Fern" + ], + "family": "Athyriaceae", + "category": "Fern" + }, + { + "scientific_name": "Blechnum brasiliense", + "common_names": [ + "Brazilian Tree Fern" + ], + "family": "Blechnaceae", + "category": "Fern" + }, + { + "scientific_name": "Blechnum occidentale", + "common_names": [ + "Hammock Fern" + ], + "family": "Blechnaceae", + "category": "Fern" + }, + { + "scientific_name": "Blechnum spicant", + "common_names": [ + "Deer Fern", + "Hard Fern" + ], + "family": "Blechnaceae", + "category": "Fern" + }, + { + "scientific_name": "Cyathea cooperi", + "common_names": [ + "Australian Tree Fern", + "Lacy Tree Fern" + ], + "family": "Cyatheaceae", + "category": "Fern" + }, + { + "scientific_name": "Begonia acetosa", + "common_names": [ + "Sorrel Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia aconitifolia", + "common_names": [ + "Aconite-leaved Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia albopicta", + "common_names": [ + "White-spotted Begonia", + "Guinea Wing Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia ampla", + "common_names": [ + "Large Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia angularis", + "common_names": [ + "Angel Wing Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia bipinnatifida", + "common_names": [ + "Fern-leaved Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia bowerae 'Nigramarga'", + "common_names": [ + "Black-margined Eyelash Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia brevirimosa", + "common_names": [ + "Short-veined Begonia", + "Exotic Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia carrieae", + "common_names": [ + "Carrie's Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia chloroneura", + "common_names": [ + "Green-veined Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia coccinea", + "common_names": [ + "Scarlet Begonia", + "Angelwing Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia conchifolia", + "common_names": [ + "Shell-leaved Begonia", + "Zip Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia corallina", + "common_names": [ + "Coral Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia cucullata", + "common_names": [ + "Wax Begonia", + "Bedding Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia deliciosa", + "common_names": [ + "Delicious Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia diadema", + "common_names": [ + "Diadem Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia dietrichiana", + "common_names": [ + "Dietrich's Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia dipetala", + "common_names": [ + "Two-petaled Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia dregei", + "common_names": [ + "Drege's Begonia", + "Maple Leaf Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia echinosepala", + "common_names": [ + "Spiny-sepaled Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia elatior", + "common_names": [ + "Rieger Begonia", + "Winter Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia eminii", + "common_names": [ + "Emin's Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia exotica", + "common_names": [ + "Exotic Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia foliosa", + "common_names": [ + "Fern-leaved Begonia", + "Fuchsia Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia formosana", + "common_names": [ + "Taiwan Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia fuchsioides", + "common_names": [ + "Fuchsia Begonia", + "Shrub Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia gehrtii", + "common_names": [ + "Gehrt's Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia glabra", + "common_names": [ + "Smooth Begonia", + "Grape-leaf Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Begonia goegoensis", + "common_names": [ + "Fire King Begonia" + ], + "family": "Begoniaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Adonidia merrillii", + "common_names": [ + "Manila Palm", + "Christmas Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Archontophoenix alexandrae", + "common_names": [ + "Alexandra Palm", + "King Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Archontophoenix cunninghamiana", + "common_names": [ + "Bangalow Palm", + "Piccabeen Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Areca catechu", + "common_names": [ + "Betel Nut Palm", + "Areca Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Areca triandra", + "common_names": [ + "Triandra Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Arenga pinnata", + "common_names": [ + "Sugar Palm", + "Black Sugar Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Bismarckia nobilis", + "common_names": [ + "Bismarck Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Brahea armata", + "common_names": [ + "Mexican Blue Palm", + "Blue Hesper Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Brahea edulis", + "common_names": [ + "Guadalupe Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Butia capitata", + "common_names": [ + "Pindo Palm", + "Jelly Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Calamus caryotoides", + "common_names": [ + "Fishtail Lawyer Cane" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Calamus rotang", + "common_names": [ + "Rattan Palm", + "Common Rattan" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Caryota urens", + "common_names": [ + "Toddy Palm", + "Wine Palm", + "Fishtail Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Chamaedorea costaricana", + "common_names": [ + "Costa Rican Bamboo Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Chamaedorea ernesti-augustii", + "common_names": [ + "Fishtail Palm", + "Ernest August Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Chamaedorea geonomiformis", + "common_names": [ + "Geonoma-like Chamaedorea" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Chamaedorea hooperiana", + "common_names": [ + "Hooper's Palm", + "Maya Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Chamaedorea microspadix", + "common_names": [ + "Hardy Bamboo Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Chamaedorea radicalis", + "common_names": [ + "Radicalis Palm", + "Dwarf Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Chamaedorea stolonifera", + "common_names": [ + "Stoloniferous Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Chamaedorea tepejilote", + "common_names": [ + "Pacaya Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Chamaerops humilis 'Cerifera'", + "common_names": [ + "Blue Mediterranean Fan Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Cocos nucifera 'Dwarf'", + "common_names": [ + "Dwarf Coconut Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Copernicia alba", + "common_names": [ + "Caranday Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Copernicia prunifera", + "common_names": [ + "Carnauba Wax Palm" + ], + "family": "Arecaceae", + "category": "Palm" + }, + { + "scientific_name": "Peperomia abyssinica", + "common_names": [ + "Abyssinian Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peromia albovittata", + "common_names": [ + "Ivy Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia angulata", + "common_names": [ + "Beetle Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia arifolia", + "common_names": [ + "Arum-leaved Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia asperula", + "common_names": [ + "Rough Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia blanda", + "common_names": [ + "Soft Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia boivinii", + "common_names": [ + "Boivin's Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia campylotropa", + "common_names": [ + "Curved Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia caperata 'Abricos'", + "common_names": [ + "Apricot Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia caperata 'Luna Red'", + "common_names": [ + "Red Luna Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia caperata 'Schumi Red'", + "common_names": [ + "Schumi Red Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia caperata 'Silver Ripple'", + "common_names": [ + "Silver Ripple Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia caulibarbis", + "common_names": [ + "Bearded Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia clusiifolia 'Jelly'", + "common_names": [ + "Jelly Peperomia", + "Tricolor Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia clusiifolia 'Rainbow'", + "common_names": [ + "Rainbow Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia cubensis", + "common_names": [ + "Cuban Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia deppeana", + "common_names": [ + "Deppe's Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia dolabriformis", + "common_names": [ + "Prayer Pepper", + "Axe-shaped Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia emarginella", + "common_names": [ + "Notched Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia fagerlindii", + "common_names": [ + "Fagerlind's Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia fraseri", + "common_names": [ + "Flowering Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia glabella", + "common_names": [ + "Cypress Peperomia", + "Wax Privet Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia griseoargentea", + "common_names": [ + "Ivy Leaf Peperomia", + "Platinum Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia hoffmannii", + "common_names": [ + "Hoffmann's Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Peperomia kimnachii", + "common_names": [ + "Kimnach's Peperomia" + ], + "family": "Piperaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Goeppertia albertii", + "common_names": [ + "Albert's Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Goeppertia allouia", + "common_names": [ + "Leren", + "Guinea Arrowroot" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Goeppertia bachemiana", + "common_names": [ + "Bacheman's Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Goeppertia burle-marxii", + "common_names": [ + "Burle Marx Calathea", + "Ice Blue Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Goeppertia concinna", + "common_names": [ + "Elegant Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Goeppertia crocata", + "common_names": [ + "Eternal Flame", + "Saffron Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Goeppertia elliptica", + "common_names": [ + "Vittata Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Goeppertia fasciata", + "common_names": [ + "Banded Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Goeppertia freddie", + "common_names": [ + "Freddie Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Goeppertia fucata", + "common_names": [ + "Colored Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Goeppertia gandersii", + "common_names": [ + "Ganders' Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Goeppertia grandiflora", + "common_names": [ + "Large-flowered Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Goeppertia hopkinsii", + "common_names": [ + "Hopkins' Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Goeppertia illustris", + "common_names": [ + "Illustrious Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Goeppertia inocephala", + "common_names": [ + "Fiber-headed Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Goeppertia kegeliana", + "common_names": [ + "Kegel's Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Goeppertia loeseneri", + "common_names": [ + "Loesener's Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Goeppertia louisae", + "common_names": [ + "Louise Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Goeppertia lutea", + "common_names": [ + "Yellow Calathea", + "Havana Cigar Plant" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Goeppertia makoyana 'Exotica'", + "common_names": [ + "Exotic Peacock Plant" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Goeppertia metallica", + "common_names": [ + "Metallic Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Goeppertia micans", + "common_names": [ + "Shining Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Goeppertia musaica 'Network'", + "common_names": [ + "Network Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Goeppertia nigrifolia", + "common_names": [ + "Black-leaved Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Goeppertia oppenheimiana", + "common_names": [ + "Oppenheim's Calathea", + "Never Never Plant" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Goeppertia orbifolia var. viridifolia", + "common_names": [ + "Green Orbifolia" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Goeppertia ornata 'Beauty Star'", + "common_names": [ + "Beauty Star Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Goeppertia picturata", + "common_names": [ + "Painted Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Goeppertia picturata 'Argentea'", + "common_names": [ + "Silver Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Goeppertia picturata 'Vandenheckei'", + "common_names": [ + "Vandenheck's Calathea" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia alba", + "common_names": [ + "White Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia boa", + "common_names": [ + "Boa Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia brancifolia", + "common_names": [ + "Branch-leaved Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia bullata", + "common_names": [ + "Bullate Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia cadieri", + "common_names": [ + "Cadier's Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia clypeolata", + "common_names": [ + "Shield Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia culionensis", + "common_names": [ + "Culion Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia devansayana", + "common_names": [ + "Devansar's Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia esculenta", + "common_names": [ + "Edible Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia fornicata", + "common_names": [ + "Arched Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia 'Frydek'", + "common_names": [ + "Frydek Alocasia", + "Green Velvet Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia gageana", + "common_names": [ + "Gage's Alocasia", + "Elephant Ear" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia grandis", + "common_names": [ + "Giant Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia heterophylla", + "common_names": [ + "Variable-leaved Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia infernalis", + "common_names": [ + "Black Magic Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia jacklyn", + "common_names": [ + "Jacklyn Alocasia", + "Tandurusa Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia lancifolia", + "common_names": [ + "Lance-leaved Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia longiloba 'Watsoniana'", + "common_names": [ + "Watson's Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia maharani", + "common_names": [ + "Maharani Alocasia", + "Grey Dragon Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia melo", + "common_names": [ + "Melo Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia micholitziana 'Frydek'", + "common_names": [ + "Micholitz's Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia navicularis", + "common_names": [ + "Boat-shaped Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia nebula", + "common_names": [ + "Nebula Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia pycnoneura", + "common_names": [ + "Dense-veined Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia reginae", + "common_names": [ + "Queen Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia reversa", + "common_names": [ + "Reversed Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Alocasia robusta", + "common_names": [ + "Robust Alocasia" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium ace of spades", + "common_names": [ + "Ace of Spades Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium bakeri", + "common_names": [ + "Baker's Anthurium", + "Bird's Nest Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium besseae", + "common_names": [ + "Besse's Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium brownii", + "common_names": [ + "Brown's Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium carlablackiae", + "common_names": [ + "Carla Black's Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium chamberlainii", + "common_names": [ + "Chamberlain's Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium clavigerum", + "common_names": [ + "Club-bearing Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium coriaceum", + "common_names": [ + "Leathery Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium corrugatum", + "common_names": [ + "Corrugated Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium crassinervium", + "common_names": [ + "Thick-veined Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium crystallinum 'Silver'", + "common_names": [ + "Silver Crystal Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium decipiens", + "common_names": [ + "Deceiving Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium dressleri", + "common_names": [ + "Dressler's Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium effusilobum", + "common_names": [ + "Spreading-lobed Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium ellipticum", + "common_names": [ + "Elliptical Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium faustomirandae", + "common_names": [ + "Fausto Miranda's Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium friedrichsthalii", + "common_names": [ + "Friedrich's Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium gracile", + "common_names": [ + "Slender Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium grandifolium", + "common_names": [ + "Large-leaved Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium hookeri", + "common_names": [ + "Hooker's Anthurium", + "Bird's Nest Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium jenmanii", + "common_names": [ + "Jenman's Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium kunthii", + "common_names": [ + "Kunth's Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium lancifolium", + "common_names": [ + "Lance-leaved Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium luxurians", + "common_names": [ + "Luxuriant Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium macrolobium", + "common_names": [ + "Large-lobed Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium marmoratum", + "common_names": [ + "Marbled Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium metallicum", + "common_names": [ + "Metallic Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium moodeanum", + "common_names": [ + "Moode's Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium nymphaeifolium", + "common_names": [ + "Water Lily-leaved Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium pallidiflorum", + "common_names": [ + "Pale-flowered Anthurium", + "Strap Leaf Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium papillilaminum", + "common_names": [ + "Papillate Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium pedatoradiatum", + "common_names": [ + "Finger Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium pendulifolium", + "common_names": [ + "Pendulous Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium plowmanii", + "common_names": [ + "Plowman's Anthurium", + "Wave of Love" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium podophyllum", + "common_names": [ + "Foot-leaved Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium polyschistum", + "common_names": [ + "Many-cleft Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Anthurium portillae", + "common_names": [ + "Portilla's Anthurium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum 'Albo Variegatum'", + "common_names": [ + "Albo Syngonium", + "Variegated Arrowhead" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum 'Exotic Allusion'", + "common_names": [ + "Exotic Allusion Syngonium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum 'Gold Allusion'", + "common_names": [ + "Gold Allusion Syngonium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum 'Godzilla'", + "common_names": [ + "Godzilla Syngonium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum 'Imperial White'", + "common_names": [ + "Imperial White Syngonium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum 'Jade'", + "common_names": [ + "Jade Syngonium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum 'Mango Allusion'", + "common_names": [ + "Mango Allusion Syngonium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum 'Maria'", + "common_names": [ + "Maria Syngonium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum 'Mojito'", + "common_names": [ + "Mojito Syngonium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum 'Painted Arrow'", + "common_names": [ + "Painted Arrow Syngonium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum 'Pixie'", + "common_names": [ + "Pixie Syngonium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum 'Rayii'", + "common_names": [ + "Rayii Syngonium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum 'Robusta'", + "common_names": [ + "Robusta Syngonium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum 'Silver Pearl'", + "common_names": [ + "Silver Pearl Syngonium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium podophyllum 'Three Kings'", + "common_names": [ + "Three Kings Syngonium" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium angustatum", + "common_names": [ + "Narrow-leaved Arrowhead" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium chiapense", + "common_names": [ + "Chiapas Arrowhead" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium erythrophyllum", + "common_names": [ + "Red-leaved Syngonium", + "Red Arrow" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Syngonium macrophyllum", + "common_names": [ + "Large-leaved Arrowhead" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Anyamanee'", + "common_names": [ + "Anyamanee Chinese Evergreen", + "Lady Valentine" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Aurora Siam'", + "common_names": [ + "Aurora Siam Aglaonema" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Creta'", + "common_names": [ + "Creta Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Diamond Bay'", + "common_names": [ + "Diamond Bay Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Firecracker'", + "common_names": [ + "Firecracker Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'First Diamond'", + "common_names": [ + "First Diamond Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Golden Fluorite'", + "common_names": [ + "Golden Fluorite Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Harlequin'", + "common_names": [ + "Harlequin Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Juliette'", + "common_names": [ + "Juliette Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Key Largo'", + "common_names": [ + "Key Largo Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'King of Siam'", + "common_names": [ + "King of Siam Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Leprechaun'", + "common_names": [ + "Leprechaun Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Lumina'", + "common_names": [ + "Lumina Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Manila Pride'", + "common_names": [ + "Manila Pride Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Moonstone'", + "common_names": [ + "Moonstone Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Osaka'", + "common_names": [ + "Osaka Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Pattaya Beauty'", + "common_names": [ + "Pattaya Beauty Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Peacock'", + "common_names": [ + "Peacock Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Pink Moon'", + "common_names": [ + "Pink Moon Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Pride of Sumatra'", + "common_names": [ + "Pride of Sumatra Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Romeo'", + "common_names": [ + "Romeo Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Sapphire Suzanne'", + "common_names": [ + "Sapphire Suzanne Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Silver King'", + "common_names": [ + "Silver King Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Silverado'", + "common_names": [ + "Silverado Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aglaonema 'Spring Snow'", + "common_names": [ + "Spring Snow Chinese Evergreen" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena trifasciata 'Bantel's Sensation'", + "common_names": [ + "Bantel's Sensation Snake Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena trifasciata 'Cleopatra'", + "common_names": [ + "Cleopatra Snake Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena trifasciata 'Fernwood'", + "common_names": [ + "Fernwood Snake Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena trifasciata 'Futura Robusta'", + "common_names": [ + "Futura Robusta Snake Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena trifasciata 'Futura Superba'", + "common_names": [ + "Futura Superba Snake Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena trifasciata 'Golden Hahnii'", + "common_names": [ + "Golden Bird's Nest Snake Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena trifasciata 'Jade Pagoda'", + "common_names": [ + "Jade Pagoda Snake Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena trifasciata 'Jaboa'", + "common_names": [ + "Jaboa Snake Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena trifasciata 'Night Owl'", + "common_names": [ + "Night Owl Snake Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena trifasciata 'Samurai Dwarf'", + "common_names": [ + "Samurai Dwarf Snake Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena trifasciata 'Sayuri'", + "common_names": [ + "Sayuri Snake Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena trifasciata 'Silver Blue'", + "common_names": [ + "Silver Blue Snake Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena trifasciata 'Silver Flame'", + "common_names": [ + "Silver Flame Snake Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena trifasciata 'Silver Hahnii'", + "common_names": [ + "Silver Bird's Nest Snake Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena trifasciata 'Starfish'", + "common_names": [ + "Starfish Snake Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena trifasciata 'Whitney'", + "common_names": [ + "Whitney Snake Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena angolensis", + "common_names": [ + "Cylindrical Snake Plant", + "African Spear" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena bacularis", + "common_names": [ + "Mikado Snake Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena ehrenbergii", + "common_names": [ + "Blue Sansevieria", + "Sword Sansevieria" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena francisii", + "common_names": [ + "Francis' Snake Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena gracilis", + "common_names": [ + "Graceful Snake Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena pearsonii", + "common_names": [ + "Pearson's Snake Plant", + "Rhino Grass" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dracaena pinguicula", + "common_names": [ + "Walking Sansevieria" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Sedum adolphi", + "common_names": [ + "Golden Sedum", + "Adolph's Sedum" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum anglicum", + "common_names": [ + "English Stonecrop" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum brevifolium", + "common_names": [ + "Short-leaved Stonecrop" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum caeruleum", + "common_names": [ + "Blue Stonecrop" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum confusum", + "common_names": [ + "Lesser Mexican Stonecrop" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum cuspidatum", + "common_names": [ + "Pointed Stonecrop" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum dendroideum", + "common_names": [ + "Tree Sedum" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum divergens", + "common_names": [ + "Spreading Stonecrop" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum furfuraceum", + "common_names": [ + "Bonsai Sedum" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum glaucophyllum", + "common_names": [ + "Cliff Stonecrop" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum griseum", + "common_names": [ + "Grey Stonecrop" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum humifusum", + "common_names": [ + "Ground-covering Sedum" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum lucidum", + "common_names": [ + "Shiny Stonecrop" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum lydium", + "common_names": [ + "Lydian Stonecrop" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sedum palmeri", + "common_names": [ + "Palmer's Sedum" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum 'Ashes of Roses'", + "common_names": [ + "Ashes of Roses Hens and Chicks" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum 'Black Knight'", + "common_names": [ + "Black Knight Hens and Chicks" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum 'Blood Tip'", + "common_names": [ + "Blood Tip Hens and Chicks" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum 'Blue Boy'", + "common_names": [ + "Blue Boy Hens and Chicks" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum 'Bronco'", + "common_names": [ + "Bronco Hens and Chicks" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum 'Bronze Pastel'", + "common_names": [ + "Bronze Pastel Hens and Chicks" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum 'Burgundy Velvet'", + "common_names": [ + "Burgundy Velvet Hens and Chicks" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum 'Cafe'", + "common_names": [ + "Cafe Hens and Chicks" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum 'Candy Apple'", + "common_names": [ + "Candy Apple Hens and Chicks" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum 'Carmen'", + "common_names": [ + "Carmen Hens and Chicks" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum 'Cherry Frost'", + "common_names": [ + "Cherry Frost Hens and Chicks" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum 'Chocolate Sundae'", + "common_names": [ + "Chocolate Sundae Hens and Chicks" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum 'Chick Charms Gold Nugget'", + "common_names": [ + "Gold Nugget Hens and Chicks" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum 'Commander Hay'", + "common_names": [ + "Commander Hay Hens and Chicks" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum 'Coral Red'", + "common_names": [ + "Coral Red Hens and Chicks" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum 'Dark Beauty'", + "common_names": [ + "Dark Beauty Hens and Chicks" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum 'Director Jacobs'", + "common_names": [ + "Director Jacobs Hens and Chicks" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum 'Eldorado'", + "common_names": [ + "Eldorado Hens and Chicks" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum 'Emerald Empress'", + "common_names": [ + "Emerald Empress Hens and Chicks" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum 'Firewitch'", + "common_names": [ + "Firewitch Hens and Chicks" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum 'Fuzzy Wuzzy'", + "common_names": [ + "Fuzzy Wuzzy Hens and Chicks" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum 'Galahad'", + "common_names": [ + "Galahad Hens and Chicks" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum 'Green Ice'", + "common_names": [ + "Green Ice Hens and Chicks" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum 'Grey Lady'", + "common_names": [ + "Grey Lady Hens and Chicks" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum 'Hayling'", + "common_names": [ + "Hayling Hens and Chicks" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum 'Hester'", + "common_names": [ + "Hester Hens and Chicks" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum 'Hey Hey'", + "common_names": [ + "Hey Hey Hens and Chicks" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum 'Hippie Chick'", + "common_names": [ + "Hippie Chick Hens and Chicks" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Sempervivum 'Jungle Shadows'", + "common_names": [ + "Jungle Shadows Hens and Chicks" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Mammillaria albicans", + "common_names": [ + "White-haired Mammillaria" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria albicoma", + "common_names": [ + "White-haired Pincushion" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria albilanata", + "common_names": [ + "White-wooled Mammillaria" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria backebergiana", + "common_names": [ + "Backeberg's Mammillaria" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria baumii", + "common_names": [ + "Baum's Mammillaria" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria bocasana 'Roseiflora'", + "common_names": [ + "Pink Powder Puff Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria boolii", + "common_names": [ + "Bool's Pincushion" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria camptotricha", + "common_names": [ + "Bird's Nest Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria candida", + "common_names": [ + "Snowball Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria carnea", + "common_names": [ + "Flesh-colored Mammillaria" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria columbiana", + "common_names": [ + "Colombian Mammillaria" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria compressa", + "common_names": [ + "Mother of Hundreds" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria crinita", + "common_names": [ + "Hairy Mammillaria" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria decipiens", + "common_names": [ + "Deceiving Mammillaria" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria densispina", + "common_names": [ + "Dense-spined Mammillaria" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria dixanthocentron", + "common_names": [ + "Two-colored Spine Mammillaria" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria duwei", + "common_names": [ + "Duwe's Mammillaria" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria elongata 'Cristata'", + "common_names": [ + "Brain Cactus", + "Crested Ladyfinger" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria erythrosperma", + "common_names": [ + "Red-seeded Mammillaria" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria formosa", + "common_names": [ + "Beautiful Mammillaria", + "Owl Eye Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria fragilis", + "common_names": [ + "Fragile Pincushion", + "Thimble Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria geminispina", + "common_names": [ + "Twin-spined Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria glassii", + "common_names": [ + "Glass' Mammillaria" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria huitzilopochtli", + "common_names": [ + "Aztec God Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Mammillaria karwinskiana", + "common_names": [ + "Karwinsky's Mammillaria" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Gymnocalycium andreae", + "common_names": [ + "Andrea's Gymnocalycium" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Gymnocalycium bruchii", + "common_names": [ + "Bruch's Gymnocalycium" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Gymnocalycium calochlorum", + "common_names": [ + "Beautiful Green Gymnocalycium" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Gymnocalycium capillaense", + "common_names": [ + "Hairy Gymnocalycium" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Gymnocalycium castellanosii", + "common_names": [ + "Castellanos' Gymnocalycium" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Gymnocalycium chiquitanum", + "common_names": [ + "Chiquitano Gymnocalycium" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Gymnocalycium damsii", + "common_names": [ + "Dams' Gymnocalycium" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Gymnocalycium erinaceum", + "common_names": [ + "Hedgehog Gymnocalycium" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Gymnocalycium friedrichii", + "common_names": [ + "Friedrich's Gymnocalycium" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Gymnocalycium gibbosum", + "common_names": [ + "Humped Gymnocalycium" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Gymnocalycium horstii", + "common_names": [ + "Horst's Gymnocalycium" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Gymnocalycium hossei", + "common_names": [ + "Hosse's Gymnocalycium" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Gymnocalycium marsoneri", + "common_names": [ + "Marsoner's Gymnocalycium" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Gymnocalycium mihanovichii 'Hibotan'", + "common_names": [ + "Moon Cactus", + "Ruby Ball Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Gymnocalycium monvillei", + "common_names": [ + "Monville's Gymnocalycium" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Gymnocalycium mostii", + "common_names": [ + "Most's Gymnocalycium" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Gymnocalycium neuhuberi", + "common_names": [ + "Neuhuber's Gymnocalycium" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Gymnocalycium pflanzii", + "common_names": [ + "Pflanz's Gymnocalycium" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Aechmea blanchetiana", + "common_names": [ + "Orange Aechmea" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Aechmea bracteata", + "common_names": [ + "Bracted Aechmea" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Aechmea calyculata", + "common_names": [ + "Cup Aechmea" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Aechmea caudata", + "common_names": [ + "Tailed Aechmea" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Aechmea chantinii", + "common_names": [ + "Amazonian Zebra Plant" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Aechmea coelestis", + "common_names": [ + "Sky Blue Aechmea" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Aechmea distichantha", + "common_names": [ + "Brazilian Vase Plant" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Aechmea fasciata 'Primera'", + "common_names": [ + "Primera Silver Vase" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Aechmea fasciata 'Purpurea'", + "common_names": [ + "Purple Silver Vase" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Aechmea fendleri", + "common_names": [ + "Fendler's Aechmea" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Aechmea fulgens", + "common_names": [ + "Coral Berry Bromeliad" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Aechmea gamosepala", + "common_names": [ + "Matchstick Bromeliad" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Aechmea lueddemanniana", + "common_names": [ + "Lueddemann's Aechmea" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Aechmea magdalenae", + "common_names": [ + "Magdalena Aechmea" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Aechmea mariae-reginae", + "common_names": [ + "Queen's Aechmea" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Aechmea mexicana", + "common_names": [ + "Mexican Aechmea" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Aechmea miniata", + "common_names": [ + "Miniature Aechmea" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Aechmea nudicaulis", + "common_names": [ + "Bare-stalked Aechmea" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Aechmea orlandiana", + "common_names": [ + "Orlando's Aechmea" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Aechmea pectinata", + "common_names": [ + "Comb-like Aechmea" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Billbergia amoena", + "common_names": [ + "Lovely Billbergia" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Billbergia distachia", + "common_names": [ + "Two-spiked Billbergia" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Billbergia elegans", + "common_names": [ + "Elegant Billbergia" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Billbergia euphemiae", + "common_names": [ + "Euphemia's Billbergia" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Billbergia horrida", + "common_names": [ + "Horrible Billbergia" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Billbergia leptopoda", + "common_names": [ + "Slender-stalked Billbergia" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Billbergia pyramidalis", + "common_names": [ + "Flaming Torch Bromeliad", + "Foolproof Plant" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Billbergia saundersii", + "common_names": [ + "Saunders' Billbergia" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Billbergia vittata", + "common_names": [ + "Banded Billbergia" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Monstera adansonii 'Archipelago'", + "common_names": [ + "Archipelago Monstera" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Monstera adansonii 'Indonesia'", + "common_names": [ + "Indonesian Form Adansonii" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Monstera adansonii 'Mint'", + "common_names": [ + "Mint Monstera" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Monstera deliciosa 'Albo Borsigiana'", + "common_names": [ + "Albo Monstera", + "White Variegated Monstera" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Monstera deliciosa 'Mint'", + "common_names": [ + "Mint Variegated Monstera" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Monstera epipremnoides", + "common_names": [ + "Esqueleto Monstera" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Monstera karstenianum", + "common_names": [ + "Peru Monstera" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Monstera spruceana", + "common_names": [ + "Spruce's Monstera" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Monstera tuberculata", + "common_names": [ + "Tuberculate Monstera" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Rhaphidophora beccarii", + "common_names": [ + "Beccari's Rhaphidophora" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Rhaphidophora celatocaulis", + "common_names": [ + "Hidden Stem Rhaphidophora" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Rhaphidophora cryptantha", + "common_names": [ + "Shingle Plant" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Rhaphidophora decursiva", + "common_names": [ + "Dragon Tail Plant" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Rhaphidophora foraminifera", + "common_names": [ + "Windowed Rhaphidophora" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Rhaphidophora hayi", + "common_names": [ + "Shingle Plant", + "Hay's Rhaphidophora" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Rhaphidophora hongkongensis", + "common_names": [ + "Hong Kong Rhaphidophora" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Rhaphidophora korthalsii", + "common_names": [ + "Korthal's Rhaphidophora" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Rhaphidophora pertusa", + "common_names": [ + "Perforated Rhaphidophora" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Rhaphidophora tetrasperma", + "common_names": [ + "Mini Monstera", + "Philodendron Ginny" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Rhaphidophora tetrasperma 'Variegata'", + "common_names": [ + "Variegated Mini Monstera" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Scindapsus aureus", + "common_names": [ + "Golden Pothos" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Scindapsus hederaceus", + "common_names": [ + "Ivy-leaved Scindapsus" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Scindapsus lucens", + "common_names": [ + "Shining Scindapsus" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Scindapsus officinalis", + "common_names": [ + "Medicinal Scindapsus" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Scindapsus pictus 'Jade Satin'", + "common_names": [ + "Jade Satin Scindapsus" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Scindapsus pictus 'Platinum'", + "common_names": [ + "Platinum Scindapsus" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Scindapsus pictus 'Silver Hero'", + "common_names": [ + "Silver Hero Scindapsus" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Scindapsus pictus 'Silver Lady'", + "common_names": [ + "Silver Lady Scindapsus" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Scindapsus siamensis", + "common_names": [ + "Siamese Scindapsus" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Epipremnum aureum 'Cebu Blue'", + "common_names": [ + "Cebu Blue Pothos" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Epipremnum aureum 'Glacier'", + "common_names": [ + "Glacier Pothos" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Epipremnum aureum 'Golden Goddess'", + "common_names": [ + "Golden Goddess Pothos" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Epipremnum aureum 'Shangri La'", + "common_names": [ + "Shangri La Pothos" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Epipremnum aureum 'Variegatum'", + "common_names": [ + "Variegated Golden Pothos" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Epipremnum pinnatum 'Albo Variegata'", + "common_names": [ + "Albo Dragon Tail" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dieffenbachia 'Amy'", + "common_names": [ + "Amy Dumb Cane" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dieffenbachia 'Aurora'", + "common_names": [ + "Aurora Dumb Cane" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dieffenbachia 'Bausei'", + "common_names": [ + "Bausei Dumb Cane" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dieffenbachia 'Camouflage'", + "common_names": [ + "Camouflage Dumb Cane" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dieffenbachia 'Delilah'", + "common_names": [ + "Delilah Dumb Cane" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dieffenbachia 'Exotica'", + "common_names": [ + "Exotica Dumb Cane" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dieffenbachia 'Green Magic'", + "common_names": [ + "Green Magic Dumb Cane" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dieffenbachia 'Honeydew'", + "common_names": [ + "Honeydew Dumb Cane" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dieffenbachia 'Marianne'", + "common_names": [ + "Marianne Dumb Cane" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dieffenbachia 'Mars'", + "common_names": [ + "Mars Dumb Cane" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dieffenbachia 'Panther'", + "common_names": [ + "Panther Dumb Cane" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dieffenbachia 'Paradise'", + "common_names": [ + "Paradise Dumb Cane" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dieffenbachia 'Rebecca'", + "common_names": [ + "Rebecca Dumb Cane" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dieffenbachia 'Rudolf Roehrs'", + "common_names": [ + "Rudolf Roehrs Dumb Cane" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dieffenbachia 'Sarah'", + "common_names": [ + "Sarah Dumb Cane" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dieffenbachia 'Snow'", + "common_names": [ + "Snow Dumb Cane" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dieffenbachia 'Sterling'", + "common_names": [ + "Sterling Dumb Cane" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dieffenbachia 'Sublime'", + "common_names": [ + "Sublime Dumb Cane" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dieffenbachia 'Tiki'", + "common_names": [ + "Tiki Dumb Cane" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dieffenbachia 'Triumph'", + "common_names": [ + "Triumph Dumb Cane" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dieffenbachia 'Tropic Marianne'", + "common_names": [ + "Tropic Marianne Dumb Cane" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Dieffenbachia 'Wilson's Delight'", + "common_names": [ + "Wilson's Delight Dumb Cane" + ], + "family": "Araceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Maranta arundinacea", + "common_names": [ + "Arrowroot", + "West Indian Arrowroot" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Maranta bicolor", + "common_names": [ + "Two-colored Prayer Plant" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Maranta cristata", + "common_names": [ + "Crested Prayer Plant" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Maranta leuconeura 'Erythroneura'", + "common_names": [ + "Red Prayer Plant", + "Herringbone Plant" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Maranta leuconeura 'Fascinator'", + "common_names": [ + "Fascinator Prayer Plant" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Maranta leuconeura 'Kerchoveana'", + "common_names": [ + "Rabbit's Foot Prayer Plant", + "Green Prayer Plant" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Maranta leuconeura 'Marisela'", + "common_names": [ + "Marisela Prayer Plant" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Maranta leuconeura 'Massangeana'", + "common_names": [ + "Black Prayer Plant" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Maranta leuconeura 'Silver Band'", + "common_names": [ + "Silver Band Prayer Plant" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Stromanthe sanguinea 'Charlie'", + "common_names": [ + "Charlie Stromanthe" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Stromanthe sanguinea 'Magicstar'", + "common_names": [ + "Magicstar Stromanthe" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Stromanthe thalia", + "common_names": [ + "Thalia's Stromanthe" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ctenanthe amabilis", + "common_names": [ + "Lovely Ctenanthe" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ctenanthe burle-marxii 'Amagris'", + "common_names": [ + "Grey Star Ctenanthe" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ctenanthe compressa", + "common_names": [ + "Compressed Ctenanthe" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ctenanthe oppenheimiana 'Tricolor'", + "common_names": [ + "Tricolor Never Never Plant" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ctenanthe pilosa", + "common_names": [ + "Hairy Ctenanthe" + ], + "family": "Marantaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus aspera 'Parcellii'", + "common_names": [ + "Clown Fig" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus audrey", + "common_names": [ + "Banyan Tree", + "Bengal Fig" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus benjamina 'Danielle'", + "common_names": [ + "Danielle Weeping Fig" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus benjamina 'Exotica'", + "common_names": [ + "Exotic Weeping Fig" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus benjamina 'Golden King'", + "common_names": [ + "Golden King Ficus" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus benjamina 'Midnight'", + "common_names": [ + "Midnight Weeping Fig" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus benjamina 'Too Little'", + "common_names": [ + "Dwarf Weeping Fig" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus binnendijkii", + "common_names": [ + "Narrow Leaf Fig", + "Banana Leaf Ficus" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus binnendijkii 'Alii'", + "common_names": [ + "Alii Ficus", + "Banana Leaf Fig" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus binnendijkii 'Amstel King'", + "common_names": [ + "Amstel King Ficus" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus buxifolia", + "common_names": [ + "Box-leaved Fig" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus carica 'Brown Turkey'", + "common_names": [ + "Brown Turkey Fig" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus carica 'Petite Negra'", + "common_names": [ + "Dwarf Fig", + "Petite Negra Fig" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus cyathistipula", + "common_names": [ + "African Fig" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus deltoidea", + "common_names": [ + "Mistletoe Fig" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus elastica 'Abidjan'", + "common_names": [ + "Abidjan Rubber Plant" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus elastica 'Belize'", + "common_names": [ + "Belize Rubber Plant" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus elastica 'Decora'", + "common_names": [ + "Decora Rubber Plant" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus elastica 'Doescheri'", + "common_names": [ + "Doescheri Rubber Plant" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus elastica 'Melany'", + "common_names": [ + "Melany Rubber Plant" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus elastica 'Sophia'", + "common_names": [ + "Sophia Rubber Plant" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Ficus elastica 'Tricolor'", + "common_names": [ + "Tricolor Rubber Plant" + ], + "family": "Moraceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Pilea cadierei 'Minima'", + "common_names": [ + "Dwarf Aluminum Plant" + ], + "family": "Urticaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Pilea depressa", + "common_names": [ + "Depressed Clearweed", + "Baby Tears Pilea" + ], + "family": "Urticaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Pilea grandifolia", + "common_names": [ + "Large-leaved Pilea" + ], + "family": "Urticaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Pilea involucrata 'Moon Valley'", + "common_names": [ + "Moon Valley Pilea" + ], + "family": "Urticaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Pilea involucrata 'Norfolk'", + "common_names": [ + "Norfolk Pilea" + ], + "family": "Urticaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Pilea libanensis", + "common_names": [ + "Silver Tree Pilea" + ], + "family": "Urticaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Pilea pubescens 'Liebmanii'", + "common_names": [ + "Silver Cloud Pilea" + ], + "family": "Urticaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Pilea serpyllacea", + "common_names": [ + "Creeping Pilea" + ], + "family": "Urticaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Pilea serpyllifolia", + "common_names": [ + "Thyme-leaved Pilea" + ], + "family": "Urticaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Pilea spruceana", + "common_names": [ + "Spruce's Pilea", + "Silver Tree" + ], + "family": "Urticaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Pilea spruceana 'Ellen'", + "common_names": [ + "Ellen Pilea" + ], + "family": "Urticaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Pilea spruceana 'Norfolk'", + "common_names": [ + "Norfolk Silver Tree" + ], + "family": "Urticaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Tradescantia albiflora", + "common_names": [ + "White-flowered Wandering Jew" + ], + "family": "Commelinaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Tradescantia albiflora 'Albovittata'", + "common_names": [ + "White Striped Wandering Jew" + ], + "family": "Commelinaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Tradescantia blossfeldiana", + "common_names": [ + "Flowering Inch Plant" + ], + "family": "Commelinaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Tradescantia cerinthoides", + "common_names": [ + "Flowering Inch Plant" + ], + "family": "Commelinaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Tradescantia chrysophylla", + "common_names": [ + "Baby Bunny Bellies" + ], + "family": "Commelinaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Tradescantia fluminensis 'Aurea'", + "common_names": [ + "Golden Wandering Jew" + ], + "family": "Commelinaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Tradescantia fluminensis 'Lavender'", + "common_names": [ + "Lavender Wandering Jew" + ], + "family": "Commelinaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Tradescantia fluminensis 'Tricolor'", + "common_names": [ + "Tricolor Wandering Jew" + ], + "family": "Commelinaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Tradescantia mundula", + "common_names": [ + "Mundula Tradescantia" + ], + "family": "Commelinaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Tradescantia mundula 'Laekenensis'", + "common_names": [ + "Rainbow Tradescantia" + ], + "family": "Commelinaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Tradescantia nanouk", + "common_names": [ + "Fantasy Venice", + "Nanouk Tradescantia" + ], + "family": "Commelinaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Tradescantia pallida 'Pale Puma'", + "common_names": [ + "Pale Puma Tradescantia" + ], + "family": "Commelinaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Tradescantia pallida 'Purpurea'", + "common_names": [ + "Purple Queen" + ], + "family": "Commelinaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Tradescantia spathacea 'Sitara'", + "common_names": [ + "Sitara Oyster Plant" + ], + "family": "Commelinaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Tradescantia spathacea 'Tricolor'", + "common_names": [ + "Tricolor Oyster Plant" + ], + "family": "Commelinaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Tradescantia virginiana", + "common_names": [ + "Virginia Spiderwort" + ], + "family": "Commelinaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Tradescantia zebrina 'Purpusii'", + "common_names": [ + "Bronze Wandering Jew" + ], + "family": "Commelinaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Tradescantia zebrina 'Quadricolor'", + "common_names": [ + "Quadricolor Wandering Jew" + ], + "family": "Commelinaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Chlorophytum amaniense", + "common_names": [ + "Fire Flash", + "Orange Spider Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Chlorophytum amaniense 'Bonnie'", + "common_names": [ + "Curly Fire Flash" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Chlorophytum bichetii", + "common_names": [ + "Bichet's Spider Plant", + "Siam Lily" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Chlorophytum capense", + "common_names": [ + "Cape Spider Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Chlorophytum comosum 'Atlantic'", + "common_names": [ + "Atlantic Spider Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Chlorophytum comosum 'Hawaiian'", + "common_names": [ + "Hawaiian Spider Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Chlorophytum comosum 'Lemon'", + "common_names": [ + "Lemon Spider Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Chlorophytum comosum 'Mandaianum'", + "common_names": [ + "Mandaianum Spider Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Chlorophytum comosum 'Ocean'", + "common_names": [ + "Ocean Spider Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Chlorophytum comosum 'Picturatum'", + "common_names": [ + "Picturatum Spider Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Chlorophytum comosum 'Reverse Variegatum'", + "common_names": [ + "Reverse Spider Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Chlorophytum comosum 'Variegatum'", + "common_names": [ + "Variegated Spider Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Chlorophytum laxum", + "common_names": [ + "Zebra Grass", + "Bichetii" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Chlorophytum orchidastrum", + "common_names": [ + "Orange Star", + "Fire Flash" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Chlorophytum orchidastrum 'Green Orange'", + "common_names": [ + "Green Orange Spider Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Asparagus aethiopicus", + "common_names": [ + "Sprengeri Fern", + "Sprenger's Asparagus" + ], + "family": "Asparagaceae", + "category": "Fern" + }, + { + "scientific_name": "Asparagus aethiopicus 'Meyeri'", + "common_names": [ + "Foxtail Fern", + "Myers Fern" + ], + "family": "Asparagaceae", + "category": "Fern" + }, + { + "scientific_name": "Asparagus aethiopicus 'Sprengeri'", + "common_names": [ + "Sprengeri Fern", + "Emerald Fern" + ], + "family": "Asparagaceae", + "category": "Fern" + }, + { + "scientific_name": "Asparagus africanus", + "common_names": [ + "African Asparagus Fern" + ], + "family": "Asparagaceae", + "category": "Fern" + }, + { + "scientific_name": "Asparagus asparagoides", + "common_names": [ + "Smilax", + "Bridal Creeper" + ], + "family": "Asparagaceae", + "category": "Fern" + }, + { + "scientific_name": "Asparagus densiflorus 'Cwebe'", + "common_names": [ + "Cwebe Asparagus Fern" + ], + "family": "Asparagaceae", + "category": "Fern" + }, + { + "scientific_name": "Asparagus densiflorus 'Mazeppa'", + "common_names": [ + "Mazeppa Asparagus Fern" + ], + "family": "Asparagaceae", + "category": "Fern" + }, + { + "scientific_name": "Asparagus falcatus", + "common_names": [ + "Sicklethorn Fern", + "Climbing Asparagus" + ], + "family": "Asparagaceae", + "category": "Fern" + }, + { + "scientific_name": "Asparagus macowanii", + "common_names": [ + "Ming Asparagus Fern", + "Zigzag Fern" + ], + "family": "Asparagaceae", + "category": "Fern" + }, + { + "scientific_name": "Asparagus officinalis", + "common_names": [ + "Garden Asparagus" + ], + "family": "Asparagaceae", + "category": "Fern" + }, + { + "scientific_name": "Asparagus plumosus", + "common_names": [ + "Climbing Asparagus Fern", + "Lace Fern" + ], + "family": "Asparagaceae", + "category": "Fern" + }, + { + "scientific_name": "Asparagus retrofractus", + "common_names": [ + "Ming Fern", + "Pom Pom Asparagus" + ], + "family": "Asparagaceae", + "category": "Fern" + }, + { + "scientific_name": "Asparagus scandens", + "common_names": [ + "Climbing Asparagus" + ], + "family": "Asparagaceae", + "category": "Fern" + }, + { + "scientific_name": "Asparagus setaceus 'Nanus'", + "common_names": [ + "Dwarf Asparagus Fern" + ], + "family": "Asparagaceae", + "category": "Fern" + }, + { + "scientific_name": "Asparagus setaceus 'Pyramidalis'", + "common_names": [ + "Pyramid Asparagus Fern" + ], + "family": "Asparagaceae", + "category": "Fern" + }, + { + "scientific_name": "Asparagus setaceus 'Robustus'", + "common_names": [ + "Robust Asparagus Fern" + ], + "family": "Asparagaceae", + "category": "Fern" + }, + { + "scientific_name": "Asparagus umbellatus", + "common_names": [ + "Umbrella Asparagus Fern" + ], + "family": "Asparagaceae", + "category": "Fern" + }, + { + "scientific_name": "Asparagus virgatus", + "common_names": [ + "Tree Fern Asparagus", + "Broom Fern" + ], + "family": "Asparagaceae", + "category": "Fern" + }, + { + "scientific_name": "Euphorbia abdelkuri", + "common_names": [ + "Damask Spurge" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia aeruginosa", + "common_names": [ + "Verdigris Spurge" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia aggregata", + "common_names": [ + "Cluster Euphorbia" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia alluaudii", + "common_names": [ + "Cat Tails Euphorbia" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia ammak", + "common_names": [ + "African Candelabra" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia antisyphilitica", + "common_names": [ + "Candelilla" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia baioensis", + "common_names": [ + "Baio Euphorbia" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia bupleurifolia", + "common_names": [ + "Pine Cone Euphorbia" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia caerulescens", + "common_names": [ + "Blue Euphorbia", + "Sweet Noors" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia canariensis", + "common_names": [ + "Canary Island Spurge" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia cap-saintemariensis", + "common_names": [ + "Cap Sainte Marie Euphorbia" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia cereiformis", + "common_names": [ + "Milk Barrel" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia clandestina", + "common_names": [ + "Hidden Euphorbia" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia coerulescens", + "common_names": [ + "Blue Spurge" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia cooperi", + "common_names": [ + "Candelabra Tree" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia decaryi", + "common_names": [ + "Zigzag Plant" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia enopla", + "common_names": [ + "Pincushion Euphorbia" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia erythraea", + "common_names": [ + "Desert Candle" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia flanaganii", + "common_names": [ + "Green Crown", + "Medusa's Head" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia francoisi", + "common_names": [ + "Francois' Euphorbia" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia globosa", + "common_names": [ + "Globe Euphorbia" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia grandicornis", + "common_names": [ + "Big Horn Euphorbia", + "Cow's Horn" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia groenewaldii", + "common_names": [ + "Groenewald's Euphorbia" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia horrida", + "common_names": [ + "African Milk Barrel" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia knuthii", + "common_names": [ + "Knuth's Euphorbia" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Euphorbia lactea 'White Ghost'", + "common_names": [ + "White Ghost Euphorbia" + ], + "family": "Euphorbiaceae", + "category": "Succulent" + }, + { + "scientific_name": "Opuntia aciculata", + "common_names": [ + "Needle-spined Prickly Pear" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Opuntia chlorotica", + "common_names": [ + "Dollarjoint Prickly Pear" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Opuntia compressa", + "common_names": [ + "Eastern Prickly Pear" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Opuntia macrocentra", + "common_names": [ + "Purple Prickly Pear", + "Black-spined Prickly Pear" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Opuntia microdasys 'Albata'", + "common_names": [ + "Angel Wings Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Opuntia microdasys 'Rufida'", + "common_names": [ + "Cinnamon Bunny Ears" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Opuntia phaeacantha", + "common_names": [ + "Brown-spined Prickly Pear" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Opuntia polyacantha", + "common_names": [ + "Plains Prickly Pear", + "Starvation Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Opuntia robusta", + "common_names": [ + "Wheel Cactus", + "Silver Dollar Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Opuntia rufida", + "common_names": [ + "Blind Prickly Pear" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Opuntia stricta", + "common_names": [ + "Erect Prickly Pear" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Opuntia tuna", + "common_names": [ + "Elephant Ear Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Gasteria armstrongii", + "common_names": [ + "Armstrong's Gasteria" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Gasteria batesiana", + "common_names": [ + "Bates' Gasteria" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Gasteria bicolor", + "common_names": [ + "Two-colored Gasteria" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Gasteria croucheri", + "common_names": [ + "Croucher's Gasteria" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Gasteria disticha", + "common_names": [ + "Distichous Gasteria" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Gasteria ellaphieae", + "common_names": [ + "Ellie's Gasteria" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Gasteria excelsa", + "common_names": [ + "Tall Gasteria" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Gasteria glomerata", + "common_names": [ + "Clustered Gasteria" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Gasteria nitida", + "common_names": [ + "Shiny Gasteria" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Gasteria pillansii", + "common_names": [ + "Pillans' Gasteria" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Gasteria pulchra", + "common_names": [ + "Beautiful Gasteria" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Gasteria rawlinsonii", + "common_names": [ + "Rawlinson's Gasteria" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Gasteria vlokii", + "common_names": [ + "Vlok's Gasteria" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "x Gasteraloe 'Flow'", + "common_names": [ + "Flow Gasteraloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "x Gasteraloe 'Green Ice'", + "common_names": [ + "Green Ice Gasteraloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "x Gasteraloe 'Midnight'", + "common_names": [ + "Midnight Gasteraloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "x Gasteraloe 'Silver Swirls'", + "common_names": [ + "Silver Swirls Gasteraloe" + ], + "family": "Asphodelaceae", + "category": "Succulent" + }, + { + "scientific_name": "Lithops bella", + "common_names": [ + "Beautiful Living Stone" + ], + "family": "Aizoaceae", + "category": "Succulent" + }, + { + "scientific_name": "Lithops bromfieldii", + "common_names": [ + "Bromfield's Living Stone" + ], + "family": "Aizoaceae", + "category": "Succulent" + }, + { + "scientific_name": "Lithops comptonii", + "common_names": [ + "Compton's Living Stone" + ], + "family": "Aizoaceae", + "category": "Succulent" + }, + { + "scientific_name": "Lithops dinteri", + "common_names": [ + "Dinter's Living Stone" + ], + "family": "Aizoaceae", + "category": "Succulent" + }, + { + "scientific_name": "Lithops dorotheae", + "common_names": [ + "Dorothea's Living Stone" + ], + "family": "Aizoaceae", + "category": "Succulent" + }, + { + "scientific_name": "Lithops francisci", + "common_names": [ + "Francis' Living Stone" + ], + "family": "Aizoaceae", + "category": "Succulent" + }, + { + "scientific_name": "Lithops fulviceps", + "common_names": [ + "Tawny-headed Living Stone" + ], + "family": "Aizoaceae", + "category": "Succulent" + }, + { + "scientific_name": "Lithops gesinae", + "common_names": [ + "Gesine's Living Stone" + ], + "family": "Aizoaceae", + "category": "Succulent" + }, + { + "scientific_name": "Lithops geyeri", + "common_names": [ + "Geyer's Living Stone" + ], + "family": "Aizoaceae", + "category": "Succulent" + }, + { + "scientific_name": "Lithops gracilidelineata", + "common_names": [ + "Graceful Living Stone" + ], + "family": "Aizoaceae", + "category": "Succulent" + }, + { + "scientific_name": "Lithops hallii", + "common_names": [ + "Hall's Living Stone" + ], + "family": "Aizoaceae", + "category": "Succulent" + }, + { + "scientific_name": "Lithops helmutii", + "common_names": [ + "Helmut's Living Stone" + ], + "family": "Aizoaceae", + "category": "Succulent" + }, + { + "scientific_name": "Lithops hermetica", + "common_names": [ + "Hermetic Living Stone" + ], + "family": "Aizoaceae", + "category": "Succulent" + }, + { + "scientific_name": "Lithops herrei", + "common_names": [ + "Herre's Living Stone" + ], + "family": "Aizoaceae", + "category": "Succulent" + }, + { + "scientific_name": "Lithops hookeri", + "common_names": [ + "Hooker's Living Stone" + ], + "family": "Aizoaceae", + "category": "Succulent" + }, + { + "scientific_name": "Lithops julii", + "common_names": [ + "Juli's Living Stone" + ], + "family": "Aizoaceae", + "category": "Succulent" + }, + { + "scientific_name": "Lithops localis", + "common_names": [ + "Local Living Stone" + ], + "family": "Aizoaceae", + "category": "Succulent" + }, + { + "scientific_name": "Senecio articulatus", + "common_names": [ + "Hot Dog Cactus", + "Candle Plant" + ], + "family": "Asteraceae", + "category": "Succulent" + }, + { + "scientific_name": "Senecio barbertonicus", + "common_names": [ + "Succulent Bush Senecio", + "Lemon Bean Bush" + ], + "family": "Asteraceae", + "category": "Succulent" + }, + { + "scientific_name": "Senecio cephalophorus", + "common_names": [ + "Mountain Fire" + ], + "family": "Asteraceae", + "category": "Succulent" + }, + { + "scientific_name": "Senecio citriformis", + "common_names": [ + "String of Tears", + "Lemon Balls" + ], + "family": "Asteraceae", + "category": "Succulent" + }, + { + "scientific_name": "Senecio crassissimus", + "common_names": [ + "Vertical Leaf Senecio", + "Propeller Plant" + ], + "family": "Asteraceae", + "category": "Succulent" + }, + { + "scientific_name": "Senecio ficoides", + "common_names": [ + "Mount Everest Senecio", + "Skyscraper Senecio" + ], + "family": "Asteraceae", + "category": "Succulent" + }, + { + "scientific_name": "Senecio haworthii", + "common_names": [ + "Cocoon Plant", + "Woolly Senecio" + ], + "family": "Asteraceae", + "category": "Succulent" + }, + { + "scientific_name": "Senecio herreanus", + "common_names": [ + "String of Watermelons", + "String of Beads" + ], + "family": "Asteraceae", + "category": "Succulent" + }, + { + "scientific_name": "Senecio jacobsenii", + "common_names": [ + "Trailing Jade", + "Weeping Jade" + ], + "family": "Asteraceae", + "category": "Succulent" + }, + { + "scientific_name": "Senecio kleiniiformis", + "common_names": [ + "Spearhead Senecio" + ], + "family": "Asteraceae", + "category": "Succulent" + }, + { + "scientific_name": "Senecio macroglossus", + "common_names": [ + "Wax Ivy", + "Natal Ivy", + "Cape Ivy" + ], + "family": "Asteraceae", + "category": "Succulent" + }, + { + "scientific_name": "Senecio rowleyanus 'Variegata'", + "common_names": [ + "Variegated String of Pearls" + ], + "family": "Asteraceae", + "category": "Succulent" + }, + { + "scientific_name": "Senecio scaposus", + "common_names": [ + "Silver Coral", + "Woolly Groundsel" + ], + "family": "Asteraceae", + "category": "Succulent" + }, + { + "scientific_name": "Senecio stapeliiformis", + "common_names": [ + "Pickle Plant", + "Trailing Jade" + ], + "family": "Asteraceae", + "category": "Succulent" + }, + { + "scientific_name": "Kalanchoe beharensis 'Fang'", + "common_names": [ + "Fang Kalanchoe" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Kalanchoe delagoensis", + "common_names": [ + "Chandelier Plant", + "Mother of Millions" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Kalanchoe fedtschenkoi 'Variegata'", + "common_names": [ + "Variegated Lavender Scallops" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Kalanchoe gastonis-bonnieri", + "common_names": [ + "Donkey Ears", + "Giant Kalanchoe" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Kalanchoe humilis", + "common_names": [ + "Spotted Kalanchoe", + "Desert Surprise" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Kalanchoe laetivirens", + "common_names": [ + "Mother of Thousands" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Kalanchoe manginii", + "common_names": [ + "Chandelier Plant", + "Beach Bells" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Kalanchoe marmorata", + "common_names": [ + "Penwiper Plant" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Kalanchoe millotii", + "common_names": [ + "Millot Kalanchoe" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Kalanchoe orgyalis", + "common_names": [ + "Copper Spoons" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Kalanchoe pumila", + "common_names": [ + "Flower Dust Plant" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Kalanchoe rhombopilosa", + "common_names": [ + "Pie from the Sky" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Kalanchoe sexangularis", + "common_names": [ + "Six-angled Kalanchoe" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Kalanchoe synsepala", + "common_names": [ + "Walking Kalanchoe" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Kalanchoe tetraphylla", + "common_names": [ + "Paddle Plant" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Kalanchoe tomentosa 'Chocolate Soldier'", + "common_names": [ + "Chocolate Soldier" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Kalanchoe tomentosa 'Golden Girl'", + "common_names": [ + "Golden Panda Plant" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Kalanchoe tubiflora", + "common_names": [ + "Mother of Millions", + "Chandelier Plant" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Kalanchoe uniflora", + "common_names": [ + "Coral Bells" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Graptopetalum amethystinum", + "common_names": [ + "Lavender Pebbles", + "Amethyst Graptopetalum" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Graptopetalum bellum", + "common_names": [ + "Chihuahua Flower", + "Tacitus" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Graptopetalum filiferum", + "common_names": [ + "Thread-leaved Graptopetalum" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Graptopetalum macdougallii", + "common_names": [ + "MacDougall's Graptopetalum" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Graptopetalum mendozae", + "common_names": [ + "Mendoza's Graptopetalum" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Graptopetalum pachyphyllum", + "common_names": [ + "Thick-leaved Graptopetalum", + "Bluebean" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Graptopetalum pentandrum", + "common_names": [ + "Five-stamened Graptopetalum" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Graptopetalum rusbyi", + "common_names": [ + "Rusby's Graptopetalum", + "Leatherpetal" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Graptopetalum superbum", + "common_names": [ + "Superb Graptopetalum", + "Beautiful Graptopetalum" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "x Graptoveria 'Debbi'", + "common_names": [ + "Debbi Graptoveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "x Graptoveria 'Douglas Huth'", + "common_names": [ + "Douglas Huth Graptoveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "x Graptoveria 'Fred Ives'", + "common_names": [ + "Fred Ives Graptoveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "x Graptoveria 'Moonglow'", + "common_names": [ + "Moonglow Graptoveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "x Graptoveria 'Opalina'", + "common_names": [ + "Opalina Graptoveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "x Graptoveria 'Purple Haze'", + "common_names": [ + "Purple Haze Graptoveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "x Graptoveria 'Silver Star'", + "common_names": [ + "Silver Star Graptoveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "x Graptoveria 'Spirit of '76'", + "common_names": [ + "Spirit of '76 Graptoveria" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "x Graptoveria 'Titubans'", + "common_names": [ + "Titubans Graptoveria", + "Porcelain Plant" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Adromischus alstonii", + "common_names": [ + "Alston's Adromischus" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Adromischus cooperi", + "common_names": [ + "Plover Eggs Plant" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Adromischus cristatus 'Indian Clubs'", + "common_names": [ + "Indian Clubs Adromischus" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Adromischus filicaulis", + "common_names": [ + "Thread-stemmed Adromischus" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Adromischus hemisphaericus", + "common_names": [ + "Half-sphere Adromischus" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Adromischus maculatus", + "common_names": [ + "Spotted Adromischus", + "Calico Hearts" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Adromischus marianae", + "common_names": [ + "Mariana's Adromischus" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Adromischus schuldtianus", + "common_names": [ + "Schuldt's Adromischus" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Adromischus triflorus", + "common_names": [ + "Three-flowered Adromischus" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Rebutia albiflora", + "common_names": [ + "White-flowered Rebutia" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Rebutia arenacea", + "common_names": [ + "Sandy Rebutia" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Rebutia canigueralii", + "common_names": [ + "Canigueral's Rebutia" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Rebutia deminuta", + "common_names": [ + "Diminutive Rebutia" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Rebutia fabrisii", + "common_names": [ + "Fabrisi's Rebutia" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Rebutia fiebrigii", + "common_names": [ + "Fiebrig's Rebutia" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Rebutia flavistyla", + "common_names": [ + "Yellow-styled Rebutia" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Rebutia heliosa", + "common_names": [ + "Sunrise Rebutia", + "Crown Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Rebutia krainziana", + "common_names": [ + "Krainz's Rebutia" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Rebutia marsoneri", + "common_names": [ + "Marsoner's Crown Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Rebutia mentosa", + "common_names": [ + "Woolly Rebutia" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Rebutia muscula", + "common_names": [ + "Orange Snowball", + "White-haired Crown Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Rebutia narvaecensis", + "common_names": [ + "Narvaez Rebutia" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Rebutia perplexa", + "common_names": [ + "Perplexed Rebutia" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Rebutia pygmaea", + "common_names": [ + "Pygmy Crown Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Rebutia senilis", + "common_names": [ + "Fire Crown Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Rebutia wessneriana", + "common_names": [ + "Wessner's Crown Cactus" + ], + "family": "Cactaceae", + "category": "Cactus" + }, + { + "scientific_name": "Portulacaria afra 'Aurea'", + "common_names": [ + "Yellow Rainbow Bush" + ], + "family": "Didiereaceae", + "category": "Succulent" + }, + { + "scientific_name": "Portulacaria afra 'Cascade'", + "common_names": [ + "Cascading Elephant Bush" + ], + "family": "Didiereaceae", + "category": "Succulent" + }, + { + "scientific_name": "Portulacaria afra 'Foliis Variegatis'", + "common_names": [ + "Variegated Elephant Bush", + "Rainbow Bush" + ], + "family": "Didiereaceae", + "category": "Succulent" + }, + { + "scientific_name": "Portulacaria afra 'Limpopo'", + "common_names": [ + "Limpopo Elephant Bush" + ], + "family": "Didiereaceae", + "category": "Succulent" + }, + { + "scientific_name": "Portulacaria afra 'Macrophylla'", + "common_names": [ + "Large Leaf Elephant Bush" + ], + "family": "Didiereaceae", + "category": "Succulent" + }, + { + "scientific_name": "Portulacaria afra 'Medio-picta'", + "common_names": [ + "Mid-stripe Elephant Bush" + ], + "family": "Didiereaceae", + "category": "Succulent" + }, + { + "scientific_name": "Portulacaria afra 'Minima'", + "common_names": [ + "Miniature Elephant Bush" + ], + "family": "Didiereaceae", + "category": "Succulent" + }, + { + "scientific_name": "Portulacaria afra 'Prostrata'", + "common_names": [ + "Prostrate Elephant Bush" + ], + "family": "Didiereaceae", + "category": "Succulent" + }, + { + "scientific_name": "Cotyledon barbeyi", + "common_names": [ + "Barbey's Cotyledon" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Cotyledon campanulata", + "common_names": [ + "Bell-shaped Cotyledon" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Cotyledon ladismithiensis", + "common_names": [ + "Bear's Paw", + "Cat's Paw" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Cotyledon macrantha", + "common_names": [ + "Large-flowered Cotyledon" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Cotyledon orbiculata 'Flanaganii'", + "common_names": [ + "Flanagan's Pig's Ear" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Cotyledon orbiculata 'Oophylla'", + "common_names": [ + "Egg-leaved Pig's Ear" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Cotyledon orbiculata 'Takbok'", + "common_names": [ + "Takbok Cotyledon" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Cotyledon papillaris", + "common_names": [ + "Papillary Cotyledon" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Cotyledon pendens", + "common_names": [ + "Cliff Cotyledon" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Cotyledon tomentosa 'Ladismithiensis'", + "common_names": [ + "Ladismith Bear's Paw" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Cotyledon tomentosa 'Variegata'", + "common_names": [ + "Variegated Bear's Paw" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Cotyledon undulata", + "common_names": [ + "Silver Crown", + "Silver Ruffles" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Cotyledon velutina", + "common_names": [ + "Velvety Cotyledon" + ], + "family": "Crassulaceae", + "category": "Succulent" + }, + { + "scientific_name": "Phalaenopsis amboinensis", + "common_names": [ + "Ambon Phalaenopsis" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Phalaenopsis aphrodite", + "common_names": [ + "Aphrodite's Phalaenopsis" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Phalaenopsis bellina", + "common_names": [ + "Beautiful Phalaenopsis" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Phalaenopsis cornu-cervi", + "common_names": [ + "Stag Horn Phalaenopsis" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Phalaenopsis gigantea", + "common_names": [ + "Giant Phalaenopsis", + "Elephant Ear Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Phalaenopsis hieroglyphica", + "common_names": [ + "Hieroglyphic Phalaenopsis" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Phalaenopsis lobbii", + "common_names": [ + "Lobb's Phalaenopsis" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Phalaenopsis lueddemanniana", + "common_names": [ + "Lueddemann's Phalaenopsis" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Phalaenopsis mannii", + "common_names": [ + "Mann's Phalaenopsis" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Phalaenopsis mariae", + "common_names": [ + "Maria's Phalaenopsis" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Phalaenopsis modesta", + "common_names": [ + "Modest Phalaenopsis" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Phalaenopsis pallens", + "common_names": [ + "Pale Phalaenopsis" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Phalaenopsis pulcherrima", + "common_names": [ + "Most Beautiful Phalaenopsis" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Phalaenopsis sanderiana", + "common_names": [ + "Sander's Phalaenopsis" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Phalaenopsis speciosa", + "common_names": [ + "Showy Phalaenopsis" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Phalaenopsis sumatrana", + "common_names": [ + "Sumatran Phalaenopsis" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Phalaenopsis tetraspis", + "common_names": [ + "Four-shielded Phalaenopsis" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Phalaenopsis venosa", + "common_names": [ + "Veined Phalaenopsis" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Phalaenopsis violacea", + "common_names": [ + "Violet Phalaenopsis" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Phalaenopsis wilsonii", + "common_names": [ + "Wilson's Phalaenopsis" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Dendrobium aggregatum", + "common_names": [ + "Clustered Dendrobium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Dendrobium aphyllum", + "common_names": [ + "Leafless Dendrobium", + "Hooded Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Dendrobium bigibbum", + "common_names": [ + "Cooktown Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Dendrobium chrysotoxum", + "common_names": [ + "Fried Egg Orchid", + "Golden Bow Dendrobium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Dendrobium cuthbertsonii", + "common_names": [ + "Cuthbertson's Dendrobium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Dendrobium densiflorum", + "common_names": [ + "Dense Dendrobium", + "Pineapple Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Dendrobium fimbriatum", + "common_names": [ + "Fringed Dendrobium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Dendrobium findlayanum", + "common_names": [ + "Findlay's Dendrobium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Dendrobium formosum", + "common_names": [ + "Beautiful Dendrobium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Dendrobium harveyanum", + "common_names": [ + "Harvey's Dendrobium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Dendrobium infundibulum", + "common_names": [ + "Funnel-shaped Dendrobium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Dendrobium lindleyi", + "common_names": [ + "Lindley's Dendrobium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Dendrobium loddigesii", + "common_names": [ + "Loddiges' Dendrobium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Dendrobium moschatum", + "common_names": [ + "Musk-scented Dendrobium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Dendrobium parishii", + "common_names": [ + "Parish's Dendrobium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Dendrobium peguanum", + "common_names": [ + "Pegu Dendrobium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Dendrobium primulinum", + "common_names": [ + "Primrose Dendrobium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Dendrobium spectabile", + "common_names": [ + "Spectacular Dendrobium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Dendrobium speciosum", + "common_names": [ + "Rock Lily", + "King Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Dendrobium thyrsiflorum", + "common_names": [ + "Pinecone-like Dendrobium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Oncidium altissimum", + "common_names": [ + "Tall Oncidium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Oncidium ampliatum", + "common_names": [ + "Turtle Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Oncidium baueri", + "common_names": [ + "Bauer's Oncidium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Oncidium bifolium", + "common_names": [ + "Two-leaved Oncidium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Oncidium cebolleta", + "common_names": [ + "Rat Tail Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Oncidium cheirophorum", + "common_names": [ + "Hand-bearing Oncidium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Oncidium crispum", + "common_names": [ + "Curly Oncidium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Oncidium flexuosum", + "common_names": [ + "Dancing Lady Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Oncidium forbesii", + "common_names": [ + "Forbes' Oncidium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Oncidium gardneri", + "common_names": [ + "Gardner's Oncidium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Oncidium hastatum", + "common_names": [ + "Spear-leaved Oncidium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Oncidium jonesianum", + "common_names": [ + "Jones' Oncidium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Oncidium leucochilum", + "common_names": [ + "White-lipped Oncidium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Oncidium luridum", + "common_names": [ + "Pale Oncidium", + "Mule Ear Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Oncidium maculatum", + "common_names": [ + "Spotted Oncidium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Oncidium ornithorhynchum", + "common_names": [ + "Bird's Beak Oncidium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Oncidium papilio", + "common_names": [ + "Butterfly Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Oncidium pulchellum", + "common_names": [ + "Pretty Oncidium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Oncidium sarcodes", + "common_names": [ + "Fleshy Oncidium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Oncidium sphacelatum", + "common_names": [ + "Golden Shower Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Oncidium tigrinum", + "common_names": [ + "Tiger Oncidium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Oncidium varicosum", + "common_names": [ + "Variable Oncidium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Cymbidium aloifolium", + "common_names": [ + "Aloe-leaved Cymbidium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Cymbidium bicolor", + "common_names": [ + "Two-colored Cymbidium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Cymbidium canaliculatum", + "common_names": [ + "Channel-leaved Cymbidium", + "Tiger Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Cymbidium dayanum", + "common_names": [ + "Day's Cymbidium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Cymbidium devonianum", + "common_names": [ + "Devon's Cymbidium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Cymbidium eburneum", + "common_names": [ + "Ivory Cymbidium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Cymbidium ensifolium", + "common_names": [ + "Sword-leaved Cymbidium", + "Jian Lan" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Cymbidium erythraeum", + "common_names": [ + "Red Cymbidium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Cymbidium finlaysonianum", + "common_names": [ + "Finlayson's Cymbidium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Cymbidium floribundum", + "common_names": [ + "Many-flowered Cymbidium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Cymbidium goeringii", + "common_names": [ + "Goering's Cymbidium", + "Noble Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Cymbidium hookerianum", + "common_names": [ + "Hooker's Cymbidium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Cymbidium insigne", + "common_names": [ + "Remarkable Cymbidium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Cymbidium kanran", + "common_names": [ + "Cold-growing Cymbidium", + "Han Lan" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Cymbidium lowianum", + "common_names": [ + "Lowe's Cymbidium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Cymbidium mastersii", + "common_names": [ + "Masters' Cymbidium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Cymbidium sinense", + "common_names": [ + "Chinese Cymbidium", + "Mo Lan" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Cymbidium suave", + "common_names": [ + "Snake Orchid", + "Grass Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Cymbidium tracyanum", + "common_names": [ + "Tracy's Cymbidium" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Vanda coerulea", + "common_names": [ + "Blue Orchid", + "Autumn Lady's Tresses" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Vanda coerulescens", + "common_names": [ + "Bluish Vanda" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Vanda cristata", + "common_names": [ + "Crested Vanda" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Vanda dearei", + "common_names": [ + "Deare's Vanda" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Vanda denisoniana", + "common_names": [ + "Denison's Vanda" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Vanda flabellata", + "common_names": [ + "Fan-shaped Vanda" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Vanda hastifera", + "common_names": [ + "Spear-bearing Vanda" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Vanda hindsii", + "common_names": [ + "Hinds' Vanda" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Vanda lamellata", + "common_names": [ + "Layered Vanda" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Vanda luzonica", + "common_names": [ + "Luzon Vanda" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Vanda merrillii", + "common_names": [ + "Merrill's Vanda" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Vanda sanderiana", + "common_names": [ + "Waling-waling", + "Sander's Vanda" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Vanda spathulata", + "common_names": [ + "Spatula-leaved Vanda" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Vanda stangeana", + "common_names": [ + "Stange's Vanda" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Vanda tessellata", + "common_names": [ + "Checkered Vanda" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Vanda teres", + "common_names": [ + "Pencil Vanda", + "Terete-leaved Vanda" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Vanda tricolor", + "common_names": [ + "Three-colored Vanda", + "Soft-leaved Vanda" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Paphiopedilum acmodontum", + "common_names": [ + "Sharp-toothed Paphiopedilum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Paphiopedilum appletonianum", + "common_names": [ + "Appleton's Paphiopedilum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Paphiopedilum armeniacum", + "common_names": [ + "Armenian Paphiopedilum", + "Golden Slipper Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Paphiopedilum barbatum", + "common_names": [ + "Bearded Paphiopedilum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Paphiopedilum bellatulum", + "common_names": [ + "Beautiful Paphiopedilum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Paphiopedilum callosum", + "common_names": [ + "Hardened Paphiopedilum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Paphiopedilum charlesworthii", + "common_names": [ + "Charlesworth's Paphiopedilum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Paphiopedilum concolor", + "common_names": [ + "One-colored Paphiopedilum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Paphiopedilum delenatii", + "common_names": [ + "Delenat's Paphiopedilum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Paphiopedilum fairrieanum", + "common_names": [ + "Fairrie's Paphiopedilum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Paphiopedilum glanduliferum", + "common_names": [ + "Gland-bearing Paphiopedilum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Paphiopedilum glaucophyllum", + "common_names": [ + "Blue-gray Leaved Paphiopedilum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Paphiopedilum haynaldianum", + "common_names": [ + "Haynald's Paphiopedilum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Paphiopedilum henryanum", + "common_names": [ + "Henry's Paphiopedilum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Paphiopedilum hirsutissimum", + "common_names": [ + "Very Hairy Paphiopedilum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Paphiopedilum insigne", + "common_names": [ + "Remarkable Paphiopedilum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Paphiopedilum malipoense", + "common_names": [ + "Malipo Paphiopedilum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Paphiopedilum micranthum", + "common_names": [ + "Small-flowered Paphiopedilum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Paphiopedilum niveum", + "common_names": [ + "Snow White Paphiopedilum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Paphiopedilum parishii", + "common_names": [ + "Parish's Paphiopedilum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Paphiopedilum primulinum", + "common_names": [ + "Primrose Paphiopedilum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Paphiopedilum rothschildianum", + "common_names": [ + "Rothschild's Slipper Orchid", + "Gold of Kinabalu Orchid" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Paphiopedilum sanderianum", + "common_names": [ + "Sander's Paphiopedilum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Paphiopedilum spicerianum", + "common_names": [ + "Spicer's Paphiopedilum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Paphiopedilum sukhakulii", + "common_names": [ + "Sukhakul's Paphiopedilum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Paphiopedilum venustum", + "common_names": [ + "Lovely Paphiopedilum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Paphiopedilum villosum", + "common_names": [ + "Shaggy Paphiopedilum" + ], + "family": "Orchidaceae", + "category": "Orchid" + }, + { + "scientific_name": "Aeschynanthus lobbianus", + "common_names": [ + "Lipstick Plant" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Aeschynanthus longicaulis", + "common_names": [ + "Long-stemmed Lipstick Plant" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Aeschynanthus marmoratus", + "common_names": [ + "Zebra Basket Vine" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Aeschynanthus pulcher", + "common_names": [ + "Royal Red Bugler" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Aeschynanthus speciosus", + "common_names": [ + "Showy Lipstick Plant" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Aeschynanthus 'Twister'", + "common_names": [ + "Twister Lipstick Plant" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Columnea arguta", + "common_names": [ + "Goldfish Plant" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Columnea gloriosa", + "common_names": [ + "Glorious Goldfish Plant" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Columnea hirta", + "common_names": [ + "Hairy Goldfish Plant" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Columnea microphylla", + "common_names": [ + "Small-leaved Goldfish Plant" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Columnea 'Carnival'", + "common_names": [ + "Carnival Goldfish Plant" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Episcia dianthiflora", + "common_names": [ + "Lace Flower Vine" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Episcia lilacina", + "common_names": [ + "Lilac Flame Violet" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Episcia reptans", + "common_names": [ + "Creeping Flame Violet" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Episcia 'Cleopatra'", + "common_names": [ + "Cleopatra Flame Violet" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Episcia 'Pink Acajou'", + "common_names": [ + "Pink Acajou Episcia" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Episcia 'Silver Skies'", + "common_names": [ + "Silver Skies Episcia" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Kohleria amabilis", + "common_names": [ + "Lovely Kohleria" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Kohleria bogotensis", + "common_names": [ + "Bogota Kohleria" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Kohleria eriantha", + "common_names": [ + "Woolly Kohleria" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Kohleria hirsuta", + "common_names": [ + "Hairy Kohleria" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Nematanthus wettsteinii", + "common_names": [ + "Goldfish Plant" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Petrocosmea flaccida", + "common_names": [ + "Floppy Petrocosmea" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Primulina dryas", + "common_names": [ + "Chirita", + "Vietnamese Violet" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Primulina tamiana", + "common_names": [ + "Vietnamese Violet" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Sinningia bullata", + "common_names": [ + "Emerald Forest Gloxinia" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Sinningia cardinalis", + "common_names": [ + "Cardinal Flower" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Sinningia leucotricha", + "common_names": [ + "Brazilian Edelweiss" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Sinningia pusilla", + "common_names": [ + "Miniature Sinningia" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Streptocarpus caulescens", + "common_names": [ + "Nodding Violet" + ], + "family": "Gesneriaceae", + "category": "Flowering Houseplant" + }, + { + "scientific_name": "Hedera algeriensis 'Gloire de Marengo'", + "common_names": [ + "Variegated Algerian Ivy" + ], + "family": "Araliaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Hedera colchica", + "common_names": [ + "Persian Ivy", + "Colchis Ivy" + ], + "family": "Araliaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Hedera colchica 'Dentata Variegata'", + "common_names": [ + "Variegated Persian Ivy" + ], + "family": "Araliaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Hedera colchica 'Sulphur Heart'", + "common_names": [ + "Sulphur Heart Ivy", + "Paddy's Pride" + ], + "family": "Araliaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Hedera helix 'Adam'", + "common_names": [ + "Adam English Ivy" + ], + "family": "Araliaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Hedera helix 'Baltica'", + "common_names": [ + "Baltic Ivy" + ], + "family": "Araliaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Hedera helix 'Buttercup'", + "common_names": [ + "Buttercup Ivy" + ], + "family": "Araliaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Hedera helix 'Calico'", + "common_names": [ + "Calico English Ivy" + ], + "family": "Araliaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Hedera helix 'California'", + "common_names": [ + "California Ivy" + ], + "family": "Araliaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Hedera helix 'Cascade'", + "common_names": [ + "Cascade Ivy" + ], + "family": "Araliaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Hedera helix 'Curly Locks'", + "common_names": [ + "Curly Locks Ivy" + ], + "family": "Araliaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Hedera helix 'Gold Heart'", + "common_names": [ + "Gold Heart Ivy" + ], + "family": "Araliaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Hedera helix 'Green Ripple'", + "common_names": [ + "Green Ripple Ivy" + ], + "family": "Araliaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Hedera helix 'Manda's Crested'", + "common_names": [ + "Manda's Crested Ivy" + ], + "family": "Araliaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Hedera helix 'Parsley Crested'", + "common_names": [ + "Parsley Crested Ivy" + ], + "family": "Araliaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Hedera helix 'Shamrock'", + "common_names": [ + "Shamrock Ivy" + ], + "family": "Araliaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Hedera helix 'Spetchley'", + "common_names": [ + "Spetchley Ivy", + "Miniature Ivy" + ], + "family": "Araliaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Hedera helix 'Thorndale'", + "common_names": [ + "Thorndale Ivy" + ], + "family": "Araliaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Hedera hibernica", + "common_names": [ + "Atlantic Ivy", + "Irish Ivy" + ], + "family": "Araliaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Cissus alata", + "common_names": [ + "Winged Grape Ivy" + ], + "family": "Vitaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Cissus amazonica", + "common_names": [ + "Amazon Grape Ivy" + ], + "family": "Vitaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Cissus antarctica", + "common_names": [ + "Kangaroo Vine" + ], + "family": "Vitaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Cissus discolor", + "common_names": [ + "Rex Begonia Vine" + ], + "family": "Vitaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Cissus quadrangularis", + "common_names": [ + "Veldt Grape", + "Devil's Backbone" + ], + "family": "Vitaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Cissus rhombifolia", + "common_names": [ + "Grape Ivy", + "Venezuela Treebine" + ], + "family": "Vitaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Cissus rhombifolia 'Ellen Danica'", + "common_names": [ + "Oak Leaf Ivy" + ], + "family": "Vitaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Cissus rotundifolia", + "common_names": [ + "Arabian Wax Cissus", + "Peruvian Grape Ivy" + ], + "family": "Vitaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Cissus striata", + "common_names": [ + "Miniature Grape Ivy", + "Ivy of Uruguay" + ], + "family": "Vitaceae", + "category": "Trailing/Climbing" + }, + { + "scientific_name": "Schefflera actinophylla 'Amate'", + "common_names": [ + "Amate Umbrella Tree" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Schefflera arboricola 'Capella'", + "common_names": [ + "Capella Schefflera" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Schefflera arboricola 'Green Gold'", + "common_names": [ + "Green Gold Schefflera" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Schefflera arboricola 'Luseane'", + "common_names": [ + "Luseane Schefflera" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Schefflera arboricola 'Renate'", + "common_names": [ + "Renate Schefflera" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Schefflera arboricola 'Variegata'", + "common_names": [ + "Variegated Dwarf Umbrella Tree" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Schefflera delavayi", + "common_names": [ + "Delavay's Schefflera" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Schefflera pueckleri", + "common_names": [ + "Tuft Root Schefflera" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Schefflera taiwaniana", + "common_names": [ + "Taiwan Schefflera" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aspidistra attenuata", + "common_names": [ + "Narrow Cast Iron Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aspidistra caespitosa", + "common_names": [ + "Tufted Cast Iron Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aspidistra elatior 'Asahi'", + "common_names": [ + "Asahi Cast Iron Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aspidistra elatior 'Lennon's Song'", + "common_names": [ + "Lennon's Song Cast Iron Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aspidistra elatior 'Okame'", + "common_names": [ + "Okame Cast Iron Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aspidistra lurida", + "common_names": [ + "Chinese Cast Iron Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aspidistra sichuanensis", + "common_names": [ + "Sichuan Cast Iron Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Aspidistra yingjiangensis", + "common_names": [ + "Yingjiang Cast Iron Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Cordyline australis 'Albertii'", + "common_names": [ + "Albertii Cabbage Tree" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Cordyline australis 'Pink Passion'", + "common_names": [ + "Pink Passion Cabbage Tree" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Cordyline australis 'Red Star'", + "common_names": [ + "Red Star Cordyline" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Cordyline australis 'Torbay Dazzler'", + "common_names": [ + "Torbay Dazzler Cordyline" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Cordyline banksii", + "common_names": [ + "Forest Cabbage Tree" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Cordyline fruticosa 'Candy Cane'", + "common_names": [ + "Candy Cane Ti Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Cordyline fruticosa 'Firebrand'", + "common_names": [ + "Firebrand Ti Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Cordyline fruticosa 'Florida'", + "common_names": [ + "Florida Ti Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Cordyline fruticosa 'Hilo Rainbow'", + "common_names": [ + "Hilo Rainbow Ti Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Cordyline fruticosa 'Kiwi'", + "common_names": [ + "Kiwi Ti Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Cordyline fruticosa 'Morning Sunshine'", + "common_names": [ + "Morning Sunshine Ti Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Cordyline fruticosa 'Rubra'", + "common_names": [ + "Red Dracaena", + "Red Ti Plant" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Cordyline petiolaris", + "common_names": [ + "Broad-leaved Palm Lily" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Cordyline stricta", + "common_names": [ + "Narrow-leaved Palm Lily", + "Slender Palm Lily" + ], + "family": "Asparagaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Neoregelia ampullacea", + "common_names": [ + "Flask-shaped Neoregelia" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Neoregelia carolinae 'Flandria'", + "common_names": [ + "Flandria Bromeliad" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Neoregelia carolinae 'Meyendorffii'", + "common_names": [ + "Meyendorff's Bromeliad" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Neoregelia carolinae 'Tricolor'", + "common_names": [ + "Tricolor Blushing Bromeliad" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Neoregelia chlorosticta", + "common_names": [ + "Green-spotted Neoregelia" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Neoregelia concentrica", + "common_names": [ + "Concentric Neoregelia" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Neoregelia cruenta", + "common_names": [ + "Blood Red Neoregelia" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Neoregelia farinosa", + "common_names": [ + "Mealy Neoregelia" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Neoregelia fireball", + "common_names": [ + "Fireball Bromeliad" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Neoregelia johannis", + "common_names": [ + "Johann's Neoregelia" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Neoregelia laevis", + "common_names": [ + "Smooth Neoregelia" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Neoregelia leprosa", + "common_names": [ + "Scaly Neoregelia" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Neoregelia marmorata", + "common_names": [ + "Marble Neoregelia" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Neoregelia olens", + "common_names": [ + "Fragrant Neoregelia" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Neoregelia pendula", + "common_names": [ + "Hanging Neoregelia" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Neoregelia sarmentosa", + "common_names": [ + "Stoloniferous Neoregelia" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Neoregelia tigrina", + "common_names": [ + "Tiger Neoregelia" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Guzmania conifera", + "common_names": [ + "Cone-bearing Guzmania" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Guzmania lingulata 'Empire'", + "common_names": [ + "Empire Scarlet Star" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Guzmania lingulata 'Luna'", + "common_names": [ + "Luna Scarlet Star" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Guzmania lingulata 'Minor'", + "common_names": [ + "Dwarf Scarlet Star" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Guzmania monostachia", + "common_names": [ + "West Indian Tufted Airplant" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Guzmania musaica", + "common_names": [ + "Mosaic Vase" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Guzmania nicaraguensis", + "common_names": [ + "Nicaraguan Guzmania" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Guzmania sanguinea", + "common_names": [ + "Red Guzmania" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Guzmania 'Amaranth'", + "common_names": [ + "Amaranth Guzmania" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Guzmania 'Cherry'", + "common_names": [ + "Cherry Guzmania" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Guzmania 'Rana'", + "common_names": [ + "Rana Guzmania" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Vriesea carinata", + "common_names": [ + "Lobster Claw" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Vriesea erythrodactylon", + "common_names": [ + "Red-fingered Vriesea" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Vriesea fenestralis", + "common_names": [ + "Netted Vriesea" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Vriesea fosteriana", + "common_names": [ + "Foster's Vriesea" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Vriesea gigantea", + "common_names": [ + "Giant Vriesea" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Vriesea guttata", + "common_names": [ + "Spotted Vriesea" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Vriesea incurvata", + "common_names": [ + "Curved Vriesea" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Vriesea ospinae", + "common_names": [ + "Ospina's Vriesea" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Vriesea platynema", + "common_names": [ + "Broad-petal Vriesea" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Vriesea psittacina", + "common_names": [ + "Parrot Feather Vriesea" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Vriesea saundersii", + "common_names": [ + "Saunders' Vriesea" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Vriesea 'Charlotte'", + "common_names": [ + "Charlotte Vriesea" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Cryptanthus acaulis", + "common_names": [ + "Green Earth Star" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Cryptanthus beuckeri", + "common_names": [ + "Beucker's Cryptanthus" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Cryptanthus bivittatus 'Pink Starlight'", + "common_names": [ + "Pink Starlight Earth Star" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Cryptanthus bivittatus 'Ruby'", + "common_names": [ + "Ruby Earth Star" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Cryptanthus bromelioides", + "common_names": [ + "Bromeliad-like Cryptanthus" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Cryptanthus fosterianus", + "common_names": [ + "Foster's Cryptanthus", + "Pheasant Leaf" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Cryptanthus lacerdae", + "common_names": [ + "Silver Star" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Cryptanthus 'Black Mystic'", + "common_names": [ + "Black Mystic Earth Star" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Cryptanthus 'Elaine'", + "common_names": [ + "Elaine Earth Star" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Cryptanthus 'Red Star'", + "common_names": [ + "Red Star Cryptanthus" + ], + "family": "Bromeliaceae", + "category": "Bromeliad" + }, + { + "scientific_name": "Polyscias balfouriana", + "common_names": [ + "Balfour Aralia", + "Dinner Plate Aralia" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Polyscias balfouriana 'Marginata'", + "common_names": [ + "Variegated Balfour Aralia" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Polyscias crispata", + "common_names": [ + "Crispy Aralia", + "Chicken Gizzard" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Polyscias filicifolia", + "common_names": [ + "Fern-leaf Aralia" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Polyscias fruticosa 'Elegans'", + "common_names": [ + "Elegant Ming Aralia" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Polyscias guilfoylei 'Quinquefolia'", + "common_names": [ + "Five-leaved Aralia" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Polyscias guilfoylei 'Victoriae'", + "common_names": [ + "Victoria Aralia", + "Lace Leaf Aralia" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Polyscias paniculata", + "common_names": [ + "Panicled Aralia" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Polyscias scutellaria 'Fabian'", + "common_names": [ + "Fabian Aralia" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Fatsia japonica 'Annelise'", + "common_names": [ + "Annelise Japanese Aralia" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Fatsia japonica 'Moseri'", + "common_names": [ + "Moser's Japanese Aralia" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Fatsia japonica 'Spider's Web'", + "common_names": [ + "Spider's Web Japanese Aralia" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Fatsia japonica 'Variegata'", + "common_names": [ + "Variegated Japanese Aralia" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Fatsia polycarpa", + "common_names": [ + "Taiwan Fatsia" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "x Fatshedera lizei 'Angyo Star'", + "common_names": [ + "Angyo Star Tree Ivy" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "x Fatshedera lizei 'Variegata'", + "common_names": [ + "Variegated Tree Ivy" + ], + "family": "Araliaceae", + "category": "Tropical Foliage" + }, + { + "scientific_name": "Selaginella apoda", + "common_names": [ + "Meadow Spikemoss" + ], + "family": "Selaginellaceae", + "category": "Fern" + }, + { + "scientific_name": "Selaginella braunii", + "common_names": [ + "Braun's Spikemoss", + "Arborvitae Fern" + ], + "family": "Selaginellaceae", + "category": "Fern" + }, + { + "scientific_name": "Selaginella erythropus", + "common_names": [ + "Ruby Red Spikemoss" + ], + "family": "Selaginellaceae", + "category": "Fern" + }, + { + "scientific_name": "Selaginella involvens", + "common_names": [ + "Curly Spikemoss" + ], + "family": "Selaginellaceae", + "category": "Fern" + }, + { + "scientific_name": "Selaginella kraussiana 'Aurea'", + "common_names": [ + "Golden Spikemoss" + ], + "family": "Selaginellaceae", + "category": "Fern" + }, + { + "scientific_name": "Selaginella kraussiana 'Brownii'", + "common_names": [ + "Cushion Moss" + ], + "family": "Selaginellaceae", + "category": "Fern" + }, + { + "scientific_name": "Selaginella kraussiana 'Gold Tips'", + "common_names": [ + "Gold Tips Spikemoss" + ], + "family": "Selaginellaceae", + "category": "Fern" + }, + { + "scientific_name": "Selaginella lepidophylla", + "common_names": [ + "Resurrection Plant", + "Rose of Jericho" + ], + "family": "Selaginellaceae", + "category": "Fern" + }, + { + "scientific_name": "Selaginella martensii 'Jori'", + "common_names": [ + "Jori Frosty Fern" + ], + "family": "Selaginellaceae", + "category": "Fern" + }, + { + "scientific_name": "Selaginella martensii 'Watsoniana'", + "common_names": [ + "Frosty Fern", + "Variegated Spikemoss" + ], + "family": "Selaginellaceae", + "category": "Fern" + }, + { + "scientific_name": "Selaginella pallescens", + "common_names": [ + "Pale Spikemoss" + ], + "family": "Selaginellaceae", + "category": "Fern" + }, + { + "scientific_name": "Selaginella plana", + "common_names": [ + "Asian Spikemoss" + ], + "family": "Selaginellaceae", + "category": "Fern" + }, + { + "scientific_name": "Selaginella tamariscina", + "common_names": [ + "Tamarisk Spikemoss" + ], + "family": "Selaginellaceae", + "category": "Fern" + }, + { + "scientific_name": "Selaginella uncinata", + "common_names": [ + "Peacock Moss", + "Blue Spikemoss", + "Rainbow Fern" + ], + "family": "Selaginellaceae", + "category": "Fern" + }, + { + "scientific_name": "Selaginella willdenowii", + "common_names": [ + "Willdenow's Spikemoss", + "Peacock Fern" + ], + "family": "Selaginellaceae", + "category": "Fern" + }, + { + "scientific_name": "Platycerium alcicorne", + "common_names": [ + "Elkhorn Fern" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Platycerium andinum", + "common_names": [ + "Andean Staghorn Fern" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Platycerium coronarium", + "common_names": [ + "Crown Staghorn Fern" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Platycerium elephantotis", + "common_names": [ + "Angola Staghorn Fern", + "Elephant Ear Fern" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Platycerium grande", + "common_names": [ + "Grand Staghorn Fern" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Platycerium hillii", + "common_names": [ + "Hill's Staghorn Fern" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Platycerium holttumii", + "common_names": [ + "Holttum's Staghorn Fern" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Platycerium madagascariense", + "common_names": [ + "Madagascar Staghorn Fern" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Platycerium quadridichotomum", + "common_names": [ + "Four-forked Staghorn Fern" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Platycerium ridleyi", + "common_names": [ + "Ridley's Staghorn Fern" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Platycerium stemaria", + "common_names": [ + "Triangle Staghorn Fern" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Platycerium veitchii", + "common_names": [ + "Silver Elkhorn", + "Veitch's Staghorn Fern" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Platycerium wallichii", + "common_names": [ + "Wallich's Staghorn Fern" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Platycerium wandae", + "common_names": [ + "Queen Staghorn Fern" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Platycerium willinckii", + "common_names": [ + "Java Staghorn Fern", + "Sumatra Staghorn" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Davallia denticulata", + "common_names": [ + "Toothed Davallia" + ], + "family": "Davalliaceae", + "category": "Fern" + }, + { + "scientific_name": "Davallia mariesii", + "common_names": [ + "Squirrel's Foot Fern", + "Ball Fern" + ], + "family": "Davalliaceae", + "category": "Fern" + }, + { + "scientific_name": "Davallia parvula", + "common_names": [ + "Small Rabbit's Foot Fern" + ], + "family": "Davalliaceae", + "category": "Fern" + }, + { + "scientific_name": "Davallia solida", + "common_names": [ + "Giant Rabbit's Foot Fern" + ], + "family": "Davalliaceae", + "category": "Fern" + }, + { + "scientific_name": "Davallia trichomanoides", + "common_names": [ + "Black Rabbit's Foot Fern" + ], + "family": "Davalliaceae", + "category": "Fern" + }, + { + "scientific_name": "Davallia tyermannii", + "common_names": [ + "Bear's Paw Fern", + "Tyerman's Fern" + ], + "family": "Davalliaceae", + "category": "Fern" + }, + { + "scientific_name": "Pteris argyraea", + "common_names": [ + "Silver Brake Fern" + ], + "family": "Pteridaceae", + "category": "Fern" + }, + { + "scientific_name": "Pteris cretica 'Albolineata'", + "common_names": [ + "White-lined Brake Fern" + ], + "family": "Pteridaceae", + "category": "Fern" + }, + { + "scientific_name": "Pteris cretica 'Evergemiensis'", + "common_names": [ + "Silver Ribbon Fern" + ], + "family": "Pteridaceae", + "category": "Fern" + }, + { + "scientific_name": "Pteris cretica 'Mayi'", + "common_names": [ + "May's Brake Fern" + ], + "family": "Pteridaceae", + "category": "Fern" + }, + { + "scientific_name": "Pteris cretica 'Parkeri'", + "common_names": [ + "Parker's Brake Fern" + ], + "family": "Pteridaceae", + "category": "Fern" + }, + { + "scientific_name": "Pteris cretica 'Rowerii'", + "common_names": [ + "Roweri's Brake Fern" + ], + "family": "Pteridaceae", + "category": "Fern" + }, + { + "scientific_name": "Pteris cretica 'Wimsettii'", + "common_names": [ + "Wimsett's Brake Fern" + ], + "family": "Pteridaceae", + "category": "Fern" + }, + { + "scientific_name": "Pteris ensiformis 'Evergemiensis'", + "common_names": [ + "Silver Lace Fern" + ], + "family": "Pteridaceae", + "category": "Fern" + }, + { + "scientific_name": "Pteris ensiformis 'Victoriae'", + "common_names": [ + "Victoria Brake Fern" + ], + "family": "Pteridaceae", + "category": "Fern" + }, + { + "scientific_name": "Pteris multifida", + "common_names": [ + "Spider Brake Fern", + "Huguenot Fern" + ], + "family": "Pteridaceae", + "category": "Fern" + }, + { + "scientific_name": "Pteris nipponica", + "common_names": [ + "Japanese Brake Fern" + ], + "family": "Pteridaceae", + "category": "Fern" + }, + { + "scientific_name": "Pteris quadriaurita", + "common_names": [ + "Four-eared Brake Fern" + ], + "family": "Pteridaceae", + "category": "Fern" + }, + { + "scientific_name": "Pteris tremula", + "common_names": [ + "Trembling Brake Fern", + "Australian Brake" + ], + "family": "Pteridaceae", + "category": "Fern" + }, + { + "scientific_name": "Pteris umbrosa", + "common_names": [ + "Jungle Brake Fern" + ], + "family": "Pteridaceae", + "category": "Fern" + }, + { + "scientific_name": "Pteris vittata", + "common_names": [ + "Chinese Brake Fern", + "Ladder Brake" + ], + "family": "Pteridaceae", + "category": "Fern" + }, + { + "scientific_name": "Phlebodium aureum 'Davana'", + "common_names": [ + "Davana Blue Fern" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Phlebodium aureum 'Mandaianum'", + "common_names": [ + "Crested Blue Fern" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Phlebodium pseudoaureum", + "common_names": [ + "False Golden Polypody" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Polypodium cambricum", + "common_names": [ + "Southern Polypody", + "Welsh Polypody" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Polypodium glycyrrhiza", + "common_names": [ + "Licorice Fern" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Polypodium polypodioides", + "common_names": [ + "Resurrection Fern" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Polypodium vulgare", + "common_names": [ + "Common Polypody" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Nephrolepis biserrata", + "common_names": [ + "Giant Sword Fern", + "Macho Fern" + ], + "family": "Nephrolepidaceae", + "category": "Fern" + }, + { + "scientific_name": "Nephrolepis biserrata 'Macho'", + "common_names": [ + "Macho Fern" + ], + "family": "Nephrolepidaceae", + "category": "Fern" + }, + { + "scientific_name": "Nephrolepis cordifolia", + "common_names": [ + "Sword Fern", + "Tuberous Sword Fern" + ], + "family": "Nephrolepidaceae", + "category": "Fern" + }, + { + "scientific_name": "Nephrolepis cordifolia 'Duffii'", + "common_names": [ + "Duffy Fern", + "Lemon Button Fern" + ], + "family": "Nephrolepidaceae", + "category": "Fern" + }, + { + "scientific_name": "Nephrolepis cordifolia 'Lemon Button'", + "common_names": [ + "Lemon Button Fern" + ], + "family": "Nephrolepidaceae", + "category": "Fern" + }, + { + "scientific_name": "Nephrolepis exaltata 'Compacta'", + "common_names": [ + "Compact Boston Fern" + ], + "family": "Nephrolepidaceae", + "category": "Fern" + }, + { + "scientific_name": "Nephrolepis exaltata 'Dallas'", + "common_names": [ + "Dallas Fern" + ], + "family": "Nephrolepidaceae", + "category": "Fern" + }, + { + "scientific_name": "Nephrolepis exaltata 'Green Fantasy'", + "common_names": [ + "Green Fantasy Fern" + ], + "family": "Nephrolepidaceae", + "category": "Fern" + }, + { + "scientific_name": "Nephrolepis exaltata 'Kimberly Queen'", + "common_names": [ + "Kimberly Queen Fern" + ], + "family": "Nephrolepidaceae", + "category": "Fern" + }, + { + "scientific_name": "Nephrolepis exaltata 'Petticoat'", + "common_names": [ + "Petticoat Fern" + ], + "family": "Nephrolepidaceae", + "category": "Fern" + }, + { + "scientific_name": "Nephrolepis exaltata 'Rita's Gold'", + "common_names": [ + "Rita's Gold Fern", + "Golden Boston Fern" + ], + "family": "Nephrolepidaceae", + "category": "Fern" + }, + { + "scientific_name": "Nephrolepis exaltata 'Roosevelt'", + "common_names": [ + "Roosevelt Fern" + ], + "family": "Nephrolepidaceae", + "category": "Fern" + }, + { + "scientific_name": "Nephrolepis exaltata 'Whitmanii'", + "common_names": [ + "Fluffy Ruffles Fern", + "Whitman Fern" + ], + "family": "Nephrolepidaceae", + "category": "Fern" + }, + { + "scientific_name": "Nephrolepis falcata", + "common_names": [ + "Fishtail Fern" + ], + "family": "Nephrolepidaceae", + "category": "Fern" + }, + { + "scientific_name": "Microsorum musifolium", + "common_names": [ + "Crocodile Fern" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Microsorum punctatum", + "common_names": [ + "Climbing Bird's Nest Fern" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Microsorum punctatum 'Grandiceps'", + "common_names": [ + "Fishtail Fern" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Microsorum scolopendria", + "common_names": [ + "Wart Fern", + "Monarch Fern" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Microsorum thailandicum", + "common_names": [ + "Thai Blue Fern", + "Blue Fern" + ], + "family": "Polypodiaceae", + "category": "Fern" + }, + { + "scientific_name": "Dicksonia antarctica", + "common_names": [ + "Soft Tree Fern", + "Man Fern" + ], + "family": "Dicksoniaceae", + "category": "Fern" + }, + { + "scientific_name": "Dicksonia fibrosa", + "common_names": [ + "Wheki-ponga", + "Golden Tree Fern" + ], + "family": "Dicksoniaceae", + "category": "Fern" + }, + { + "scientific_name": "Dicksonia sellowiana", + "common_names": [ + "Brazilian Tree Fern" + ], + "family": "Dicksoniaceae", + "category": "Fern" + }, + { + "scientific_name": "Dicksonia squarrosa", + "common_names": [ + "Rough Tree Fern", + "Wheki" + ], + "family": "Dicksoniaceae", + "category": "Fern" + }, + { + "scientific_name": "Lavandula dentata", + "common_names": [ + "French Lavender", + "Fringed Lavender" + ], + "family": "Lamiaceae", + "category": "Herb" + }, + { + "scientific_name": "Lavandula stoechas", + "common_names": [ + "Spanish Lavender", + "Butterfly Lavender" + ], + "family": "Lamiaceae", + "category": "Herb" + }, + { + "scientific_name": "Mentha piperita", + "common_names": [ + "Peppermint" + ], + "family": "Lamiaceae", + "category": "Herb" + }, + { + "scientific_name": "Mentha x piperita 'Chocolate'", + "common_names": [ + "Chocolate Mint" + ], + "family": "Lamiaceae", + "category": "Herb" + }, + { + "scientific_name": "Ocimum basilicum 'Purple Ruffles'", + "common_names": [ + "Purple Ruffles Basil" + ], + "family": "Lamiaceae", + "category": "Herb" + }, + { + "scientific_name": "Ocimum basilicum var. citriodorum", + "common_names": [ + "Lemon Basil" + ], + "family": "Lamiaceae", + "category": "Herb" + }, + { + "scientific_name": "Ocimum tenuiflorum", + "common_names": [ + "Holy Basil", + "Tulsi" + ], + "family": "Lamiaceae", + "category": "Herb" + }, + { + "scientific_name": "Thymus citriodorus", + "common_names": [ + "Lemon Thyme" + ], + "family": "Lamiaceae", + "category": "Herb" + } + ] +} \ No newline at end of file diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..3de9f81 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,58 @@ +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + upstream backend { + server backend:8000; + } + + upstream frontend { + server frontend:3000; + } + + server { + listen 80; + server_name localhost; + + # API routes + location /api { + proxy_pass http://backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Increase timeouts for slow API calls + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + # Health check + location /health { + proxy_pass http://backend; + } + + # WebSocket support for hot reload + location /ws { + proxy_pass http://frontend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + # Frontend + location / { + proxy_pass http://frontend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + } +}