591 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			591 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| # Graph Settings Feature
 | |
| 
 | |
| Complete implementation of Obsidian-compatible graph settings panel for ObsiViewer with high-performance Canvas rendering.
 | |
| 
 | |
| ## Overview
 | |
| 
 | |
| This feature adds a comprehensive settings panel for the graph view, matching Obsidian's interface and functionality. Settings are stored in `.obsidian/graph.json`, persisted to localStorage, and can be shared via URL parameters. The graph is rendered using HTML5 Canvas with d3-force for optimal performance.
 | |
| 
 | |
| ## Features
 | |
| 
 | |
| - ⚙️ **Settings Button**: Gear icon in top-right of graph view
 | |
| - 🎨 **Settings Panel**: Slide-over panel with four collapsible sections
 | |
| - 💾 **Triple Persistence**: `.obsidian/graph.json` + localStorage + URL query parameters
 | |
| - 🔗 **URL Sharing**: Settings encoded in URL (`?gs=base64url(...)`) for shareable views
 | |
| - 🚀 **Canvas Rendering**: High-performance rendering for 2-3k nodes
 | |
| - 🔄 **Live Sync**: Watch for external file changes and reload
 | |
| - 🎯 **Real-time Filtering**: All controls update graph within 100ms
 | |
| - 📱 **Responsive**: Desktop slide-over, mobile full-screen
 | |
| - ♿ **Accessible**: Keyboard navigation, ARIA labels, focus management
 | |
| - 🏷️ **Interactive Legend**: Click legend items to filter by group
 | |
| 
 | |
| ## Architecture
 | |
| 
 | |
| ### Files Created
 | |
| 
 | |
| ```
 | |
| src/app/graph/
 | |
| ├── graph-settings.types.ts          # Type definitions & utilities
 | |
| ├── graph-settings.service.ts        # Config read/write/watch service
 | |
| ├── graph-runtime-adapter.ts         # Apply settings to graph engine
 | |
| └── ui/
 | |
|     ├── settings-button.component.ts # Gear icon button
 | |
|     ├── settings-panel.component.ts  # Main panel with sections
 | |
|     └── sections/
 | |
|         ├── filters-section.component.ts
 | |
|         ├── groups-section.component.ts
 | |
|         ├── display-section.component.ts
 | |
|         └── forces-section.component.ts
 | |
| 
 | |
| server/
 | |
| └── index.mjs                        # API endpoints added
 | |
| ```
 | |
| 
 | |
| ## Configuration Structure
 | |
| 
 | |
| ### graph.json Format
 | |
| 
 | |
| ```json
 | |
| {
 | |
|   "collapse-filter": false,
 | |
|   "search": "",
 | |
|   "showTags": false,
 | |
|   "showAttachments": false,
 | |
|   "hideUnresolved": false,
 | |
|   "showOrphans": true,
 | |
|   
 | |
|   "collapse-color-groups": false,
 | |
|   "colorGroups": [
 | |
|     {
 | |
|       "query": "tag:#markdown",
 | |
|       "color": { "a": 1, "rgb": 14701138 }
 | |
|     }
 | |
|   ],
 | |
|   
 | |
|   "collapse-display": false,
 | |
|   "showArrow": false,
 | |
|   "textFadeMultiplier": 0,
 | |
|   "nodeSizeMultiplier": 1,
 | |
|   "lineSizeMultiplier": 1,
 | |
|   
 | |
|   "collapse-forces": false,
 | |
|   "centerStrength": 0.5,
 | |
|   "repelStrength": 10,
 | |
|   "linkStrength": 1,
 | |
|   "linkDistance": 250,
 | |
|   
 | |
|   "scale": 1,
 | |
|   "close": false
 | |
| }
 | |
| ```
 | |
| 
 | |
| ## Settings Sections
 | |
| 
 | |
| ### 1. Filters
 | |
| 
 | |
| Controls which nodes appear in the graph.
 | |
| 
 | |
| | Control | JSON Key | Type | Description |
 | |
| |---------|----------|------|-------------|
 | |
