JSDoc Style Guide for Luxar Viewer

This guide ensures consistency across all TypeScript documentation in the luxar-viewer project.

General Principles

  1. Every exported function/class/interface must have JSDoc

  2. Use complete sentences with proper capitalization and punctuation

  3. Focus on WHY and HOW, not just WHAT (code shows what)

  4. Include examples for complex or frequently-used APIs

  5. Document error conditions with @throws

  6. Cross-reference related code with @see

File-Level Documentation

Every TypeScript file should start with a file-level comment:

/**
 * Module description (1-2 sentences).
 *
 * Additional context about the module's role, key concepts,
 * or important design decisions (2-3 sentences).
 *
 * Key components:
 * - ComponentName: Purpose and responsibility
 * - ComponentName: Purpose and responsibility
 *
 * @module package-name/module-name
 */

Example:

/**
 * Input handling system with context-aware key binding management.
 *
 * Provides a hierarchical input context system that allows different
 * parts of the UI to handle keyboard and mouse input independently
 * without conflicts. Uses event delegation for efficiency.
 *
 * Key components:
 * - InputHandler: Main input coordinator and event router
 * - InputContextManager: Context stack and priority management
 * - Navigation utilities: Dimension navigation helpers
 *
 * @module input/input-handler
 */

Function Documentation

Complete Template

/**
 * One-line summary of what the function does (imperative mood).
 *
 * Detailed description providing context, design rationale, or
 * important behavior notes (2-4 sentences). Explain edge cases,
 * performance characteristics, or when to use this vs alternatives.
 *
 * @param paramName - Description including type constraints, valid ranges,
 *                    units, and any important behavior. Use hyphens for multi-line.
 * @param optionalParam - Optional parameters should indicate default behavior
 *                        if not provided
 * @returns Description of return value, including type information,
 *          possible values, and what they signify
 * @throws {ErrorType} When and why this error is thrown (be specific)
 * @throws {AnotherError} Document all possible error conditions
 *
 * @example
 * ```typescript
 * // Basic usage with explanation
 * const result = functionName(arg1, arg2);
 * console.log(result); // Expected output
 * ```
 *
 * @example
 * ```typescript
 * // Advanced usage or error handling
 * try {
 *   const result = functionName(complexArg);
 * } catch (error) {
 *   console.error('Failed:', error);
 * }
 * ```
 *
 * @see {@link RelatedFunction} for related functionality
 * @see {@link ../package/README.md#section} for usage guide
 */

Minimal Acceptable (Simple Functions)

For simple functions with obvious behavior:

/**
 * One-line summary.
 *
 * @param param - Brief description
 * @returns Brief description
 */

Real Examples from Luxar

Example 1: Public Method with Complex Logic

/**
 * Navigate forward or backward in the specified dimension.
 *
 * Advances or retreats the slice position by the dimension's step size,
 * respecting min/max bounds. Emits 'dimensionChanged' event on success.
 * Frame-rate independent: step size determines travel distance.
 *
 * @param dimIndex - Zero-based index of dimension to navigate (0=X, 1=Y, etc.)
 *                   Must be < total dimensionality
 * @param direction - Navigation direction: 1 for forward, -1 for backward
 * @returns true if navigation succeeded, false if already at boundary
 *
 * @throws {Error} If dims not initialized (call initFromScene first)
 *
 * @example
 * ```typescript
 * // Navigate forward in time dimension (typically index 3 for 4D data)
 * const success = inputHandler.navigateDimension(3, 1);
 * if (!success) {
 *   console.log('Already at last time point');
 * }
 * ```
 *
 * @example
 * ```typescript
 * // Bind to arrow keys
 * document.addEventListener('keydown', (e) => {
 *   if (e.key === 'ArrowLeft') {
 *     inputHandler.navigateDimension(currentDim, -1);
 *   }
 * });
 * ```
 *
 * @see {@link NavigationManager.clampPosition} for clamping logic
 * @see {@link SceneDimsManager.getDims} for dimension metadata
 */
navigateDimension(dimIndex: number, direction: 1 | -1): boolean {
  // Implementation...
}

Example 2: Interface Documentation

