Skip to content

CSV nodes + edges pair

Two files: nodes.csv carries per-node properties, edges.csv carries the connections. This is the right format when your spreadsheet has columns like age, category, tags on each node.

nodes.csv
id,label,x,y,age:number,joined:date,active:boolean,tags:string[]
n1,Alice,10,20,34,2021-03-15,true,engineer|founder
n2,Bob,-5,8,28,2023-11-02,false,designer
n3,Carol,100,-30,45,2019-07-20,true,
edges.csv
source,target,weight
n1,n2,0.8
n2,n3,1.2

Download nodes.csv · Download edges.csv

TSV works identically — save with tab separators and rename to .tsv.

Labelled slots (recommended). The drop zone has two slots — Nodes and Edges. Drop one file into each; the graph loads the moment both slots are filled. Filenames don’t matter.

Multi-select. Drag both files onto the drop zone at once. Knotviz pairs them by filename — one must contain nodes, the other edges.

ColumnRequiredNotes
idyesUnique string.
labelnoDisplay text. Also exposed as a filterable property (see the label column below).
x, ynoNumeric positions. Preserved only if all nodes have them (positions).
Any othernoPer-node property. Typed via :type suffix or inferred from sample values.

Append :type to a column name to declare its type explicitly. Five recognised types:

age:number
joined:date
active:boolean
homepage:string
tags:string[]

Inference fills in untyped columns. For string[] specifically, Knotviz auto-detects the type when every non-empty cell in the column contains a pipe — so a column literally named tags with values a|b, c|d, e|f becomes string[] without a suffix. If some cells are prose that happens to contain a pipe ("a | b" as a sentence), use :string to force the literal interpretation.

Full inference rules and edge cases: Shared conventions → Type inference.

label is dual-role: it drives the node’s display label and is exposed as a filterable / colourable property. So if your data has a real label column (e.g. taxonomy names, category labels), you keep it for filters and colour-encoding rather than having the display layer silently absorb it.

id, x, and y stay structural-only — they never appear in the Filter or Analyze panels.

Same shape as the CSV edge list format. Unknown source / target ids (ones not in the nodes file) skip the edge with a console warning.

If a typed column has cells that don’t match the declared type, Knotviz drops the offending cell and counts it. Before the graph loads, a modal summarises per-column failures so you can fix the source file:

age — 300 nodes failed (e.g. "thirty-four") joined — 12 nodes failed (e.g. "March 15 2021")

Cancel if the numbers look wrong, fix the source, re-drop. Load anyway if you’re comfortable treating the failed cells as missing — they’ll back-fill with the type default.

Two DataFrames — nodes_df with an id column, edges_df with source and target.

pandas_to_csv_pair.py
# Keep only the columns you want as properties; drop anything else first.
nodes_df.to_csv("nodes.csv", index=False)
edges_df[["source", "target", "weight"]].to_csv("edges.csv", index=False)

Add :type suffixes to column headers if inference won’t pick the type you want:

nodes_df.rename(columns={"age": "age:number", "joined": "joined:date", "tags": "tags:string[]"}) \
.to_csv("nodes.csv", index=False)

Two \copy statements — one per file. Put the type suffix in the query alias.

pg_to_csv_pair.sh
psql -d mydb -c "\copy (
SELECT id, name AS label, age AS \"age:number\", joined AS \"joined:date\"
FROM people
) TO 'nodes.csv' CSV HEADER"
psql -d mydb -c "\copy (
SELECT source_id AS source, target_id AS target, weight
FROM edges
) TO 'edges.csv' CSV HEADER"

Walk nodes and edges separately.

nx_to_csv_pair.py
import csv, networkx as nx
# Decide up front which node attributes become property columns.
prop_keys = ["age", "community", "joined"]
with open("nodes.csv", "w", newline="") as f:
w = csv.writer(f)
w.writerow(["id", "label", *prop_keys])
for node_id, data in G.nodes(data=True):
w.writerow([node_id, data.get("label", node_id), *(data.get(k) for k in prop_keys)])
with open("edges.csv", "w", newline="") as f:
w = csv.writer(f)
w.writerow(["source", "target", "weight"])
for u, v, data in G.edges(data=True):
w.writerow([u, v, data.get("weight", 1)])
  • Edge endpoints must match ids in the nodes file. Unknown ids are skipped with a console warning.
  • Pipe cells in non-array columns. A column with pipes only in some cells infers as string (pipes treated literally). A column with pipes in every non-empty cell infers as string[]. When in doubt, declare with :string or :string[].
  • Leading-zero strings stay strings. 0012 as an id or property value is kept as "0012" (zip codes, phone numbers). Force numeric with :number if you want 12.
  • Declared-but-empty columns survive. A column whose every cell is empty is still registered — it defaults to number and back-fills with 0. Example: id,label,notes with every notes cell blank will still produce a notes number filter in the UI. You’ll see a pre-load modal reporting the replacement count.
  • A column called label isn’t lost. It shows up as both the display label and a filterable property.