Adjacency Matrix Generator – A Powerful Desktop App for Graph Analysis

 

Adjacency Matrix Generator – A Powerful Desktop App for Graph Analysis

Understanding the structure of a network is one of the most important tasks in graph theory, data science, and computer science. Whether you are analyzing social networks, mapping connections in a dataset, exploring relationships in biological systems, or working on academic graph projects, an adjacency matrix provides a clear and systematic representation of how nodes are connected.

To make this process simple, fast, and user-friendly, I created a desktop-based Adjacency Matrix Generator. This app is designed for students, researchers, developers, and anyone who works with graphs but wants to avoid manual calculations or complex scripts. The tool provides automation, visualization, and easy export options — all inside a clean graphical interface.


⭐ What Is an Adjacency Matrix?

An adjacency matrix is a square table that shows which nodes in a graph are connected to each other.

  • Rows represent nodes

  • Columns represent nodes

  • Values represent connections (1 for an edge, 0 for no edge)

  • Weighted graphs store actual numerical weights instead of 1 or 0

This matrix is extremely helpful in analyzing graph behavior, finding patterns, and performing mathematical operations on networks.


⭐ What This Desktop App Can Do

This Adjacency Matrix Generator is not just a simple table builder — it’s a complete graph analysis tool packed with powerful features:

1. Load Edge-List CSV Files Easily

You can load any CSV file containing two or three columns such as:

  • source

  • target

  • weight (optional)

The app automatically detects column names and organizes them into a usable format.


2. Supports Both Directed and Undirected Graphs

Simply check the “Directed” option if you are working with:

  • flow networks

  • one-direction relationships

  • dependency chains

Otherwise, keep it unchecked for undirected graphs like:

  • social networks

  • road networks

  • undirected data relationships


3. Supports Weighted and Unweighted Graphs

If your dataset has a weight column, the app will generate a weighted adjacency matrix.
If not, it automatically creates a binary connection matrix.


4. Generates the Complete Adjacency Matrix Automatically

Once the CSV is loaded, the app instantly builds:

  • A full adjacency matrix in table format

  • A preview of the top results

  • Summary statistics such as number of nodes and nonzero values

It saves a lot of time compared to doing all of this manually.


5. Graph Visualization with NetworkX

The app includes a built-in graph drawing feature using a spring layout. You can visually see:

  • How nodes are positioned

  • How they connect

  • The density of relationships

  • Clusters or isolated nodes

This is especially useful for presentations, research papers, and data exploration.


6. Heatmap Visualization of the Matrix

Graphs with many nodes often produce large matrices.
To make interpretation easier, the app generates a heatmap where:

  • Darker colors show stronger or more frequent connections

  • Lighter colors represent weaker or no connections

This gives you an immediate understanding of graph structure.


7. Export Options for Sharing and Analysis

You can export:

  • Adjacency Matrix as CSV for Excel, Python, or machine learning tools

  • Heatmap as PNG for reports, articles, or blog posts

Everything is generated with a single click.


8. Extra Options for Node Ordering

If you want a fixed order of nodes (e.g., alphabetical or custom sequence), you can load a separate node list file.
This is very useful when comparing multiple graphs or datasets.


⭐ Why This App Is Useful

This tool is extremely helpful for:

✔ Students

Working on graph theory assignments, projects, or research.

✔ Data Scientists

Exploring hidden relationships and cluster patterns in datasets.

✔ Researchers

Analyzing network structures in biology, sociology, or physics.

✔ Developers

Building graph-based applications or conducting network tests.

✔ Machine Learning Engineers

Preparing adjacency matrices for:

  • Graph Neural Networks (GNNs)

  • Knowledge graph analysis

  • Recommendation systems

The app saves hours of manual work and removes the need for writing complex code every time.


⭐ A Smooth and Beginner-Friendly Experience

The interface is simple and clean:

  • Load data

  • Choose your options

  • Generate matrix

  • Visualize

  • Export

No programming knowledge required.
Everything runs locally on your computer, making it fast, secure, and offline-friendly.


⭐ Final Thoughts

The Adjacency Matrix Generator Desktop App is a complete solution for working with graph datasets. It automates matrix creation, enables graph visualization, and makes exporting easier than ever. If you regularly deal with networks or node-based relationships, this app will quickly become one of your most valuable tools.

#!/usr/bin/env python3

"""

Adjacency Matrix Generator - single-file desktop app.


Features:

- Load edge-list CSV (columns: source,target[,weight])

- Option: treat graph as directed or undirected

- Build adjacency matrix (weighted or binary)

- Display matrix in a table (Treeview) and as a heatmap

- Visualize graph (NetworkX) with spring layout

- Export matrix to CSV and heatmap to PNG


Run: python adj_matrix_generator.py

"""


