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