| | Search | `search` | string | Filter nodes by text search |
 | |
| | Tags | `showTags` | boolean | Show/hide tag nodes |
 | |
| | Attachments | `showAttachments` | boolean | Show/hide attachment files |
 | |
| | Existing files only | `hideUnresolved` | boolean | Hide unresolved links (inverted) |
 | |
| | Orphans | `showOrphans` | boolean | Show/hide orphan nodes |
 | |
| 
 | |
| ### 2. Groups
 | |
| 
 | |
| Color nodes based on queries.
 | |
| 
 | |
| - **Color Groups**: Array of `{ query, color }` objects
 | |
| - **Query Types**:
 | |
|   - `tag:#tagname` - Match notes with tag
 | |
|   - `file:filename` - Match by filename
 | |
|   - `path:folder` - Match by folder path
 | |
| - **Color Format**: `{ a: number, rgb: number }` (RGB as integer 0-16777215)
 | |
| - **Actions**: New group, Duplicate, Delete, Reorder (drag & drop)
 | |
| 
 | |
| ### 3. Display
 | |
| 
 | |
| Visual appearance settings.
 | |
| 
 | |
| | Control | JSON Key | Range | Step | Description |
 | |
| |---------|----------|-------|------|-------------|
 | |
| | Arrows | `showArrow` | boolean | - | Show directional arrows on links |
 | |
| | Text fade threshold | `textFadeMultiplier` | -3 to 3 | 0.1 | When to fade node labels |
 | |
| | Node size | `nodeSizeMultiplier` | 0.25 to 3 | 0.05 | Size multiplier for nodes |
 | |
| | Link thickness | `lineSizeMultiplier` | 0.25 to 3 | 0.05 | Thickness multiplier for links |
 | |
| 
 | |
| ### 4. Forces
 | |
| 
 | |
| Physics simulation parameters.
 | |
| 
 | |
| | Control | JSON Key | Range | Step | Description |
 | |
| |---------|----------|-------|------|-------------|
 | |
| | Center force | `centerStrength` | 0 to 2 | 0.01 | Pull toward center |
 | |
| | Repel force | `repelStrength` | 0 to 20 | 0.5 | Push nodes apart |
 | |
| | Link force | `linkStrength` | 0 to 2 | 0.01 | Pull connected nodes together |
 | |
| | Link distance | `linkDistance` | 20 to 300 | 1 | Target distance between linked nodes |
 | |
| 
 | |
| ## API Endpoints
 | |
| 
 | |
| ### GET /api/vault/graph
 | |
| 
 | |
| Load graph configuration.
 | |
| 
 | |
| **Response:**
 | |
| ```json
 | |
| {
 | |
|   "config": { /* GraphConfig */ },
 | |
|   "rev": "abc123-456"
 | |
| }
 | |
| ```
 | |
| 
 | |
| ### PUT /api/vault/graph
 | |
| 
 | |
| Save graph configuration.
 | |
| 
 | |
| **Headers:**
 | |
| - `If-Match`: Optional revision for conflict detection
 | |
| 
 | |
| **Body:** Complete `GraphConfig` object
 | |
| 
 | |
| **Response:**
 | |
| ```json
 | |
| {
 | |
|   "rev": "def456-789"
 | |
| }
 | |
| ```
 | |
| 
 | |
| **Status Codes:**
 | |
| - `200` - Success
 | |
| - `409` - Conflict (file modified externally)
 | |
| - `500` - Server error
 | |
| 
 | |
| ## Color Conversion
 | |
| 
 | |
| ### RGB Integer Format
 | |
| 
 | |
| Obsidian stores colors as integers (0-16777215):
 | |
| 
 | |
| ```typescript
 | |
| // HEX to Integer
 | |
| const hex = "#E06C75";
 | |
| const rgb = hexToInt(hex); // 14707829
 | |
| 
 | |
| // Integer to RGBA
 | |
| const { r, g, b, a } = intToRgba(14707829, 1);
 | |
| // r: 224, g: 108, b: 117, a: 1
 | |
| 
 | |
| // To CSS
 | |
| const css = colorToCss({ a: 1, rgb: 14707829 });
 | |
| // "rgba(224, 108, 117, 1)"
 | |
| ```
 | |