import tkinter as tk

from tkinter import ttk, filedialog, messagebox

import pandas as pd

import numpy as np

import networkx as nx

import matplotlib.pyplot as plt

from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

import os

import io


plt.rcParams.update({"figure.autolayout": True})


class AdjacencyMatrixApp(tk.Tk):

    def __init__(self):

        super().__init__()

        self.title("Adjacency Matrix Generator")

        self.geometry("1100x720")

        self.minsize(900, 600)


        self.df_edges = None   # original edge dataframe

        self.adj_df = None     # pandas adjacency matrix

        self.G = None          # networkx graph


        self._make_widgets()

        self._make_plot_area()


    def _make_widgets(self):

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

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


        btn_load = ttk.Button(top, text="Load Edge CSV", command=self.load_edge_csv)

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


        btn_load_nodes = ttk.Button(top, text="Load Node List (optional)", command=self.load_node_list)

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


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

        chk_directed = ttk.Checkbutton(top, text="Directed", variable=self.directed_var)

        chk_directed.pack(side=tk.LEFT, padx=8)


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

        chk_weighted = ttk.Checkbutton(top, text="Weighted (use 'weight' col)", variable=self.weighted_var)

        chk_weighted.pack(side=tk.LEFT, padx=8)


        btn_build = ttk.Button(top, text="Build Matrix", command=self.build_matrix)

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


        btn_export = ttk.Button(top, text="Export Matrix CSV", command=self.export_matrix)

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


        btn_export_png = ttk.Button(top, text="Export Heatmap PNG", command=self.export_heatmap)

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


        btn_clear = ttk.Button(top, text="Clear", command=self.clear_all)

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


        info = ttk.Label(top, text="CSV format: source,target[,weight]. Header optional. Node list: one node per line.")

        info.pack(side=tk.LEFT, padx=12)


        # Bottom split: left = plot, right = table + log

        bottom = ttk.Frame(self)

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


        # Plot area

        self.plot_frame = ttk.Frame(bottom, relief=tk.SUNKEN)

        self.plot_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)


        # Right frame

        right = ttk.Frame(bottom, width=420)

        right.pack(side=tk.RIGHT, fill=tk.Y)


        # Treeview for adjacency matrix

        mat_label = ttk.Label(right, text="Adjacency Matrix (preview)")

        mat_label.pack(anchor=tk.NW)

        self.tree = ttk.Treeview(right, show="headings")

        self.tree.pack(fill=tk.BOTH, expand=True, pady=(4,8))


        # Row count / info

        self.info_text = tk.Text(right, height=8, wrap=tk.WORD)

        self.info_text.pack(fill=tk.BOTH, expand=False)


        # Log

        log_label = ttk.Label(right, text="Log")

        log_label.pack(anchor=tk.NW, pady=(8,0))

        self.log_text = tk.Text(right, height=6, wrap=tk.WORD)

        self.log_text.pack(fill=tk.BOTH, expand=False)


    def _make_plot_area(self):

        # create matplotlib figures: one for network visualization, one for heatmap

        self.fig_net, self.ax_net = plt.subplots(figsize=(6,5))

        self.fig_heat, self.ax_heat = plt.subplots(figsize=(6,5))

        # use a single canvas that we'll swap between figures

        self.canvas = FigureCanvasTkAgg(self.fig_net, master=self.plot_frame)

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


        # small control to switch view

        ctrl = ttk.Frame(self.plot_frame)

        ctrl.pack(fill=tk.X)

        btn_net = ttk.Button(ctrl, text="Show Graph", command=self.show_network)

        btn_net.pack(side=tk.LEFT, padx=2, pady=4)

        btn_heat = ttk.Button(ctrl, text="Show Heatmap", command=self.show_heatmap)

        btn_heat.pack(side=tk.LEFT, padx=2, pady=4)

        btn_zoom = ttk.Button(ctrl, text="Redraw", command=self.redraw_current)

        btn_zoom.pack(side=tk.LEFT, padx=2, pady=4)


        self._current_view = "net"


    def load_edge_csv(self):

        path = filedialog.askopenfilename(title="Select edge-list CSV", 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


        # normalize columns

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

        if len(cols) < 2:

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

            return


        # Try to detect source/target column names

        source_col = None

        target_col = None

        for c in df.columns:

            lc = c.lower()

            if 'source' in lc or 'from' in lc or lc == 'u' or lc == 'node1':

                source_col = c

                break

        for c in df.columns:

            lc = c.lower()

            if 'target' in lc or 'to' in lc or lc == 'v' or lc == 'node2':

                target_col = c

                break

        if source_col is None or target_col is None:

            # fallback to first two columns

            source_col, target_col = df.columns[0], df.columns[1]


        # If weight column exists and user will choose weighted, we accept 'weight' column if present

        weight_col = None

        for c in df.columns:

            if 'weight' in c.lower():

                weight_col = c

                break


        # store in expected format

        if weight_col is not None:

            df = df[[source_col, target_col, weight_col]].rename(columns={source_col:'source', target_col:'target', weight_col:'weight'})

        else:

            df = df[[source_col, target_col]].rename(columns={source_col:'source', target_col:'target'})


        # cast to string for nodes

        df['source'] = df['source'].astype(str)

        df['target'] = df['target'].astype(str)

        self.df_edges = df

        self.log(f"Loaded edges: {len(df)} rows from {os.path.basename(path)}. Columns used: {list(df.columns)}")

        # Auto-check weighted if weight column present

        if 'weight' in df.columns:

            self.weighted_var.set(True)

        self.build_matrix()


    def load_node_list(self):

        path = filedialog.askopenfilename(title="Select node list (one node per line)", filetypes=[("Text files","*.txt"),("All files","*.*")])

        if not path:

            return

        try:

            with open(path, 'r', encoding='utf-8') as f:

                nodes = [line.strip() for line in f if line.strip()]

            if not nodes:

                messagebox.showwarning("Empty file", "Node list file is empty.")

                return

            # If there is an edge dataframe, ensure nodes are included (this file just provides ordering/all nodes)

            if self.df_edges is None:

                # create empty edges DF to be filled later

                self.df_edges = pd.DataFrame(columns=['source','target'])

            # Save nodes ordering in a special attribute

            self._node_list = nodes

            self.log(f"Loaded node list with {len(nodes)} nodes from {os.path.basename(path)}")

            self.build_matrix()

        except Exception as e:

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


    def build_matrix(self):

        if self.df_edges is None or self.df_edges.empty:

            messagebox.showinfo("No data", "Please load an edge CSV first.")

            return


        weighted = self.weighted_var.get()

        directed = self.directed_var.get()


        # Determine node set: from edge list and optionally from provided node list

        nodes = pd.unique(self.df_edges[['source','target']].values.ravel())

        # If user provided node list earlier, use that ordering and include missing nodes

        node_list = getattr(self, '_node_list', None)

        if node_list:

            # include any nodes from edges not in node_list and append them

            extras = [n for n in nodes if n not in node_list]

            nodes_ordered = list(node_list) + extras

        else:

            nodes_ordered = sorted(nodes.astype(str).tolist(), key=lambda x: str(x))


        # create adjacency matrix

        n = len(nodes_ordered)

        mat = pd.DataFrame(0, index=nodes_ordered, columns=nodes_ordered, dtype=float)


        # fill matrix

        if 'weight' in self.df_edges.columns and weighted:

            for _, row in self.df_edges.iterrows():

                s = str(row['source'])

                t = str(row['target'])

                try:

                    w = float(row['weight'])

                except Exception:

                    w = 1.0

                if s not in mat.index or t not in mat.columns:

                    continue

                mat.at[s, t] += w

                if not directed:

                    mat.at[t, s] += w

        else:

            # binary edges

            for _, row in self.df_edges.iterrows():

                s = str(row['source'])

                t = str(row['target'])

                if s not in mat.index or t not in mat.columns:

                    continue

                mat.at[s, t] = 1

                if not directed:

                    mat.at[t, s] = 1


        self.adj_df = mat

        self.log(f"Adjacency matrix built: {n} nodes, directed={directed}, weighted={weighted}")

        self._update_table_preview()

        self._build_networkx_graph()

        self.show_heatmap()


    def _update_table_preview(self, max_preview=50):

        # Clear existing tree

        for i in self.tree.get_children():

            self.tree.delete(i)

        if self.adj_df is None:

            return

        df = self.adj_df.copy()

        # For very large matrices, show only top-left max_preview x max_preview

        if df.shape[0] > max_preview:

            show_idx = df.index[:max_preview]

            dfp = df.loc[show_idx, df.columns[:max_preview]]

            truncated = True

        else:

            dfp = df

            truncated = False


        cols = ['node'] + list(dfp.columns)

        self.tree["columns"] = cols

        for c in cols:

            self.tree.heading(c, text=str(c))

            # small width; allow horizontal scroll

            self.tree.column(c, width=80, anchor=tk.CENTER)


        # insert rows

        for idx, row in dfp.iterrows():

            vs = [str(idx)] + [str(row[c]) for c in dfp.columns]

            self.tree.insert("", tk.END, values=vs)


        if truncated:

            self.log(f"Preview truncated to {max_preview}x{max_preview}. Full matrix still available to export.")

        else:

            self.log("Preview updated.")


        # update info text

        info = io.StringIO()

        info.write(f"Matrix shape: {self.adj_df.shape}\n")

        info.write(f"Nodes: {len(self.adj_df.index)}\n")

        info.write(f"Total edges (non-zero entries): {int((self.adj_df != 0).sum().sum())}\n")

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

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


    def _build_networkx_graph(self):

        if self.adj_df is None:

            return

        directed = self.directed_var.get()

        if directed:

            G = nx.DiGraph()

        else:

            G = nx.Graph()

        # add nodes

        G.add_nodes_from(self.adj_df.index.tolist())

        # add edges with weights if >0

        for u in self.adj_df.index:

            for v in self.adj_df.columns:

                val = self.adj_df.at[u, v]

                if val != 0:

                    if val == 1:

                        G.add_edge(u, v)

                    else:

                        G.add_edge(u, v, weight=float(val))

        self.G = G

        self.log(f"NetworkX graph created: nodes={G.number_of_nodes()}, edges={G.number_of_edges()}")


    def show_network(self):

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

            messagebox.showinfo("No graph", "Build matrix first.")

            return

        self._current_view = "net"

        self.fig_net.clf()

        self.ax_net = self.fig_net.subplots()

        self.ax_net.set_title("Graph Visualization (spring layout)")

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

        # node sizes by degree

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

        node_sizes = [50 + 30*deg[n] for n in self.G.nodes()]

        nx.draw_networkx_edges(self.G, pos, ax=self.ax_net, alpha=0.4)

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

        # labels small to avoid clutter

        nx.draw_networkx_labels(self.G, pos, font_size=7)

        self.ax_net.set_axis_off()

        self.canvas.figure = self.fig_net

        self.canvas.draw()


    def show_heatmap(self):

        if self.adj_df is None:

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

            return

        self._current_view = "heat"

        self.fig_heat.clf()

        self.ax_heat = self.fig_heat.subplots()

        self.ax_heat.set_title("Adjacency Matrix Heatmap")

        mat = self.adj_df.values.astype(float)

        # If huge, sample or use imshow with interpolation

        im = self.ax_heat.imshow(mat, aspect='auto', interpolation='nearest')

        # ticks only if small

        n = mat.shape[0]

        if n <= 60:

            self.ax_heat.set_xticks(np.arange(n))

            self.ax_heat.set_yticks(np.arange(n))

            self.ax_heat.set_xticklabels(self.adj_df.columns, rotation=90, fontsize=7)

            self.ax_heat.set_yticklabels(self.adj_df.index, fontsize=7)

        else:

            self.ax_heat.set_xticks([])

            self.ax_heat.set_yticks([])

        self.fig_heat.colorbar(im, ax=self.ax_heat, fraction=0.046, pad=0.04)

        self.canvas.figure = self.fig_heat

        self.canvas.draw()


    def redraw_current(self):

        if self._current_view == "net":

            self.show_network()

        else:

            self.show_heatmap()


    def export_matrix(self):

        if self.adj_df is None:

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

            return

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

        if not path:

            return

        try:

            self.adj_df.to_csv(path)

            messagebox.showinfo("Exported", f"Matrix exported to:\n{path}")

            self.log(f"Matrix exported: {path}")

        except Exception as e:

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


    def export_heatmap(self):

        if self.adj_df is None:

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

            return

        path = filedialog.asksaveasfilename(defaultextension=".png", filetypes=[("PNG image","*.png")])

        if not path:

            return

        try:

            # ensure heatmap is drawn large and saved

            self.fig_heat.savefig(path, dpi=200)

            messagebox.showinfo("Exported", f"Heatmap exported to:\n{path}")

            self.log(f"Heatmap exported: {path}")

        except Exception as e:

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


    def clear_all(self):

        self.df_edges = None

        self.adj_df = None

        self.G = None

        self._node_list = None

        for i in self.tree.get_children():

            self.tree.delete(i)

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

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

        self.ax_net.clear()

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

        self.canvas.draw()

        self.log("Cleared all data.")


    def log(self, text):

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

        self.log_text.see(tk.END)


if __name__ == "__main__":

    app = AdjacencyMatrixApp()

    app.mainloop()

https://github.com/gagandeep44489/DiscreteStrucutreAndAlgoApp/blob/main/Adjacency%20Matrix%20Generator.py

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