Skip to content

Latest commit

 

History

History
343 lines (260 loc) · 12.2 KB

workshop.md

File metadata and controls

343 lines (260 loc) · 12.2 KB

Gramex Network Workshop

This workshop is for JavaScript developers on using gramex-network to build knowledge graphs using force-directed layouts.

Pre-requisites: HTML, SVG, JavaScript, and (ideally) a working knowledge of D3.

About graphs

Graphs are made of nodes (circles, entities) and links (lines, relations). Identify them first.

For example,

These visualizations uses d3.force. @gramex/network is a convenience library on top of it.

This workshop teaches you how you can build such networks.

Set up simple data

The network() function accepts a { nodes, links } object.

  • nodes is an array of objects.
  • links is an array of { source, target } objects that to the node by index number or by reference.
{
  "nodes": [{ "name": "Alice" }, { "name": "Bob" }, { "name": "Carol" }],
  "links": [
    { "source": 0, "target": 1 },
    { "source": 1, "target": 2 }
  ]
}

Render simple network

STEP 1: Log into Codepen and Create a new pen titled Gramex Network workshop

STEP 2: Go through each line in this code and add it to the HTML.

<svg id="simple" width="300" height="100"></svg>

<script type="module">
  // Import the Gramex network library
  import { network } from "https://cdn.jsdelivr.net/npm/@gramex/network@2";
  // Import the underlying D3 library
  import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";

  // Create the nodes as an array of objects. One object per circle
  const nodes = [{ name: "Alice" }, { name: "Bob" }, { name: "Carol" }];
  // Create a links as an array of source-target index pairs. Each pair is a line
  const links = [
    { source: 0, target: 1 },
    { source: 1, target: 2 },
  ];
  // Draw the network. This is a shortcut for { nodes: nodes, links: links, d3: d3 }
  // Note that we pass d3 -- that's to let the network use our specific version of D3.
  const graph = network("#simple", { nodes, links, d3 });
  // `graph` returns an object with { nodes, links }. These are D3 joins. You can style them
  // Fill the SVG in red and give it an 8px radius.
  graph.nodes.attr("fill", "red").attr("r", 8);
  // Make the links grey and semi-transparent
  graph.links.attr("stroke", "rgba(0,0,0,0.2)");
</script>

The output should look like this:

Example

Notice a few things that @gramex/network automatically does:

  1. DRAG each node by clicking and dragging it
  2. PIN the node when you release it
  3. UNPIN a pinned node by double-clicking it
  4. UNPIN all pinned nodes by double-clicking anywhere else

These are useful features to explore the network.

When dragging, the node gets a dragging class. When pinned, it gets a pinned class. You can use this to style nodes that are dragged or pinned.

Add this to your CSS to style nodes when dragged or pinned:

.dragging {
  stroke: black;
  stroke-width: 5;
}
.pinned {
  stroke: black;
  stroke-width: 3;
}

Exercise #1

  1. Add a new node "David" to the nodes array. Add a new link from "Carol" to "David" and from "David" to "Alice".
  2. Change the color of the nodes to blue and the links to green.
  3. Change the radius of the nodes to 10px and the opacity of the links to 0.5.

Dynamic styling

The node and link styles can be dynamic, based on data. For example, modify graph.nodes.... to:

graph.nodes
  // Bob is red, others are blue
  .attr("fill", (d) => (d.name === "Bob" ? "red" : "blue"))
  // Longer names are bigger
  .attr("r", (d) => d.name.length * 3);

Add tooltips

You can use Bootstrap tooltips.

Add this to the top of your HTML:

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

Modify your graph.nodes to add:

graph.nodes.attr("data-bs-toggle", "tooltip").attr("title", (d) => d.name);

Add this to the end of your script:

new bootstrap.Tooltip("body", { selector: '[data-bs-toggle="tooltip"]' });

Now, you should be able to see tooltips when you hover over the nodes.

Exercise #2

  1. Modify the tooltip to show their names in uppercase.
  2. Add the length of their names in the tooltip (e.g. "ALICE (5)").
  3. Position the tooltip on the left of the node using the tooltip direction

Tabular data

Any tabular data can be converted into a node-link structure. For example, take this table of countries and religions:

Country-religion dataset screenshot

Convert this into a flat array of objects like this:

