What Is the Weighted Graph Editor?

 

⭐ What Is the Weighted Graph Editor?

The Weighted Graph Editor is a desktop application where you can visually add nodes, draw edges, and assign weights. Unlike plain text-based tools where you must manually type data, this app gives you a user-friendly interface with buttons and dialogs.

You can:

  • Add nodes

  • Connect nodes with weighted edges

  • Edit or update existing weights

  • Delete edges

  • Delete nodes

  • Save your graph

  • Load previously saved graphs

  • Visualize the graph instantly

It makes graph creation and editing extremely fast and intuitive.


⭐ Key Features of the App

1. Add Nodes Instantly

With a simple button click, you can create new nodes. Each node is automatically added to the graph and becomes available for connections. This is useful for small experiments or building large network structures.


2. Add Weighted Edges

The app allows you to connect nodes and specify a weight.

For example:

Node ANode B (Weight = 10)

Weights are important in many algorithms like:

  • Dijkstra's shortest path

  • Minimum spanning tree

  • Network optimization

  • Cost and distance calculations

You can add any numerical weight using an input dialog.


3. View Graph Visualization in Real-Time

Every time you add or modify nodes or edges, the graph visualization updates instantly. This helps you understand the structure clearly and visually.

The app uses:

  • A spring layout

  • Clear labels for edges

  • Node labels

  • Color-coded nodes

This makes it extremely easy to identify connections and structure.


4. Edit or Update Edge Weights

If you want to change the weight between two nodes, simply use the “Edit Weight” option. This feature is perfect for testing multiple network scenarios without rebuilding the graph from scratch.


5. Delete Nodes and Edges

You can also remove nodes or connections easily. When a node is deleted, all connected edges are removed automatically. This makes the editing process smooth and efficient.


6. Save and Load Graphs

This is one of the most useful features.
You can save your graph structure to a JSON file, including:

  • Nodes

  • Edges

  • Weights

Later, you can load the file back into the app and continue editing.

It is excellent for:

  • Students preparing assignments

  • Researchers working with datasets

  • Developers testing algorithms

  • Re-using graph models


7. Simple and Lightweight Interface

The interface is built with Tkinter, so it runs on any system using Visual Studio Code. It does not require heavy software or complex setup.


⭐ Why Use This App?

This graph editor is ideal for:

✔ Students

Practice graph theory, create examples, visualize algorithms.

✔ Teachers

Demonstrate weighted graphs in classrooms easily.

✔ Data Scientists

Model and experiment with network-based datasets.

✔ Algorithm Learners

Manually create graphs to test Dijkstra, Prim, Kruskal, Bellman-Ford, etc.

✔ Developers

Design and test graph logic without manually writing adjacency lists or matrices.


⭐ Final Thoughts

The Weighted Graph Editor Desktop App is a complete and powerful tool for creating, editing, and saving weighted graphs. Its simplicity, visualization features, and real-time updates make it perfect for academic use, algorithm practice, and research.

https://github.com/gagandeep44489/DiscreteStrucutreAndAlgoApp/blob/main/Weighted%20Graph%20Editor.py

#!/usr/bin/env python3

"""

Weighted Graph Editor - single-file desktop app.


Features:

- Create and edit weighted graphs (directed or undirected)

- Add / remove nodes and edges, edit weights

- Load / Save edge-list CSV (source,target,weight)

- Optional GraphML save/load (if using networkx)

- Visualize the graph with edge weights shown

- Export adjacency matrix to CSV

- Simple Tkinter UI suitable for running in VS Code


Run: python weighted_graph_editor.py

"""


import tkinter as tk

from tkinter import ttk, filedialog, messagebox, simpledialog

import networkx as nx

import pandas as pd

import matplotlib.pyplot as plt

from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

import os

import io

import math


# ---------- Helper utilities ----------

def safe_float(x, default=1.0):

    try:

        return float(x)

    except Exception:

        return default


# ---------- Main App ----------