/**
 * Configuration for dimension sliders UI component.
 *
 * Controls the appearance and behavior of the nD navigation sliders.
 * All visual properties use CSS units (px, rem, etc.).
 */
export interface SliderConfig {
  /**
   * Container element for slider UI.
   * Sliders will be appended as children to this element.
   */
  container: HTMLElement;

  /**
   * Initial dimensions to display.
   * If null, dimensions will be initialized from first loaded scene.
   * @default null
   */
  initialDims?: DimensionMetadata[] | null;

  /**
   * Slider track height in CSS units.
   * Affects click target size and visual prominence.
   * @default '4px'
   */
  trackHeight?: string;

  /**
   * Show numeric value labels next to sliders.
   * Labels display current slice position with dimension units.
   * @default true
   */
  showLabels?: boolean;
}

Example 3: Class Documentation

/**
 * Manages keyboard and mouse input with context-aware routing.
 *
 * Coordinates input across multiple UI contexts (main view, modal dialogs,
 * text input fields) using a priority-based context stack. Higher priority
 * contexts receive events first and can prevent propagation to lower contexts.
 *
 * Uses event delegation for efficiency - one global listener handles all
 * input and routes to appropriate contexts. This avoids performance issues
 * from adding/removing many individual listeners.
 *
 * Event Flow:
 * 1. Global listener captures event
 * 2. InputContextManager queries active contexts
 * 3. Contexts receive event in priority order
 * 4. First context to handle event stops propagation
 *
 * @example
 * ```typescript
 * const handler = new InputHandler(canvas, dimsManager);
 *
 * // Register context for modal dialog
 * const modalContext = handler.createContext('modal', priority: 100);
 * modalContext.on('keydown', (e) => {
 *   if (e.key === 'Escape') {
 *     closeModal();
 *     return true; // Handled, stop propagation
 *   }
 *   return false; // Not handled, continue to lower contexts
 * });
 *
 * // Main view receives events only if modal doesn't handle them
 * const mainContext = handler.createContext('main', priority: 0);
 * mainContext.on('keydown', handleMainViewInput);
 * ```
 */
export class InputHandler {
  // Implementation...
}

Example 4: Utility Function

/**
 * Clamp a position value to dimension bounds.
 *
 * Ensures position stays within valid range defined by dimension metadata.
 * Handles edge case where min === max (returns min).
 *
 * @param value - Position value to clamp
 * @param dimension - Dimension metadata containing min/max bounds
 * @returns Clamped value guaranteed to be in range [min, max]
 *
 * @example
 * ```typescript
 * const dim = { name: 'time', min: 0, max: 100, step: 1 };
 * const clamped = clampPosition(150, dim);
 * console.log(clamped); // 100 (clamped to max)
 * ```
 */
export function clampPosition(
  value: number,
  dimension: DimensionMetadata
): number {
  return Math.max(dimension.min, Math.min(dimension.max, value));
}

Tag Usage Guidelines

@param

  • Always include for every parameter

  • Describe constraints: valid ranges, units, null behavior

  • Explain purpose: not just type, but WHY the parameter exists

  • Multi-line format: Use hyphens and indent continuation

/**
 * @param positions - Array of nD coordinates with shape (N, ndim).
 *                    Must be float32 dtype. Coordinates are in world space
 *                    units (not pixels). Can contain NaN for hidden points.
 */

@returns

  • Always include unless void

  • Describe meaning: not just type, but what the value signifies

  • Document special values: null, undefined, empty array, etc.

/**
 * @returns Matching chunk indices as array, or empty array if no chunks
 *          intersect the view region. Array is sorted by chunk index.
 */

@throws

  • Document all errors: don’t just document one, list all possibilities

  • Be specific: explain exact conditions that trigger the error

  • Include error type: use {ErrorType} syntax

/**
 * @throws {ValidationError} If array is not 2D
 * @throws {ValidationError} If dtype is not float32
 * @throws {RangeError} If dimension index >= ndim
 */

@example

  • Include for complex APIs: especially public methods used frequently

  • Show complete code: include imports, setup, and expected output

  • Multiple examples: basic usage first, then advanced/error handling

  • Add comments: explain what the example demonstrates