const data = [
  { Country: "USA", Religion: "Christian", Population: 100 },
  { Country: "UK", Religion: "Christian", Population: 90 },
  { Country: "Iran", Religion: "Muslim", Population: 80 },
];

Now you can convert it to a node-link dataset using { nodes, links } = kpartite(data, keys, values). It accepts 3 parameters:

  1. data - array of objects containing the data.
  2. keys - object of {key: column} pairs or an array of [key, column] pairs.
    • key is a string node type
    • column is the string name of a field in data, or a function(object) that returns the field, or a static value.
  3. values - object of accessor functions for link values that are aggregated across links and nodes

For example:

const { nodes, links } = kpartite(
  data,
  {
    Country: "Country", // Create a node for each country
    Religion: (d) => d.Religion, // Create a node for each religion
  },
  {
    count: 1, // Count the number of links between countries and religions
    Population: "Population", // Sum the population of countries and religions
  },
);

This creates the following nodes:

[
  {
    key: "Country",
    value: "USA",
    id: '["Country","USA"]',
    count: 1,
    Population: 100,
  },
  {
    key: "Religion",
    value: "Christian",
    id: '["Religion","Christian"]',
    count: 2,
    Population: 190,
  },
  // ... etc.
];

... and the following links:

[
  {
    source: {}, // link to USA source node
    target: {}, // link to Christian target node
    id: '["[\\"Country\\",\\"USA\\"]","[\\"Religion\\",\\"Christian\\"]"]',
    count: 1,
    Population: 100,
  },
  // ... etc.
];

Try it out. Add this to the bottom of your HTML:

<svg id="religion" width="600" height="380"></svg>

<script type="module">
  import { network } from "https://cdn.jsdelivr.net/npm/@gramex/network@2";
  // Load the kpartite function from the network library
  import { kpartite } from "https://cdn.jsdelivr.net/npm/@gramex/network@2/dist/kpartite.js";
  import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";

  // Load the data
  const data = await fetch("https://gramener.com/gramex-network/docs/country-religion.json").then((r) => r.json());
  // Convert tabular data into a node-link structure
  const { nodes, links } = kpartite(
    data,
    { country: "Country", religion: "Religion" },
    { count: 1, Population: "Value" },
  );

  // Create the network
  const graph = network("#religion", { nodes, links, d3 });
  graph.nodes
    // countries are in red, religions are in blue
    .attr("fill", (d) => (d.key == "country" ? "rgba(255,0,0,0.5)" : "rgba(0,0,255,0.5)"))
    .attr("r", 5);
  graph.links.attr("stroke", "rgba(0,0,0,0.2)");
</script>

Exercise #3

  1. Modify the radius based on the population. Perhaps a linear scale from 3 to 20 based on the population.
  2. Add a tooltip to show the country / religion.

Filter nodes and links

To dynamically filter nodes and links, pass a subset of the SAME nodes and links.

Make sure the nodes and links are the same objects as the original nodes and links. This ensures that the simulation is not restarted.

For example, add this range slider:

<input type="range" min="0" max="10" step="0.1" value="6" id="link-filter" class="form-range" />

Modify the graph drawing code (starting from // Create the network) as follows:

function draw() {
  // Filter the data
  const minPopulation = Math.pow(10, document.querySelector("#link-filter").value);
  const filteredLinks = links.filter((d) => d.Population >= minPopulation);
  const filteredNodes = nodes.filter((d) => filteredLinks.some((l) => l.source == d || l.target == d));

  // Create the network
  const graph = network("#religion", { nodes: filteredNodes, links: filteredLinks, d3 });

  // Style the network
  const rScale = d3
    .scaleLinear()
    .domain(d3.extent(nodes, (d) => d.Population))
    .range([3, 20]);
  graph.nodes
    .attr("fill", (d) => (d.key == "country" ? "rgba(255,0,0,0.5)" : "rgba(0,0,255,0.5)"))
    .attr("r", (d) => rScale(d.Population))
    .attr("data-bs-toggle", "tooltip")
    .attr("title", (d) => d.value);
  graph.links.attr("stroke", "rgba(0,0,0,0.2)");
}

draw();
document.querySelector("#link-filter").addEventListener("input", draw);

Exercise #4

  1. Modify the tooltip to show the country / religion and the population (e.g. "USA (100)").

Further exploration

Submit your code

Create an issue titled Exercise submission. Add a link to your CodePen and submit the issue.

Verification

Check if the output looks like this:

Validation output

Video

Workshop video