Skip to content

Install Required Libraries: You'll need watchdog for monitoring file changes and Flask for serving the web application.Install these libraries using pip:

pip install watchdog flask graphviz

Certainly! Below is the complete script including the functionality for searching nodes and displaying both the heading and the filename in the node labels:

Steps - data path - markdown into headings - heading to html - flask realtime - search - ````` python import os import re import json from flask import Flask, send_from_directory from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler import threading

Path to your Obsidian vault

vault_path = '/data/data/com.termux/files/home/storage/shared/obsidian/dental'

app = Flask(name)

Function to extract headings and links from a file

def extract_headings(file_path): with open(file_path, 'r', encoding='utf-8') as file: content = file.read() headings = re.findall(r'^(#+) (.+)$', content, re.MULTILINE) links = re.findall(r'[[(.*?)]]', content) return headings, links

def generate_graph_data(): nodes = [] links = [] node_id = 0 node_map = {}

for root, dirs, files in os.walk(vault_path):
    for file in files:
        if file.endswith('.md'):
            file_path = os.path.join(root, file)
            note_name = os.path.splitext(file)[0]

            # Add a node for the filename
            filename_node_id = node_id
            nodes.append({"id": filename_node_id, "label": note_name, "group": note_name, "type": "filename"})
            node_map[note_name] = node_id
            node_id += 1

            headings, note_links = extract_headings(file_path)
            for i, (level, heading) in enumerate(headings):
                node_key = f"{note_name}#{heading}"
                if node_key not in node_map:
                    node_map[node_key] = node_id
                    nodes.append({"id": node_id, "label": heading, "group": note_name, "type": "heading"})
                    node_id += 1

                # Link headings to the filename node
                links.append({"source": filename_node_id, "target": node_map[node_key]})

                if level.count('#') > 1:
                    parent_level = level.count('#') - 1
                    parent_key = None
                    # Search for the nearest parent heading
                    for j in range(i-1, -1, -1):
                        prev_level, prev_heading = headings[j]
                        if prev_level.count('#') == parent_level:
                            parent_key = f"{note_name}#{prev_heading}"
                            break
                    if parent_key in node_map:
                        links.append({"source": node_map[parent_key], "target": node_map[node_key]})
            for link in note_links:
                if link in node_map:
                    links.append({"source": node_map[note_name], "target": node_map[link]})

# Save the data to a JSON file
graph_data = {"nodes": nodes, "links": links}
with open('graph_data.json', 'w', encoding='utf-8') as f:
    json.dump(graph_data, f)

print("Graph data saved to graph_data.json")

# HTML template with embedded JavaScript
html_content = f"""
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Obsidian Notes Graph</title>
    <style>
        body {{
            background-color: #1e1e1e;
            color: #c0c0c0;
            font-family: Arial, sans-serif;
        }}
        .node {{
            stroke: #fff;
            stroke-width: 1.5px;
            fill: #6baed6;
        }}
        .link {{
            stroke: #999;
            stroke-opacity: 0.6;
        }}
        text {{
            font-size: 12px;
            fill: #c0c0c0;
            pointer-events: none;
        }}
        .filename {{
            fill: red;
        }}
        .search-container {{
            position: fixed;
            top: 10px;
            left: 50%;
            transform: translateX(-50%);
            z-index: 10;
        }}
        .search-input {{
            padding: 5px;
            font-size: 14px;
        }}
    </style>
</head>
<body>
    <div class="search-container">
        <input type="text" id="search-input" class="search-input" placeholder="Search nodes...">
    </div>
    <script src="https://d3js.org/d3.v6.min.js"></script>
    <script>
        // Load the data
        d3.json('graph_data.json').then(function(graph) {{

            const width = window.innerWidth;
            const height = window.innerHeight;

            const svg = d3.select("body").append("svg")
                .attr("width", width)
                .attr("height", height)
                .call(d3.zoom().on("zoom", function (event) {{
                    svg.attr("transform", event.transform)
                }}))
                .append("g");

            const simulation = d3.forceSimulation(graph.nodes)
                .force("link", d3.forceLink(graph.links).id(d => d.id).distance(100))
                .force("charge", d3.forceManyBody().strength(-200))
                .force("center", d3.forceCenter(width / 2, height / 2));

            const link = svg.append("g")
                .attr("class", "links")
                .selectAll("line")
                .data(graph.links)
                .enter().append("line")
                .attr("class", "link");

            const node = svg.append("g")
                .attr("class", "nodes")
                .selectAll("circle")
                .data(graph.nodes)
                .enter().append("circle")
                .attr("class", "node")
                .attr("r", 5)
                .call(drag(simulation));

            const label = svg.append("g")
                .attr("class", "labels")
                .selectAll("text")
                .data(graph.nodes)
                .enter().append("text")
                .attr("class", "label")
                .attr("dy", -10)
                .attr("dx", 6)
                .text(d => d.label);

            const filenameLabel = svg.append("g")
                .attr("class", "filenames")
                .selectAll("text")
                .data(graph.nodes.filter(d => d.type === "filename"))
                .enter().append("text")
                .attr("class", "filename")
                .attr("dy", 10)
                .attr("dx", 6)
                .text(d => d.label);

            node.append("title")
                .text(d => d.label);

            simulation.on("tick", () => {{
                link
                    .attr("x1", d => d.source.x)
                    .attr("y1", d => d.source.y)
                    .attr("x2", d => d.target.x)
                    .attr("y2", d => d.target.y);

                node
                    .attr("cx", d => d.x)
                    .attr("cy", d => d.y);

                label
                    .attr("x", d => d.x)
                    .attr("y", d => d.y - 10);

                filenameLabel
                    .attr("x", d => d.x)
                    .attr("y", d => d.y + 10);
            }});

            function drag(simulation) {{
                function dragstarted(event, d) {{
                    if (!event.active) simulation.alphaTarget(0.3).restart();
                    d.fx = d.x;
                    d.fy = d.y;
                }}

                function dragged(event, d) {{
                    d.fx = event.x;
                    d.fy = event.y;
                }}

                function dragended(event, d) {{
                    if (!event.active) simulation.alphaTarget(0);
                    d.fx = null;
                    d.fy = null;
                }}

                return d3.drag()
                    .on("start", dragstarted)
                    .on("drag", dragged)
                    .on("end", dragended);
            }}

            // Search functionality
            document.getElementById('search-input').addEventListener('input', function(event) {{
                const searchTerm = event.target.value.toLowerCase();
                label.style('display', d => d.label.toLowerCase().includes(searchTerm) ? '' : 'none');
                filenameLabel.style('display', d => d.label.toLowerCase().includes(searchTerm) ? '' : 'none');
                node.style('display', d => d.label.toLowerCase().includes(searchTerm) ? '' : 'none');
            }});
        }});
    </script>
</body>
</html>
"""

# Save the HTML content to a file
with open('index.html', 'w', encoding='utf-8') as f:
    f.write(html_content)

print("HTML file saved to index.html")

generate_graph_data()

class ChangeHandler(FileSystemEventHandler): def on_modified(self, event): if event.src_path.endswith('.md'): print(f"Change detected in {event.src_path}. Regenerating graph data...") generate_graph_data()

@app.route('/') def index(): return send_from_directory('.', 'index.html')

@app.route('/graph_data.json') def graph_data(): return send_from_directory('.', 'graph_data.json')

if name == "main": event_handler = ChangeHandler observer = Observer() observer.schedule(event_handler, path=vault_path, recursive=True) observer_thread = threading.Thread(target=observer.start) observer_thread.start()

try:
    app.run(debug=True, use_reloader=False)
except KeyboardInterrupt:
    print("Stopping server...")
    observer.stop()

observer_thread.join()

``````

How to Run the Real-Time Interactive Visualization with Search and Dark Mode

  1. Save the Script: Save the script to a file, for example, real_time_graph_with_search.py.

  2. Run the Script: Open Termux, navigate to the directory where you saved the script, and run it: sh python r.py

  3. View the Interactive Graph: Open your browser and navigate to http://localhost:5000 to view the interactive graph. Use the search input field at the top to filter nodes by their labels.

Summary

This updated script adds a search functionality to filter nodes based on their labels and includes the filename in the node labels for better context. The visualization is in dark mode and provides a real-time interactive view of your Obsidian notes.