class WeightedGraphEditor(tk.Tk):

    def __init__(self):

        super().__init__()

        self.title("Weighted Graph Editor")

        self.geometry("1120x720")

        self.minsize(900, 600)


        # Graph: directed by default = False

        self.directed_var = tk.BooleanVar(value=False)

        self.G = nx.Graph()  # will switch to DiGraph when directed_var True

        self.current_file = None

        self.adj_df = None


        self._build_ui()

        self._build_plot_area()

        self._refresh_lists()

        self.log("App started.")


    # ---------- UI ----------

    def _build_ui(self):

        top = ttk.Frame(self, padding=6)

        top.pack(side=tk.TOP, fill=tk.X)


        ttk.Button(top, text="New Graph", command=self.new_graph).pack(side=tk.LEFT, padx=4)

        ttk.Button(top, text="Load CSV", command=self.load_csv).pack(side=tk.LEFT, padx=4)

        ttk.Button(top, text="Save CSV", command=self.save_csv).pack(side=tk.LEFT, padx=4)

        ttk.Button(top, text="Load GraphML", command=self.load_graphml).pack(side=tk.LEFT, padx=4)

        ttk.Button(top, text="Save GraphML", command=self.save_graphml).pack(side=tk.LEFT, padx=4)


        ttk.Separator(top, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=6)

        chk = ttk.Checkbutton(top, text="Directed", variable=self.directed_var, command=self.toggle_directed)

        chk.pack(side=tk.LEFT, padx=4)

        ttk.Label(top, text="  ").pack(side=tk.LEFT, padx=6)


        ttk.Button(top, text="Add Node", command=self.add_node_dialog).pack(side=tk.LEFT, padx=4)

        ttk.Button(top, text="Remove Node", command=self.remove_node).pack(side=tk.LEFT, padx=4)

        ttk.Button(top, text="Add/Edit Edge", command=self.add_edge_dialog).pack(side=tk.LEFT, padx=4)

        ttk.Button(top, text="Remove Edge", command=self.remove_edge).pack(side=tk.LEFT, padx=4)


        ttk.Button(top, text="Build Adjacency Matrix", command=self.build_adj_matrix).pack(side=tk.LEFT, padx=8)

        ttk.Button(top, text="Export Matrix CSV", command=self.export_matrix).pack(side=tk.LEFT, padx=4)


        # Main split: left = controls/lists, right = plot

        main = ttk.Frame(self)

        main.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=6, pady=6)


        left = ttk.Frame(main, width=380)

        left.pack(side=tk.LEFT, fill=tk.Y)


        # Node list

        ttk.Label(left, text="Nodes").pack(anchor=tk.NW)

        self.node_listbox = tk.Listbox(left, height=12, exportselection=False)

        self.node_listbox.pack(fill=tk.X, padx=2, pady=2)

        self.node_listbox.bind('<<ListboxSelect>>', lambda e: self.on_node_select())


        # Edge list

        ttk.Label(left, text="Edges (u -> v : weight)").pack(anchor=tk.NW, pady=(8,0))

        self.edge_listbox = tk.Listbox(left, height=12, exportselection=False)

        self.edge_listbox.pack(fill=tk.X, padx=2, pady=2)

        self.edge_listbox.bind('<<ListboxSelect>>', lambda e: self.on_edge_select())


        # Quick controls for selected node/edge

        control_frame = ttk.Frame(left)

        control_frame.pack(fill=tk.X, pady=(8,0))


        ttk.Button(control_frame, text="Center on Node", command=self.center_on_node).pack(side=tk.LEFT, padx=4)

        ttk.Button(control_frame, text="Highlight Edge", command=self.highlight_edge).pack(side=tk.LEFT, padx=4)


        # Info & log

        ttk.Label(left, text="Graph Info").pack(anchor=tk.NW, pady=(8,0))

        self.info_text = tk.Text(left, height=6, wrap=tk.WORD)

        self.info_text.pack(fill=tk.X, padx=2, pady=2)


        ttk.Label(left, text="Log").pack(anchor=tk.NW, pady=(8,0))

        self.log_text = tk.Text(left, height=8, wrap=tk.WORD)

        self.log_text.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)


    # ---------- Plot area ----------

    def _build_plot_area(self):

        right = ttk.Frame(self)

        right.place(x=400, y=70, relwidth=0.6, relheight=0.82)


        # Matplotlib figure

        self.fig, self.ax = plt.subplots(figsize=(6,6))

        plt.tight_layout()

        self.canvas = FigureCanvasTkAgg(self.fig, master=right)

        self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)


        # small controls

        ctrl = ttk.Frame(right)

        ctrl.pack(fill=tk.X)

        ttk.Button(ctrl, text="Redraw", command=self.redraw).pack(side=tk.LEFT, padx=4, pady=4)

        ttk.Button(ctrl, text="Toggle Labels", command=self.toggle_labels).pack(side=tk.LEFT, padx=4)

        self.show_labels = True


        self._pos = None  # store last layout positions

        self._highlight = None  # (u,v) tuple to highlight


    # ---------- Graph operations ----------

    def new_graph(self):

        self.G = nx.DiGraph() if self.directed_var.get() else nx.Graph()

        self.current_file = None

        self._pos = None

        self._highlight = None

        self._refresh_lists()

        self.log("Created new graph.")


    def toggle_directed(self):

        # convert graph type

        directed = self.directed_var.get()

        if directed and not isinstance(self.G, nx.DiGraph):

            self.G = nx.DiGraph(self.G)  # convert simple

            self.log("Converted graph to Directed.")

        elif not directed and isinstance(self.G, nx.DiGraph):

            self.G = nx.Graph(self.G)

            self.log("Converted graph to Undirected.")

        self._refresh_lists()

        self.redraw()


    def add_node_dialog(self):

        node = simpledialog.askstring("Add Node", "Enter node label (string):", parent=self)

        if node:

            node = str(node).strip()

            if node in self.G:

                messagebox.showinfo("Exists", f"Node '{node}' already exists.")

                return

            self.G.add_node(node)

            self.log(f"Added node: {node}")

            self._refresh_lists()

            self.redraw()


    def remove_node(self):

        sel = self._get_selected(self.node_listbox)

        if sel is None:

            messagebox.showinfo("Select", "Select a node to remove.")

            return

        node = sel

        if messagebox.askyesno("Confirm", f"Remove node '{node}' and its edges?"):

            self.G.remove_node(node)

            self.log(f"Removed node: {node}")

            self._refresh_lists()

            self.redraw()


    def add_edge_dialog(self):

        # dialog for source, target and weight

        u = simpledialog.askstring("Edge - source", "Enter source node:", parent=self)

        if u is None: return

        v = simpledialog.askstring("Edge - target", "Enter target node:", parent=self)

        if v is None: return

        w = simpledialog.askstring("Edge - weight", "Enter weight (number):", initialvalue="1.0", parent=self)

        if w is None: return

        u = str(u).strip(); v = str(v).strip()

        weight = safe_float(w, 1.0)

        if u == "" or v == "":

            messagebox.showwarning("Input", "Source and target cannot be empty.")

            return

        # auto-add nodes if missing

        if u not in self.G:

            self.G.add_node(u); self.log(f"Auto-added node: {u}")

        if v not in self.G:

            self.G.add_node(v); self.log(f"Auto-added node: {v}")

        self.G.add_edge(u, v, weight=weight)

        self.log(f"Added/Updated edge: {u} -> {v} (weight={weight})")

        self._refresh_lists()

        self.redraw()


    def remove_edge(self):

        sel = self.edge_listbox.curselection()

        if not sel:

            messagebox.showinfo("Select", "Select an edge to remove.")

            return

        idx = sel[0]

        text = self.edge_listbox.get(idx)

        # parse u,v from displayed text

        try:

            u_v = text.split(':')[0].strip()

            if '->' in u_v:

                u, v = [s.strip() for s in u_v.split('->')]

            elif '--' in u_v:

                u, v = [s.strip() for s in u_v.split('--')]

            else:

                parts = u_v.split()

                u, v = parts[0], parts[1]

        except Exception:

            messagebox.showerror("Parse error", "Failed to parse edge selection.")

            return

        if messagebox.askyesno("Confirm", f"Remove edge {u} -> {v}?"):

            if self.G.has_edge(u, v):

                self.G.remove_edge(u, v)

                self.log(f"Removed edge: {u} -> {v}")

            else:

                self.log(f"Edge not found: {u} -> {v}")

            self._refresh_lists()

            self.redraw()


    # ---------- Load/Save ----------

    def load_csv(self):

        path = filedialog.askopenfilename(filetypes=[("CSV files","*.csv"), ("All files","*.*")])

        if not path:

            return

        try:

            df = pd.read_csv(path)

        except Exception as e:

            messagebox.showerror("Read error", f"Failed to read CSV: {e}")

            return

        cols = [c.lower() for c in df.columns]

        # detect columns

        src = None; tgt = None; wt = None

        for c in df.columns:

            lc = c.lower()

            if 'source' in lc or 'from' in lc or lc in ('u','node1','a'):

                src = c; break

        for c in df.columns:

            lc = c.lower()

            if 'target' in lc or 'to' in lc or lc in ('v','node2','b'):

                tgt = c; break

        for c in df.columns:

            if 'weight' in c.lower() or 'w' == c.lower():

                wt = c; break

        if src is None or tgt is None:

            # fallback to first two columns

            if len(df.columns) >= 2:

                src, tgt = df.columns[0], df.columns[1]

            else:

                messagebox.showerror("Format", "CSV must contain at least two columns: source, target")

                return

        # Build graph

        directed = self.directed_var.get()

        self.G = nx.DiGraph() if directed else nx.Graph()

        for _, r in df.iterrows():

            u = str(r[src]); v = str(r[tgt])

            if wt is not None and wt in df.columns:

                w = safe_float(r[wt], 1.0)

            else:

                w = 1.0

            if u == "" or v == "":

                continue

            self.G.add_node(u); self.G.add_node(v)

            self.G.add_edge(u, v, weight=w)

        self.current_file = path

        self.log(f"Loaded CSV: {os.path.basename(path)} ({len(self.G.nodes())} nodes, {len(self.G.edges())} edges)")

        self._refresh_lists()

        self.redraw()


    def save_csv(self):

        if self.G is None or len(self.G.nodes()) == 0:

            messagebox.showinfo("Empty", "Graph is empty.")

            return

        path = filedialog.asksaveasfilename(defaultextension=".csv", filetypes=[("CSV files","*.csv")])

        if not path:

            return

        rows = []

        for u,v,data in self.G.edges(data=True):

            w = data.get('weight', 1.0)

            rows.append((u,v,w))

        df = pd.DataFrame(rows, columns=['source','target','weight'])

        try:

            df.to_csv(path, index=False)

            self.log(f"Saved CSV: {path}")

            messagebox.showinfo("Saved", f"Saved to: {path}")

        except Exception as e:

            messagebox.showerror("Save error", f"Failed to save: {e}")


    def load_graphml(self):

        path = filedialog.askopenfilename(filetypes=[("GraphML files","*.graphml"), ("All files","*.*")])

        if not path:

            return

        try:

            G = nx.read_graphml(path)

            # convert nodes to str and ensure weight attribute present

            G2 = nx.DiGraph() if self.directed_var.get() else nx.Graph()

            for n,d in G.nodes(data=True):

                G2.add_node(str(n), **d)

            for u,v,d in G.edges(data=True):

                w = d.get('weight', 1.0)

                try:

                    w = float(w)

                except Exception:

                    w = 1.0

                G2.add_edge(str(u), str(v), weight=w)

            self.G = G2

            self.log(f"Loaded GraphML: {os.path.basename(path)}")

            self._refresh_lists()

            self.redraw()

        except Exception as e:

            messagebox.showerror("Read error", f"Failed to read GraphML: {e}")


    def save_graphml(self):

        path = filedialog.asksaveasfilename(defaultextension=".graphml", filetypes=[("GraphML files","*.graphml")])

        if not path:

            return

        try:

            nx.write_graphml(self.G, path)

            self.log(f"Saved GraphML: {path}")

            messagebox.showinfo("Saved", f"Saved to: {path}")

        except Exception as e:

            messagebox.showerror("Save error", f"Failed to save GraphML: {e}")


    # ---------- Lists & UI refresh ----------

    def _refresh_lists(self):

        # refresh node listbox

        self.node_listbox.delete(0, tk.END)

        for n in sorted(self.G.nodes()):

            self.node_listbox.insert(tk.END, str(n))

        # refresh edge listbox

        self.edge_listbox.delete(0, tk.END)

        for u,v,data in sorted(self.G.edges(data=True)):

            w = data.get('weight', 1.0)

            arrow = '->' if self.directed_var.get() else '--'

            self.edge_listbox.insert(tk.END, f"{u} {arrow} {v} : {w}")

        # update info

        self._update_info()


    def _update_info(self):

        buf = io.StringIO()

        buf.write(f"Nodes: {len(self.G.nodes())}\n")

        buf.write(f"Edges: {len(self.G.edges())}\n")

        # basic stats

        degs = [d for _,d in self.G.degree(weight='weight')]

        if degs:

            buf.write(f"Avg degree (weighted sum): {sum(degs)/len(degs):.3f}\n")

        else:

            buf.write("Avg degree: 0\n")

        self.info_text.delete(1.0, tk.END)

        self.info_text.insert(tk.END, buf.getvalue())


    # ---------- Selection helpers ----------

    def _get_selected(self, listbox):

        sel = listbox.curselection()

        if not sel:

            return None

        return listbox.get(sel[0])


    def on_node_select(self):

        sel = self.node_listbox.curselection()

        if not sel:

            return

        node = self.node_listbox.get(sel[0])

        # show neighbors or details

        neighbors = list(self.G.neighbors(node)) if node in self.G else []

        self.log(f"Selected node: {node} (neighbors: {len(neighbors)})")

        # center view on node

        self.center_on_node(node)


    def on_edge_select(self):

        sel = self.edge_listbox.curselection()

        if not sel:

            return

        text = self.edge_listbox.get(sel[0])

        self.log(f"Selected edge: {text}")


    # ---------- Visualization ----------

    def redraw(self):

        self.ax.clear()

        if self.G is None or len(self.G.nodes()) == 0:

            self.ax.set_title("No graph loaded")

            self.canvas.draw()

            return

        # compute layout (re-use positions if present)

        if self._pos is None or len(self._pos) != len(self.G):

            self._pos = nx.spring_layout(self.G, seed=42)

        pos = self._pos

        # draw edges

        weights = [self.G[u][v].get('weight',1.0) for u,v in self.G.edges()]

        # normalize widths

        widths = []

        if weights:

            minw = min(weights); maxw = max(weights)

            for w in weights:

                if math.isclose(minw, maxw):

                    widths.append(1.5)

                else:

                    widths.append(0.5 + 3.5*(w - minw)/(maxw - minw))

        else:

            widths = 1

        nx.draw_networkx_edges(self.G, pos, ax=self.ax, width=widths, alpha=0.7)

        # draw nodes

        deg = dict(self.G.degree(weight='weight'))

        node_sizes = [100 + 60*deg.get(n,0) for n in self.G.nodes()]

        nx.draw_networkx_nodes(self.G, pos, ax=self.ax, node_size=node_sizes)

        # labels

        if self.show_labels:

            nx.draw_networkx_labels(self.G, pos, ax=self.ax, font_size=9)

        # edge labels (weights)

        edge_labels = {(u,v): f"{self.G[u][v].get('weight',1.0):.2f}" for u,v in self.G.edges()}

        nx.draw_networkx_edge_labels(self.G, pos, edge_labels=edge_labels, ax=self.ax, font_size=8)

        # highlight if requested

        if self._highlight:

            u,v = self._highlight

            if self.G.has_edge(u,v):

                nx.draw_networkx_edges(self.G, pos, edgelist=[(u,v)], ax=self.ax, edge_color='red', width=4.0)

        self.ax.set_axis_off()

        self.canvas.draw()

        self._update_info()


    def toggle_labels(self):

        self.show_labels = not self.show_labels

        self.redraw()


    def center_on_node(self, node_label=None):

        if node_label is None:

            sel = self.node_listbox.curselection()

            if not sel:

                messagebox.showinfo("Select", "Select a node first.")

                return

            node_label = self.node_listbox.get(sel[0])

        # recompute layout with that node centered by using spring with fixed initial pos

        try:

            self._pos = nx.spring_layout(self.G, center=(0,0), seed=42)

            # small trick: nudge positions so chosen node is near center (not perfect but helps)

            if node_label in self._pos:

                cx, cy = self._pos[node_label]

                shiftx, shifty = -cx, -cy

                for k in self._pos:

                    x,y = self._pos[k]

                    self._pos[k] = (x + shiftx, y + shifty)

            self.redraw()

            self.log(f"Centered on node: {node_label}")

        except Exception as e:

            self.log(f"Center failed: {e}")


    def highlight_edge(self):

        sel = self.edge_listbox.curselection()

        if not sel:

            messagebox.showinfo("Select", "Select an edge to highlight.")

            return

        text = self.edge_listbox.get(sel[0])

        try:

            u_v = text.split(':')[0].strip()

            if '->' in u_v:

                u,v = [s.strip() for s in u_v.split('->')]

            elif '--' in u_v:

                u,v = [s.strip() for s in u_v.split('--')]

            else:

                parts = u_v.split()

                u, v = parts[0], parts[1]

            if self.G.has_edge(u,v):

                self._highlight = (u,v)

                self.log(f"Highlighting edge: {u} -> {v}")

                self.redraw()

            else:

                messagebox.showinfo("Not found", "Edge not found in graph.")

        except Exception:

            messagebox.showerror("Parse error", "Failed to parse selected edge.")


    # ---------- Adjacency matrix ----------

    def build_adj_matrix(self):

        if self.G is None or len(self.G.nodes()) == 0:

            messagebox.showinfo("Empty", "Graph is empty.")

            return

        nodes = list(self.G.nodes())

        mat = pd.DataFrame(0.0, index=nodes, columns=nodes)

        for u,v,data in self.G.edges(data=True):

            w = data.get('weight', 1.0)

            mat.at[u, v] = w

            if not isinstance(self.G, nx.DiGraph):

                mat.at[v, u] = w

        self.adj_df = mat

        # show a small preview in a dialog or saveable

        preview = io.StringIO()

        preview.write(f"Adjacency matrix ({mat.shape[0]}x{mat.shape[1]}) created.\n")

        preview.write(mat.head(10).to_string())

        messagebox.showinfo("Matrix Built", f"Adjacency matrix built. Preview printed to log.")

        self.log(preview.getvalue())


    def export_matrix(self):

        if self.adj_df is None:

            messagebox.showinfo("No matrix", "Build the adjacency matrix first.")

            return

        path = filedialog.asksaveasfilename(defaultextension=".csv", filetypes=[("CSV files","*.csv")])

        if not path:

            return

        try:

            self.adj_df.to_csv(path)

            self.log(f"Exported adjacency matrix: {path}")

            messagebox.showinfo("Saved", f"Saved adjacency matrix to {path}")

        except Exception as e:

            messagebox.showerror("Export error", f"Failed to export matrix: {e}")


    # ---------- Logging ----------

    def log(self, text):

        self.log_text.insert(tk.END, f"{text}\n")

        self.log_text.see(tk.END)


# ---------- Run ----------

if __name__ == "__main__":

    app = WeightedGraphEditor()

    app.mainloop()


Comments

Popular posts from this blog

NAND / NOR Logic Simulator: A Python Desktop App for Understanding Universal Logic Gates

Subset Sum Problem Visualizer Using Python (Dynamic Programming GUI Tool)

String Matching Algorithm Trainer (KMP & Rabin-Karp) – Python Desktop App