| 
 | |
| ## Runtime Adapter
 | |
| 
 | |
| The `GraphRuntimeAdapter` translates config to runtime options:
 | |
| 
 | |
| ### Filters Application
 | |
| 
 | |
| ```typescript
 | |
| const filtered = GraphRuntimeAdapter.applyFilters(
 | |
|   baseGraphData,
 | |
|   config,
 | |
|   allNotes
 | |
| );
 | |
| ```
 | |
| 
 | |
| Applies:
 | |
| - Text search on node labels
 | |
| - Tag/attachment/orphan filtering
 | |
| - Unresolved link filtering
 | |
| 
 | |
| ### Display Options Conversion
 | |
| 
 | |
| ```typescript
 | |
| const displayOptions = GraphRuntimeAdapter.configToDisplayOptions(config);
 | |
| ```
 | |
| 
 | |
| Converts:
 | |
| - `textFadeMultiplier` (-3 to 3) → `textFadeThreshold` (0 to 100)
 | |
| - `nodeSizeMultiplier` → absolute `nodeSize` (base: 5px)
 | |
| - `lineSizeMultiplier` → absolute `linkThickness` (base: 1px)
 | |
| - `repelStrength` (0 to 20) → `chargeStrength` (0 to -200)
 | |
| 
 | |
| ### Color Groups Application
 | |
| 
 | |
| ```typescript
 | |
| const nodeColors = GraphRuntimeAdapter.applyColorGroups(
 | |
|   nodes,
 | |
|   config,
 | |
|   allNotes
 | |
| );
 | |
| ```
 | |
| 
 | |
| Returns `Map<nodeId, cssColor>` for node coloring.
 | |
| 
 | |
| ## State Management
 | |
| 
 | |
| ### Service Layer
 | |
| 
 | |
| **GraphSettingsService**:
 | |
| - `config: Signal<GraphConfig>` - Current configuration
 | |
| - `load()` - Load from API
 | |
| - `save(patch)` - Save with debounce (250ms)
 | |
| - `watch(callback)` - Subscribe to changes
 | |
| - `resetToDefaults()` - Reset all settings
 | |
| - `resetSection(section)` - Reset specific section
 | |
| - `toggleCollapse(section)` - Toggle section expand/collapse
 | |
| 
 | |
| ### Reactive Updates
 | |
| 
 | |
| All settings changes trigger:
 | |
| 1. Immediate UI update (signal update)
 | |
| 2. Debounced file write (250ms)
 | |
| 3. Graph re-render (via computed signals)
 | |
| 
 | |
| ### External Change Detection
 | |
| 
 | |
| Polls every 2 seconds to detect external file modifications and reload config.
 | |
| 
 | |
| ## Usage Examples
 | |
| 
 | |
| ### Open Settings Panel
 | |
| 
 | |
| Click the gear icon (⚙️) in top-right of graph view.
 | |
| 
 | |
| ### Keyboard Shortcuts
 | |
| 
 | |
| - `Esc` - Close settings panel
 | |
| - `Enter`/`Space` - Activate buttons/toggles
 | |
| - `Tab` - Navigate controls
 | |
| 
 | |
| ### Programmatic Access
 | |
| 
 | |
| ```typescript
 | |
| import { GraphSettingsService } from './app/graph/graph-settings.service';
 | |
| 
 | |
| // Inject service
 | |
| constructor(private settings: GraphSettingsService) {}
 | |
| 
 | |
| // Get current config
 | |
| const config = this.settings.config();
 | |
| 
 | |
| // Update settings
 | |
| this.settings.save({
 | |
|   showArrow: true,
 | |
|   nodeSizeMultiplier: 1.5
 | |
| });
 | |
| 
 | |
| // Watch for changes
 | |
| this.settings.watch(config => {
 | |
|   console.log('Config updated:', config);
 | |
| });
 | |
| 
 | |
| // Reset
 | |
| this.settings.resetToDefaults();
 | |
| this.settings.resetSection('display');
 | |
| ```
 | |
