Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,870 changes: 386 additions & 1,484 deletions docs/developer-guide.md

Large diffs are not rendered by default.

1,222 changes: 540 additions & 682 deletions docs/user-guide.md

Large diffs are not rendered by default.

180 changes: 133 additions & 47 deletions src/neuview/services/index_generator_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
README documentation, help pages, and landing pages.
"""

import logging
import json
from pathlib import Path
import logging
from datetime import datetime
from typing import List, Dict, Any, Optional
from pathlib import Path
from typing import Any, Dict, List, Optional

logger = logging.getLogger(__name__)

Expand All @@ -23,43 +23,73 @@ def __init__(self, page_generator):
async def generate_neuron_search_js(
self, output_dir: Path, neuron_data: List[Dict[str, Any]], generation_time
) -> Optional[str]:
"""Generate the neuron-search.js file with embedded neuron types data."""
"""
Generate neuron search files: neuron-search.js, neurons.json, and neurons.js fallback.

Returns:
Path to the generated neuron-search.js file, or None if generation failed
"""
# Prepare neuron types data for JavaScript
neuron_types_for_js = []

for neuron in neuron_data:
# Helper function to remove "types/" prefix from URLs
def strip_types_prefix(url):
if url and url.startswith("types/"):
return url[6:] # Remove "types/" prefix
return url

# Create an entry with the neuron name and available URLs
neuron_entry = {
"name": neuron["name"],
"urls": {},
"synonyms": neuron.get("synonyms", ""),
"flywire_types": neuron.get("flywire_types", ""),
}

# Add available URLs for this neuron type
# Add available URLs for this neuron type (without "types/" prefix)
if neuron.get("combined_url") or neuron.get("both_url"):
combined_url = neuron.get("combined_url")
neuron_entry["urls"]["combined"] = combined_url
combined_url = neuron.get("combined_url") or neuron.get("both_url")
neuron_entry["urls"]["combined"] = strip_types_prefix(combined_url)
if neuron["left_url"]:
neuron_entry["urls"]["left"] = neuron["left_url"]
neuron_entry["urls"]["left"] = strip_types_prefix(neuron["left_url"])
if neuron["right_url"]:
neuron_entry["urls"]["right"] = neuron["right_url"]
neuron_entry["urls"]["right"] = strip_types_prefix(neuron["right_url"])
if neuron["middle_url"]:
neuron_entry["urls"]["middle"] = neuron["middle_url"]

# Set primary URL (prefer 'combined' if available, otherwise first available)
if neuron.get("combined_url") or neuron.get("both_url"):
neuron_entry["primary_url"] = neuron.get("combined_url") or neuron.get(
"both_url"
neuron_entry["urls"]["middle"] = strip_types_prefix(
neuron["middle_url"]
)
elif neuron["left_url"]:
neuron_entry["primary_url"] = neuron["left_url"]
elif neuron["right_url"]:
neuron_entry["primary_url"] = neuron["right_url"]
elif neuron["middle_url"]:
neuron_entry["primary_url"] = neuron["middle_url"]
else:
neuron_entry["primary_url"] = f"{neuron['name']}.html" # fallback

# Build types dictionary with flywire and synonyms
types_dict = {}

# Add FlyWire types
if neuron.get("flywire_types"):
flywire_value = neuron.get("flywire_types")
# Split by comma if multiple values
if isinstance(flywire_value, str):
flywire_list = [
t.strip() for t in flywire_value.split(",") if t.strip()
]
if flywire_list:
types_dict["flywire"] = flywire_list
elif isinstance(flywire_value, list):
types_dict["flywire"] = flywire_value

# Add synonyms
if neuron.get("synonyms"):
synonyms_value = neuron.get("synonyms")
# Parse synonyms (format: "Author Year: name; Author Year: name")
if isinstance(synonyms_value, str):
synonym_list = [
s.strip() for s in synonyms_value.split(";") if s.strip()
]
if synonym_list:
types_dict["synonyms"] = synonym_list
elif isinstance(synonyms_value, list):
types_dict["synonyms"] = synonyms_value

# Only add types field if it has content
if types_dict:
neuron_entry["types"] = types_dict

neuron_types_for_js.append(neuron_entry)

Expand All @@ -69,40 +99,96 @@ async def generate_neuron_search_js(
# Extract just the names for the simple search functionality
neuron_names = [neuron["name"] for neuron in neuron_types_for_js]

# Prepare template data
js_template_data = {
"neuron_types_json": json.dumps(neuron_names, indent=2),
"neuron_types_data_json": json.dumps(neuron_types_for_js, indent=2),
"generation_timestamp": generation_time.strftime("%Y-%m-%d %H:%M:%S")
# Prepare timestamp
timestamp = (
generation_time.strftime("%Y-%m-%d %H:%M:%S")
if generation_time
else datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"neuron_types": neuron_types_for_js,
else datetime.now().strftime("%Y-%m-%d %H:%M:%S")
)

# Prepare data structure for JSON and JS fallback
data_structure = {
"names": neuron_names,
"neurons": neuron_types_for_js,
"metadata": {
"generated": timestamp,
"total_types": len(neuron_names),
"version": "2.0",
},
}

# Load and render the neuron-search.js template
js_template = self.page_generator.env.get_template(
"static/js/neuron-search.js.template.jinja"
)
js_content = js_template.render(js_template_data)
# Ensure data directory exists
data_dir = output_dir / "data"
data_dir.mkdir(parents=True, exist_ok=True)

# 1. Generate neurons.json (for external services & web servers)
try:
json_path = data_dir / "neurons.json"
json_path.write_text(
json.dumps(data_structure, separators=(",", ":"), ensure_ascii=False),
encoding="utf-8",
)
logger.info(f"Generated neurons.json at {json_path}")
except Exception as e:
logger.error(f"Failed to generate neurons.json: {e}")

# 2. Generate neurons.js (fallback for CORS-restricted environments)
try:
js_fallback_template = self.page_generator.env.get_template(
"data/neurons.js.jinja"
)
js_fallback_content = js_fallback_template.render(
{
"neuron_data": data_structure,
"neuron_data_json": json.dumps(
data_structure, indent=2, ensure_ascii=False
),
"generation_timestamp": timestamp,
}
)

# Ensure static/js directory exists
js_dir = output_dir / "static" / "js"
js_dir.mkdir(parents=True, exist_ok=True)
js_fallback_path = data_dir / "neurons.js"
js_fallback_path.write_text(js_fallback_content, encoding="utf-8")
logger.info(f"Generated neurons.js fallback at {js_fallback_path}")
except Exception as e:
logger.error(f"Failed to generate neurons.js: {e}")

# Write the neuron-search.js file
js_path = js_dir / "neuron-search.js"
js_path.write_text(js_content, encoding="utf-8")
return str(js_path)
# 3. Generate neuron-search.js (search logic only, no embedded data)
try:
# Prepare template data
js_template_data = {
"neuron_types_json": json.dumps(neuron_names, indent=2),
"neuron_types_data_json": json.dumps(neuron_types_for_js, indent=2),
"generation_timestamp": timestamp,
"neuron_types": neuron_types_for_js,
}

# Load and render the neuron-search.js template
js_template = self.page_generator.env.get_template(
"static/js/neuron-search.js.jinja"
)
js_content = js_template.render(js_template_data)

# Ensure static/js directory exists
js_dir = output_dir / "static" / "js"
js_dir.mkdir(parents=True, exist_ok=True)

# Write the neuron-search.js file
js_path = js_dir / "neuron-search.js"
js_path.write_text(js_content, encoding="utf-8")
logger.info(f"Generated neuron-search.js at {js_path}")
return str(js_path)
except Exception as e:
logger.error(f"Failed to generate neuron-search.js: {e}")
return None

async def generate_readme(
self, output_dir: Path, template_data: Dict[str, Any]
) -> Optional[str]:
"""Generate README.md documentation for the generated website."""
try:
# Load the README template
readme_template = self.page_generator.env.get_template(
"README_template.md.jinja"
)
readme_template = self.page_generator.env.get_template("README.md.jinja")
readme_content = readme_template.render(template_data)

# Write the README.md file
Expand Down
10 changes: 6 additions & 4 deletions src/neuview/services/layer_analysis_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@

import logging
import re
from typing import Any, Dict, List, Optional, Tuple

import pandas as pd
from typing import Dict, Any, List, Optional, Tuple

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -42,7 +43,7 @@ def analyze_layer_roi_data(
Args:
roi_counts_df: DataFrame with ROI count data
neurons_df: DataFrame with neuron data
soma_side: Side of soma (left/right)
soma_side: Side of soma ('left', 'right', 'middle', or 'combined')
neuron_type: Name of the neuron type
connector: Database connector for additional queries

Expand Down Expand Up @@ -143,13 +144,14 @@ def _filter_roi_data_by_optic_lobe_side(
"""Filter ROI data to include only ROIs from the ipsilateral optic lobe
side to the soma side. Use the ipsilateral side to ensure that the layer
table data matches the hexagonal eyemap plots."""
if soma_side == "combined":
if soma_side in ["combined", "middle"]:
# For combined and middle soma sides, include all layer ROIs
layer_rois_filtered = layer_rois
else:
side_key = {"left": "_L_", "right": "_R_"}
if soma_side not in side_key:
raise ValueError(
f"Unsupported soma_side: {soma_side!r} (use 'left', 'right', or 'combined')"
f"Unsupported soma_side: {soma_side!r} (use 'left', 'right', 'middle', or 'combined')"
)
pattern = side_key[soma_side]
mask = (
Expand Down
9 changes: 5 additions & 4 deletions src/neuview/services/neuron_search_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
neuron type data for client-side search functionality.
"""

