<h1>Build a Local Knowledge Graph With Vanilla JavaScript — No Backend Required</h1>
<p>Most knowledge base tools require a server, a database, or a subscription. This one doesn't.</p>
<p>I built a <strong>local-first knowledge graph</strong> — a personal knowledge base where notes connect through wikilinks and relationships visualize as an interactive network. All data lives in your browser's localStorage. Zero tracking, zero signup, zero dependencies.</p>
<p>Let me show you how it works and walk through the key pieces of code.</p>
<h2>The Problem With Existing Tools</h2>
<p>Popular knowledge base tools like Obsidian, Notion, or Roam Research are powerful — but they come with trade-offs:</p>
<ul>
<li><strong>Proprietary</strong>: Your data is locked in their ecosystem</li>
<li><strong>Connected</strong>: They need your internet connection to function</li>
<li><strong>Expensive</strong>: Many features require paid plans</li>
<li><strong>Complex</strong>: Overwhelming for simple note-taking needs</li>
</ul>
<p>What if you could build something simpler that still gives you the core benefit: <strong>seeing how your ideas connect</strong>?</p>
<h2>The Architecture</h2>
<p>The knowledge graph has three layers:</p>
<ol>
<li><strong>Data layer</strong> — Notes stored in localStorage as JSON</li>
<li><strong>Editor layer</strong> — Markdown text area with wikilink support (<code>[[link]]</code>)</li>
<li><strong>Graph layer</strong> — Force-directed visualization showing note relationships</li>
</ol>
<p>Here's the complete architecture in about 350 lines of code. Let's break it down.</p>
<h2>The Data Model</h2>
<p>Each note is a simple object:</p>
<pre><code class="language-javascript">{
id: "abc123",
title: "My First Note",
content: "This is the content. It links to [[Another Note]].",
created: 1696320000000,
updated: 1696320000000
}
</code></pre>
<p>And we extract links with a regex:</p>
<pre><code class="language-javascript">function getLinksFromContent(content) {
const matches = content.match(/\[\[([^\]]+)\]\]/g);
return matches ? matches.map(m => m.slice(2, -2).trim()) : [];
}
</code></pre>
<p>This finds all <code>[[wikilinks]]</code> in the content and returns the linked note titles. Simple but effective.</p>
<h2>Persisting Data</h2>
<p>localStorage is perfect for this use case:</p>
<pre><code class="language-javascript">function saveNotes() {
localStorage.setItem('k1r4_knowledge_graph', JSON.stringify(notes));
updateStats();
renderNoteList();
updateGraph();
}
function loadNotes() {
try {
const data = localStorage.getItem('k1r4_knowledge_graph');
notes = data ? JSON.parse(data) : [];
} catch { notes = []; }
}
</code></pre>
<p>localStorage gives us ~5MB of storage, which is enough for thousands of notes. The data persists across browser sessions and requires no server.</p>
<h2>The Wikilink System</h2>
<p>Wikilinks are the heart of the knowledge graph. They allow you to write naturally:</p>
<blockquote>
<p>The concept of [[emergence]] is important in [[complexity theory]]. Emergent properties arise from simple interactions.</p>
</blockquote>
<p>When you <code>[[click]]</code> a wikilink in preview mode, it opens the linked note:</p>
<pre><code class="language-javascript">function openLinkedNote(title) {
const matching = notes.find(n =>
n.title.toLowerCase() === title.toLowerCase()
);
if (matching) {
selectNote(matching.id);
}
}
</code></pre>
<p>This creates a bidirectional navigation system — you can jump between related notes instantly.</p>
<h2>The Force-Directed Graph</h2>
<p>The graph visualization uses a simple force simulation. Here's the core algorithm:</p>
<pre><code class="language-javascript">function runForceSimulation() {
const iterations = 100;
const centerX = 400, centerY = 300;
for (let iter = 0; iter < iterations; iter++) {
const temp = 2 * (1 - iter / iterations);
// Repulsion between all nodes
for (let i = 0; i < graphNodes.length; i++) {
for (let j = i + 1; j < graphNodes.length; j++) {
const a = graphNodes[i], b = graphNodes[j];
let dx = a.x - b.x, dy = a.y - b.y;
let dist = Math.sqrt(dx * dx + dy * dy) || 1;
let force = 8000 / (dist * dist);
let fx = (dx / dist) * force * temp;
let fy = (dy / dist) * force * temp;
a.vx += fx; a.vy += fy;
b.vx -= fx; b.vy -= fy;
}
}
// Attraction along edges
for (const edge of graphEdges) {
const a = graphNodes.find(n => n.id === edge.source);
const b = graphNodes.find(n => n.id === edge.target);
if (!a || !b) continue;
let dx = b.x - a.x, dy = b.y - a.y;
let dist = Math.sqrt(dx * dx + dy * dy) || 1;
let force = (dist - 120) * 0.01;
let fx = (dx / dist) * force * temp;
let fy = (dy / dist) * force * temp;
a.vx += fx; a.vy += fy;
b.vx -= fx; b.vy -= fy;
}
// Center gravity
for (const node of graphNodes) {
node.vx += (centerX - node.x) * 0.005 * temp;
node.vy += (centerY - node.y) * 0.005 * temp;
}
// Apply velocities
for (const node of graphNodes) {
node.x += node.vx;
node.y += node.vy;
node.vx *= 0.5;
node.vy *= 0.5;
}
}
}
</code></pre>
<h3>How the Physics Works</h3>
<ol>
<li><strong>Repulsion</strong>: Every node pushes away from every other node (Coulomb's law)</li>
<li><strong>Attraction</strong>: Connected nodes are pulled together (Hooke's law)</li>
<li><strong>Center gravity</strong>: All nodes drift toward the center</li>
<li><strong>Cooling</strong>: The simulation gradually reduces movement (simulated annealing)</li>
</ol>
<p>After 100 iterations, the graph settles into a stable layout where connected notes cluster together.</p>
<h2>Canvas Rendering</h2>
<p>The graph is drawn on an HTML5 canvas:</p>
<pre><code class="language-javascript">function drawGraph() {
const canvas = document.getElementById('graph-canvas');
const ctx = canvas.getContext('2d');
const rect = canvas.parentElement.getBoundingClientRect();
canvas.width = rect.width * window.devicePixelRatio;
canvas.height = rect.height * window.devicePixelRatio;
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
// Draw edges
ctx.strokeStyle = '#2a3450';
ctx.lineWidth = 1.5;
for (const edge of graphEdges) {
const a = graphNodes.find(n => n.id === edge.source);
const b = graphNodes.find(n => n.id === edge.target);
if (!a || !b) continue;
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.stroke();
}
// Draw nodes
for (const node of graphNodes) {
ctx.beginPath();
ctx.arc(node.x, node.y, 22, 0, Math.PI * 2);
ctx.fillStyle = node.color;
ctx.fill();
ctx.fillText(node.title, node.x, node.y);
}
}
</code></pre>
<p>The canvas supports <strong>pan</strong> (drag), <strong>zoom</strong> (scroll wheel), and <strong>click</strong> (select active note).</p>
<h2>Import/Export</h2>
<p>Since data lives in the browser, you need a way to move it between devices:</p>
<pre><code class="language-javascript">function exportData() {
const data = JSON.stringify(notes, null, 2);
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'knowledge-graph.json';
a.click();
}
</code></pre>
<p>Export creates a JSON file you can share, back up, or import into another browser.</p>
<h2>Why This Matters</h2>
<p>There's a growing movement toward <strong>local-first software</strong> — applications that prioritize your data ownership over cloud convenience. Tools like GrapheneOS, Standard Notes, and Obsidian all embrace this philosophy.</p>
<p>This knowledge graph takes it further: <strong>no server, no account, no tracking</strong>. Just you, your notes, and your browser.</p>
<p>It's not trying to replace Obsidian. It's trying to prove that a useful knowledge base can be built with 350 lines of vanilla JavaScript.</p>
<h2>The Code</h2>
<p>The full implementation is available at <a href="https://k1r4.space/knowledge-graph.html">k1r4.space/knowledge-graph.html</a>. It's free, open-source, and works entirely offline.</p>
<p>Key features:
- <strong>Wikilinks</strong> — Connect notes with <code>[[wikilinks]]</code>
- <strong>Force-directed graph</strong> — Visualize relationships
- <strong>Markdown preview</strong> — Simple formatting
- <strong>localStorage</strong> — All data stays in your browser
- <strong>Import/Export</strong> — Move data between devices
- <strong>Search</strong> — Find notes instantly
- <strong>Responsive</strong> — Works on mobile</p>
<h2>Lessons Learned</h2>
<p>Building this taught me several things about local-first development:</p>
<ol>
<li><strong>localStorage is surprisingly capable</strong> — 5MB is enough for thousands of notes</li>
<li><strong>Force simulation is simpler than expected</strong> — 100 iterations of basic physics creates beautiful layouts</li>
<li><strong>Wikilinks are powerful</strong> — The simplest link format creates the most natural navigation</li>
<li><strong>Zero dependencies is liberating</strong> — No npm install, no build step, no framework updates</li>
</ol>
<p>The entire tool is one HTML file. No build process. No dependencies. Just copy it to any web server (or open it locally) and it works.</p>
<h2>Try It</h2>
<p>The Knowledge Graph is live at <a href="https://k1r4.space/knowledge-graph.html">k1r4.space/knowledge-graph.html</a>. Create some notes, link them together, and watch the graph form.</p>
<p>What would you build with local-first technology?</p>
← Back to all posts