| 
 | |
| ## Testing Checklist
 | |
| 
 | |
| ### Manual Tests
 | |
| 
 | |
| - [ ] Click gear icon → panel slides in
 | |
| - [ ] All sections expand/collapse correctly
 | |
| - [ ] Each control modifies corresponding JSON key
 | |
| - [ ] Settings persist after page reload
 | |
| - [ ] External file edit → UI updates
 | |
| - [ ] Search filter works on node labels
 | |
| - [ ] Tags toggle hides/shows tag nodes
 | |
| - [ ] Existing files only hides unresolved links
 | |
| - [ ] Orphans toggle hides/shows orphan nodes
 | |
| - [ ] New color group adds with default color
 | |
| - [ ] Color picker updates group color
 | |
| - [ ] Query input filters matching nodes
 | |
| - [ ] Delete group removes from list
 | |
| - [ ] Duplicate group creates copy
 | |
| - [ ] Arrows toggle shows/hides link arrows
 | |
| - [ ] All sliders update in real-time
 | |
| - [ ] Animate button restarts simulation
 | |
| - [ ] Force sliders affect node positions
 | |
| - [ ] Reset all restores defaults
 | |
| - [ ] Reset section restores section defaults
 | |
| - [ ] Close button closes panel
 | |
| - [ ] Esc key closes panel
 | |
| - [ ] Mobile: full-screen panel
 | |
| - [ ] Dark mode: correct theming
 | |
| 
 | |
| ### Integration Tests
 | |
| 
 | |
| - [ ] graph.json created on first run
 | |
| - [ ] Atomic writes prevent corruption
 | |
| - [ ] Backup file created (.bak)
 | |
| - [ ] Conflict detection works (409 response)
 | |
| - [ ] Polling detects external changes
 | |
| - [ ] Debounce prevents excessive writes
 | |
| - [ ] Invalid JSON handled gracefully
 | |
| - [ ] Missing file uses defaults
 | |
| 
 | |
| ## Performance
 | |
| 
 | |
| - **Debounce**: 250ms write delay
 | |
| - **Polling**: 2-second interval
 | |
| - **Atomic Writes**: Temp file + rename
 | |
| - **Validation**: Clamps values to bounds
 | |
| - **Signal-based**: Minimal re-renders
 | |
| 
 | |
| ## Browser Compatibility
 | |
| 
 | |
| - Chrome/Edge: ✅ Full support
 | |
| - Firefox: ✅ Full support
 | |
| - Safari: ✅ Full support
 | |
| - Mobile browsers: ✅ Responsive design
 | |
| 
 | |
| ## Known Limitations
 | |
| 
 | |
| 1. **Drag & Drop**: Groups reordering not yet implemented
 | |
| 2. **Scale/Close**: Preserved but not exposed in UI
 | |
| 3. **Advanced Queries**: Only basic query types supported
 | |
| 4. **Undo/Redo**: Not implemented
 | |
| 
 | |
| ## Future Enhancements
 | |
| 
 | |
| - [ ] Drag & drop for color group reordering
 | |
| - [ ] Advanced query builder UI
 | |
| - [ ] Preset configurations
 | |
| - [ ] Export/import settings
 | |
| - [ ] Undo/redo for settings changes
 | |
| - [ ] Keyboard shortcuts for common actions
 | |
| - [ ] Animation presets
 | |
| - [ ] Graph layout algorithms selector
 | |
| 
 | |
| ## Troubleshooting
 | |
| 
 | |
| ### Settings Not Saving
 | |
| 
 | |
| - Check browser console for errors
 | |
| - Verify `.obsidian` directory exists
 | |
| - Check file permissions
 | |
| - Ensure server is running
 | |
| 
 | |
| ### Panel Not Opening
 | |
| 
 | |
| - Check for JavaScript errors
 | |