from pathlib import Path
import json
import logging
from datetime import datetime
from typing import List, Optional, Dict, Any
from pathlib import Path
from typing import Any, Dict, List, Optional

from jinja2 import Environment

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -65,7 +66,7 @@ def generate_neuron_search_js(self, force_regenerate: bool = False) -> bool:
neuron_types = self._get_neuron_types()

# Load the template
template_path = "static/js/neuron-search.js.template.jinja"
template_path = "static/js/neuron-search.js.jinja"

if not self._template_exists(template_path):
logger.warning(f"Neuron search template not found: {template_path}")
Expand Down Expand Up @@ -263,7 +264,7 @@ def generate_with_custom_data(
context.update(template_vars)

# Load and render template
template_path = "static/js/neuron-search.js.template.jinja"
template_path = "static/js/neuron-search.js.jinja"

if not self._template_exists(template_path):
logger.error(f"Template not found: {template_path}")
Expand Down
46 changes: 24 additions & 22 deletions src/neuview/services/resource_manager_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
directories, and handling other file system operations.
"""

import shutil
import logging
import shutil
from pathlib import Path
from typing import Dict, Any, Optional, List
from typing import Any, Dict, List, Optional

from ..utils import get_project_root, get_static_dir, get_templates_dir
from .neuroglancer_js_service import NeuroglancerJSService
from ..utils import get_templates_dir, get_static_dir, get_project_root

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -196,30 +196,32 @@ def copy_static_files(self, mode: str = "check_exists") -> bool:
"Failed to generate neuroglancer JavaScript file - this is a critical error"
)
return False
else:

# Verify the generated file exists and contains expected content
if not generated_file.exists():
logger.error(
"Neuroglancer JavaScript file does not exist after generation"
)
return False

with open(generated_file, "r") as f:
content = f.read()
newline = "\n"
logger.debug(
"Neuroglancer JavaScript file already exists, skipping generation"
f"Generated file exists, size: {len(content)} chars, lines: {len(content.split(newline))}"
)

# Verify the file exists and contains expected content
if not generated_file.exists():
logger.error("Neuroglancer JavaScript file does not exist")
return False

with open(generated_file, "r") as f:
content = f.read()
newline = "\n"
logger.debug(
f"Generated file exists, size: {len(content)} chars, lines: {len(content.split(newline))}"
)
if "function initializeNeuroglancerLinks" not in content:
logger.error(
"Generated neuroglancer JavaScript file is missing required function 'initializeNeuroglancerLinks'"
)
return False

if "function initializeNeuroglancerLinks" not in content:
logger.error(
"Generated neuroglancer JavaScript file is missing required function 'initializeNeuroglancerLinks'"
logger.debug("✓ Neuroglancer JavaScript file generated successfully")
else:
logger.debug(
"Neuroglancer JavaScript file already exists, skipping generation"
)
return False

logger.debug("✓ Neuroglancer JavaScript file generated successfully")

# Copy other static assets (images, fonts, etc.) - Enhanced version
# Copy all directories and files not already handled
Expand Down
Loading