/**
 * @example
 * ```typescript
 * // Basic: Load and display a scene
 * import { SceneLoader } from './data/scene-loader';
 *
 * const loader = new SceneLoader(store);
 * const scene = await loader.loadScene('http://example.com/data.luxar.zarr');
 * threeScene.add(scene);
 * console.log(`Loaded ${scene.children.length} nodes`);
 * ```
 *
 * @example
 * ```typescript
 * // Advanced: Handle errors and show progress
 * try {
 *   loader.on('progress', (loaded, total) => {
 *     console.log(`Loading: ${(loaded/total*100).toFixed(0)}%`);
 *   });
 *   const scene = await loader.loadScene(url);
 * } catch (error) {
 *   if (error.message.includes('404')) {
 *     console.error('Dataset not found');
 *   } else {
 *     console.error('Load failed:', error);
 *   }
 * }
 * ```
 */

@see

  • Cross-reference related code: link to functions that work together

  • Link to documentation: reference README sections for deeper explanation

  • Use relative paths: keep links working if files move

/**
 * @see {@link clampPosition} for bounds clamping logic
 * @see {@link ../input/README.md#navigation} for navigation guide
 * @see {@link https://github.com/luxar/docs/navigation} for external docs
 */

Special Cases

Constructors

Document parameters but not return value (implied):

/**
 * Create a new input handler.
 *
 * Sets up global event listeners for keyboard and mouse input.
 * Automatically detects typing contexts (input/textarea elements)
 * and prevents navigation in those contexts.
 *
 * @param canvas - Canvas element to attach mouse listeners to
 * @param dimsManager - Dimension manager for navigation target
 * @param config - Optional configuration overrides
 *
 * @example
 * ```typescript
 * const handler = new InputHandler(canvas, dimsManager, {
 *   enableMouse: true,
 *   enableKeyboard: true,
 * });
 * ```
 */
constructor(
  canvas: HTMLCanvasElement,
  dimsManager: SceneDimsManager,
  config?: Partial<InputConfig>
) {
  // Implementation...
}

Getters/Setters

Brief documentation focusing on behavior:

/**
 * Current slice position in nD space.
 *
 * Setting position triggers view update and data reload.
 * Position is clamped to dimension bounds automatically.
 */
get position(): number[] {
  return this._position;
}

set position(value: number[]) {
  this._position = this.clampPosition(value);
  this.updateView();
}

Event Handlers

Document event type and callback signature:

/**
 * Register callback for dimension change events.
 *
 * Emitted whenever user navigates in any dimension via keyboard,
 * slider, or programmatic call to setPosition().
 *
 * @param event - Event name (only 'dimensionChanged' supported)
 * @param callback - Handler receiving dimension index and new position
 *
 * @example
 * ```typescript
 * handler.on('dimensionChanged', (dimIndex, newPos) => {
 *   console.log(`Dimension ${dimIndex} moved to ${newPos}`);
 *   updateDataView();
 * });
 * ```
 */
on(
  event: 'dimensionChanged',
  callback: (dimIndex: number, position: number) => void
): void {
  // Implementation...
}

Deprecated Functions

Mark clearly and provide alternatives:

/**
 * Load scene from URL.
 *
 * @deprecated Since v1.4.0 - Use {@link SceneLoader.loadScene} instead.
 *             This method will be removed in v2.0.0.
 *
 * @param url - Scene URL
 * @returns Promise resolving to scene
 *
 * @example
 * ```typescript
 * // Old way (deprecated)
 * const scene = await loadSceneFromUrl(url);
 *
 * // New way (recommended)
 * const loader = new SceneLoader(store);
 * const scene = await loader.loadScene(url);
 * ```
 */

Common Mistakes to Avoid

❌ Don’t: Repeat Type Information

TypeScript already provides type information via signatures.

// ❌ BAD - Just repeating types
/**
 * @param value - A number
 * @returns A number
 */
function double(value: number): number {
  return value * 2;
}

// ✅ GOOD - Explain meaning and constraints
/**
 * Double a numeric value.
 *
 * @param value - Value to double. Can be any finite number.
 * @returns Doubled value, guaranteed to have same sign as input
 */