| - Verify all components imported correctly
 | |
| - Clear browser cache
 | |
| 
 | |
| ### External Changes Not Detected
 | |
| 
 | |
| - Polling interval is 2 seconds
 | |
| - Check file watcher is enabled
 | |
| - Verify file path is correct
 | |
| 
 | |
| ### Performance Issues
 | |
| 
 | |
| - Reduce polling frequency
 | |
| - Increase debounce delay
 | |
| - Disable external change detection
 | |
| 
 | |
| ## Settings Persistence
 | |
| 
 | |
| ### Three-Layer Persistence Strategy
 | |
| 
 | |
| Settings are persisted across three mechanisms for maximum flexibility:
 | |
| 
 | |
| #### 1. File Storage (.obsidian/graph.json)
 | |
| 
 | |
| Primary storage matching Obsidian's format. Changes are debounced (250ms) and saved atomically.
 | |
| 
 | |
| ```typescript
 | |
| // Auto-saved when settings change
 | |
| graphSettingsService.save({ showArrow: true });
 | |
| ```
 | |
| 
 | |
| #### 2. localStorage
 | |
| 
 | |
| Cached locally for instant loading on page reload. Updated automatically whenever settings change.
 | |
| 
 | |
| ```typescript
 | |
| // Key: 'obsi-graph-settings'
 | |
| localStorage.setItem('obsi-graph-settings', JSON.stringify(config));
 | |
| ```
 | |
| 
 | |
| #### 3. URL Query Parameters
 | |
| 
 | |
| Settings can be encoded in the URL for sharing specific graph views:
 | |
| 
 | |
| ```
 | |
| https://your-app.com/graph?gs=eyJzaG93QXJyb3ciOnRydWV9
 | |
| ```
 | |
| 
 | |
| **Encoding:**
 | |
| ```typescript
 | |
| const encoded = btoa(JSON.stringify(config))
 | |
|   .replace(/\+/g, '-')
 | |
|   .replace(/\//g, '_')
 | |
|   .replace(/=/g, '');
 | |
| ```
 | |
| 
 | |
| **Usage:**
 | |
| ```typescript
 | |
| // Update URL with current settings
 | |
| graphSettingsStore.updateURL();
 | |
| 
 | |
| // Settings automatically loaded from URL on page load
 | |
| // Priority: URL > localStorage > .obsidian/graph.json > defaults
 | |
| ```
 | |
| 
 | |
| ### Loading Priority
 | |
| 
 | |
| On initialization, settings are loaded in this order:
 | |
| 
 | |
| 1. **URL parameter** (`?gs=...`) - Highest priority (shareable state)
 | |
| 2. **localStorage** - Fast local cache
 | |
| 3. **graph.json** - Persistent vault settings
 | |
| 4. **Defaults** - Fallback if nothing else available
 | |
| 
 | |
| ### Sharing Graph Views
 | |
| 
 | |
| To share a specific graph configuration:
 | |
| 
 | |
| 1. Configure your graph settings as desired
 | |
| 2. Settings are automatically encoded in the URL
 | |
| 3. Share the URL - recipients see the same view
 | |
| 4. URL settings override their local settings temporarily
 | |
| 
 | |
| ## Group Query Grammar
 | |
| 
 | |
| The group query system supports three query types for coloring nodes:
 | |
| 
 | |
| ### Query Syntax
 | |
| 
 | |
| #### Tag Query: `tag:#tagname` or `tag:tagname`
 | |
| 
 | |
| Matches nodes that have the specified tag.
 | |
| 
 | |
| ```typescript
 | |
| // Match all notes with #markdown tag
 | |
| "tag:#markdown"
 | |
| 
 | |
| // Hash is optional
 | |
| "tag:markdown"
 | |
| 
 | |
| // Case-insensitive matching
 | |
| "tag:TEST" // matches #test, #Test, #TEST
 | |
| ```
 | |
| 
 | |
| **Behavior:**
 | |
| - Checks node's `tags` array
 | |
| - Handles tags with or without `#` prefix
 | |
