ObsiViewer/docs/GRAPH_SETTINGS.md

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 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:

{
  "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 - Success
  • 409 - 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 → absolute nodeSize (base: 5px)
  • lineSizeMultiplier → absolute linkThickness (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 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

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.

// 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:

  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.

// 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.

// 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.

// 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

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.