15 KiB
		
	
	
	
	
	
	
	
			
		
		
	
	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
{
  "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 tagfile:filename- Match by filenamepath: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:
{
  "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:
{
  "rev": "def456-789"
}
Status Codes:
200- Success409- Conflict (file modified externally)500- Server error
Color Conversion
RGB Integer Format
Obsidian stores colors as integers (0-16777215):
// 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
const filtered = GraphRuntimeAdapter.applyFilters(
  baseGraphData,
  config,
  allNotes
);
Applies:
- Text search on node labels
 - Tag/attachment/orphan filtering
 - Unresolved link filtering
 
Display Options Conversion
const displayOptions = GraphRuntimeAdapter.configToDisplayOptions(config);
Converts:
textFadeMultiplier(-3 to 3) →textFadeThreshold(0 to 100)nodeSizeMultiplier→ absolutenodeSize(base: 5px)lineSizeMultiplier→ absolutelinkThickness(base: 1px)repelStrength(0 to 20) →chargeStrength(0 to -200)
Color Groups Application
const nodeColors = GraphRuntimeAdapter.applyColorGroups(
  nodes,
  config,
  allNotes
);
Returns Map<nodeId, cssColor> for node coloring.
State Management
Service Layer
GraphSettingsService:
config: Signal<GraphConfig>- Current configurationload()- Load from APIsave(patch)- Save with debounce (250ms)watch(callback)- Subscribe to changesresetToDefaults()- Reset all settingsresetSection(section)- Reset specific sectiontoggleCollapse(section)- Toggle section expand/collapse
Reactive Updates
All settings changes trigger:
- Immediate UI update (signal update)
 - Debounced file write (250ms)
 - 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 panelEnter/Space- Activate buttons/togglesTab- Navigate controls
Programmatic Access
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
- Drag & Drop: Groups reordering not yet implemented
 - Scale/Close: Preserved but not exposed in UI
 - Advanced Queries: Only basic query types supported
 - 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 
.obsidiandirectory 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.
// Auto-saved when settings change
graphSettingsService.save({ showArrow: true });
2. localStorage
Cached locally for instant loading on page reload. Updated automatically whenever settings change.
// 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:
const encoded = btoa(JSON.stringify(config))
  .replace(/\+/g, '-')
  .replace(/\//g, '_')
  .replace(/=/g, '');
Usage:
// 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:
- URL parameter (
?gs=...) - Highest priority (shareable state) - localStorage - Fast local cache
 - graph.json - Persistent vault settings
 - Defaults - Fallback if nothing else available
 
Sharing Graph Views
To share a specific graph configuration:
- Configure your graph settings as desired
 - Settings are automatically encoded in the URL
 - Share the URL - recipients see the same view
 - 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.
// 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 
tagsarray - Handles tags with or without 
#prefix - Case-insensitive comparison
 
File Query: file:searchtext
Matches nodes where title or path contains the search text.
// 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 
titleandpathfields - 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.
// 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 
pathstarts with the specified folder - Case-insensitive comparison
 - Supports nested paths with 
/separator - Exact folder boundary matching (doesn't partially match folder names)
 
Parsing Implementation
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
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:
{
  "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
// 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 graph view interface and behavior.