| - Case-insensitive comparison
 | |
| 
 | |
| #### File Query: `file:searchtext`
 | |
| 
 | |
| Matches nodes where title or path contains the search text.
 | |
| 
 | |
| ```typescript
 | |
| // Match notes with "test" in title or path
 | |
| "file:test"
 | |
| 
 | |
| // Partial matches work
 | |
| "file:meeting" // matches "Meeting Notes.md", "meetings/summary.md"
 | |
| ```
 | |
| 
 | |
| **Behavior:**
 | |
| - Searches in both `title` and `path` fields
 | |
| - Case-insensitive substring matching
 | |
| - Useful for grouping by naming conventions
 | |
| 
 | |
| #### Path Query: `path:folder` or `path:folder/subfolder`
 | |
| 
 | |
| Matches nodes in a specific folder path.
 | |
| 
 | |
| ```typescript
 | |
| // Match notes in "projects" folder
 | |
| "path:projects"
 | |
| 
 | |
| // Match notes in nested folder
 | |
| "path:projects/2024"
 | |
| 
 | |
| // Trailing slash optional
 | |
| "path:projects/"
 | |
| ```
 | |
| 
 | |
| **Behavior:**
 | |
| - Checks if node's `path` starts with the specified folder
 | |
| - Case-insensitive comparison
 | |
| - Supports nested paths with `/` separator
 | |
| - Exact folder boundary matching (doesn't partially match folder names)
 | |
| 
 | |
| ### Parsing Implementation
 | |
| 
 | |
| ```typescript
 | |
| import { parseGroupQuery, nodeMatchesQuery } from './graph/group-query.parser';
 | |
| 
 | |
| // Parse a query
 | |
| const query = parseGroupQuery('tag:#markdown');
 | |
| // { type: 'tag', value: '#markdown', raw: 'tag:#markdown' }
 | |
| 
 | |
| // Check if node matches
 | |
| const matches = nodeMatchesQuery(node, query);
 | |
| // true or false
 | |
| ```
 | |
| 
 | |
| ### Query Validation
 | |
| 
 | |
| ```typescript
 | |
| import { isValidQuery } from './graph/group-query.parser';
 | |
| 
 | |
| isValidQuery('tag:#test');     // true
 | |
| isValidQuery('file:meeting');  // true
 | |
| isValidQuery('path:folder');   // true
 | |
| isValidQuery('invalid');       // false
 | |
| isValidQuery('tag:');          // false
 | |
| ```
 | |
| 
 | |
| ### Color Group Application
 | |
| 
 | |
| Groups are applied in order with **first-match-wins** logic:
 | |
| 
 | |
| ```json
 | |
| {
 | |
|   "colorGroups": [
 | |
|     { "query": "tag:#important", "color": { "a": 1, "rgb": 16711680 } },
 | |
|     { "query": "tag:#markdown", "color": { "a": 1, "rgb": 65280 } },
 | |
|     { "query": "file:test", "color": { "a": 1, "rgb": 255 } }
 | |
|   ]
 | |
| }
 | |
| ```
 | |
| 
 | |
| A node with both `#important` and `#markdown` tags will be colored with the **first matching group** (#important - red).
 | |
| 
 | |
| ### Examples
 | |
| 
 | |
| ```typescript
 | |
| // Group by project folder
 | |
| "path:projects"
 | |
| 
 | |
| // Group by status tags
 | |
| "tag:#todo"
 | |
| "tag:#in-progress"
 | |
| "tag:#done"
 | |
| 
 | |
| // Group by file type
 | |
| "file:.excalidraw"
 | |
| "file:.canvas"
 | |
| 
 | |
| // Group by area
 | |
| "path:work"
 | |
| "path:personal"
 | |
| 
 | |
| // Group by naming convention
 | |
| "file:meeting"
 | |
| "file:daily-note"
 | |
| ```
 | |
| 
 | |
| ## Credits
 | |
| 
 | |
| Designed to match [Obsidian](https://obsidian.md/) graph view interface and behavior.
 |