function double(value: number): number {
  return value * 2;
}

❌ Don’t: State the Obvious

// ❌ BAD - Obvious from function name
/**
 * Gets the position.
 * @returns The position
 */
getPosition(): number[] { }

// ✅ GOOD - Explains behavior and units
/**
 * Current slice position in nD space.
 *
 * @returns Position array with length equal to dimensionality.
 *          Values are in world space units (not pixels).
 */
getPosition(): number[] { }

❌ Don’t: Use Vague Descriptions

// ❌ BAD - Too vague
/**
 * Handles input.
 * @param event - The event
 */
handleInput(event: KeyboardEvent): void { }

// ✅ GOOD - Specific about what and why
/**
 * Process keyboard input for dimension navigation.
 *
 * Detects arrow keys, number keys (dimension selection), and
 * bracket keys (forward/backward navigation). Respects input
 * context priority and typing detection.
 *
 * @param event - Keyboard event from global listener. Event may
 *                be from any element, not just canvas.
 */
handleInput(event: KeyboardEvent): void { }

❌ Don’t: Write Novel-Length Documentation

Keep it concise but complete:

// ❌ BAD - Too much detail obscures key points
/**
 * This function is used to calculate the size of chunks. Chunks are
 * pieces of data that are loaded from the zarr store. The size of
 * chunks affects performance. Larger chunks mean fewer HTTP requests
 * but more data transfer and memory usage. Smaller chunks mean more
 * HTTP requests but less memory. This function tries to balance these
 * tradeoffs by calculating an optimal chunk size based on the array
 * size and a target number of bytes per chunk. The target is typically
 * 64KB because that's a good balance between...
 * (continues for 20 more lines)
 */

// ✅ GOOD - Concise with key information
/**
 * Calculate optimal chunk size for array.
 *
 * Balances memory usage vs HTTP request count. Targets 64KB chunks
 * which provides good performance for most datasets. Prefers power-of-2
 * sizes for alignment efficiency.
 *
 * @param arraySize - Total array size in bytes
 * @param targetBytes - Target chunk size in bytes (default: 65536 = 64KB)
 * @returns Chunk size in bytes, will be <= arraySize
 */

Documentation Checklist

Before committing code with JSDoc, verify:

  • File has module-level documentation

  • All exported functions/classes/interfaces have JSDoc

  • All @param tags present with meaningful descriptions

  • @returns documented (unless void)

  • @throws documented for all error conditions

  • At least one @example for complex/frequently-used APIs

  • Cross-references added with @see where applicable

  • No typos or grammatical errors

  • Descriptions explain WHY and HOW, not just WHAT

  • Code examples are complete and runnable


Tools and Automation

VS Code Extension

Install Document This extension for quick JSDoc scaffolding:

  • Place cursor on function

  • Press Ctrl+Alt+D twice

  • Edit generated template

Linting

Add to eslint.config.js:

{
  "plugins": ["jsdoc"],
  "rules": {
    "jsdoc/require-jsdoc": ["error", {
      "require": {
        "FunctionDeclaration": true,
        "MethodDefinition": true,
        "ClassDeclaration": true,
        "ArrowFunctionExpression": false,
        "FunctionExpression": false
      }
    }],
    "jsdoc/require-param": "error",
    "jsdoc/require-param-description": "error",
    "jsdoc/require-returns": "error",
    "jsdoc/require-returns-description": "error"
  }
}

Coverage Checking

JSDoc coverage is part of the baseline-driven make check-docs gate. New under-documented files fail CI; existing file-level debt is recorded in scripts/docs_baseline.json. TypeDoc’s separate warning set is ratcheted by pnpm run typedoc:check-warnings. See Documentation Quality Gate for scope, limitations, and baseline-update rules.


Questions?

See existing examples in:

  • src/cache/lru-cache.ts - Excellent JSDoc examples

  • src/types/dims.ts - Good interface documentation

  • src/scene/scene-dims-manager.ts - Complete method documentation

For Python docstrings, follow the conventions in CLAUDE.md and the per-package README.md files.