Drawing networks with Cytoscape.js

Draw pathways and protein networks in a teaching page with Cytoscape.js, load them from STRING or Cytoscape desktop, and give every picture a text version. Where a node sits means nothing unless you make it mean something, so say what the layout does.

Which starting point to use

Feature Write the network in the page Ask the STRING API Export from Cytoscape desktop
What it is Nodes and edges typed into the page’s code A live request for the links among proteins you name A network file and a style file saved from the desktop program
Best for Pathways you teach, where every arrow needs a source What is known about a set of proteins, and the evidence for each link Networks you have already arranged and styled by hand
Limits You check every interaction yourself Functional and physical links have no direction, and the page needs STRING to answer during the class Some styling is lost, and the file is a snapshot
Licence Yours STRING data: CC BY 4.0 Yours

All three are drawn with Cytoscape.js, an open-source library for drawing networks in a web page [1]. It is one script of 435 kB (about 135 kB compressed) and needs no build step. The data come from STRING [2] and from Cytoscape desktop [3].

The examples below all use proteins of the p53 pathway, so you can compare the approaches on the same molecules. They were tested on 15 October 2026 with Cytoscape.js 3.34.3, cytoscape-dagre 4.0.1, cytoscape-fcose 2.2.0 and STRING 12.5.

Start with a small network

One script tag and one call to cytoscape() give you a network. The call takes a container and three things: the elements, a style and a layout.

Code cytoscape-basic.html
<!doctype html>
<html lang="en-GB">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Cytoscape.js: p53 and three of its partners</title>
  <!-- Cytoscape.js 3.34.3 from jsDelivr. The integrity hash stops a changed file from running. -->
  <script src="https://cdn.jsdelivr.net/npm/cytoscape@3.34.3/dist/cytoscape.min.js"
          integrity="sha384-qPKQxl9uMXOw7vSTUDAnpUilhLuulovw6P5Z4db4bqxW5VhumS7przEmHX0iM0Oc"
          crossorigin="anonymous"></script>
  <style>
    body { font: 15px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; margin: 0; padding: 12px 16px 16px; color: #172033; background: #fff; }
    h1 { font-size: 1.05rem; margin: 0 0 8px; }
    /* Cytoscape.js draws into this box, so it must have a height */
    #cy { height: 300px; border: 1px solid #c3cbd8; border-radius: 6px; }
  </style>
</head>
<body>
  <h1>p53 and three of its partners</h1>
  <div id="cy" role="img"
       aria-label="Network diagram: p53 switches on MDM2, p21 and BAX, and MDM2 inhibits p53."></div>

  <script>
    const cy = cytoscape({
      container: document.getElementById("cy"),

      // Elements: nodes and edges. A node needs a unique id; an edge needs a source and a target.
      // Any other field in data, such as label or effect, is yours to use.
      elements: [
        { data: { id: "TP53", label: "p53" } },
        { data: { id: "MDM2", label: "MDM2" } },
        { data: { id: "CDKN1A", label: "p21" } },
        { data: { id: "BAX", label: "BAX" } },
        { data: { source: "TP53", target: "MDM2" } },
        { data: { source: "TP53", target: "CDKN1A" } },
        { data: { source: "TP53", target: "BAX" } },
        { data: { source: "MDM2", target: "TP53", effect: "inhibits" } }
      ],

      // Style: rules like CSS. The selector picks elements; data(label) reads a field.
      style: [
        { selector: "node", style: {
            "label": "data(label)", "text-valign": "center", "font-size": 14, "color": "#172033",
            "shape": "round-rectangle", "width": 64, "height": 32,
            "background-color": "#f4f6fa", "border-width": 1.5, "border-color": "#3b4659" } },
        { selector: "edge", style: {
            "curve-style": "bezier", "width": 2.5,                 // bezier curves keep the two edges between p53 and MDM2 apart
            "line-color": "#0072b2", "target-arrow-color": "#0072b2", "target-arrow-shape": "triangle" } },
        { selector: 'edge[effect = "inhibits"]', style: {      // only edges whose data has effect: "inhibits"
            "line-color": "#d55e00", "target-arrow-color": "#d55e00", "target-arrow-shape": "tee" } }
      ],

      // Layout: where the nodes go. "cose" is built in: linked nodes pull together, all nodes push apart.
      layout: { name: "cose", animate: false, padding: 30 },

      userZoomingEnabled: false        // the mouse wheel scrolls the page instead of zooming the network
    });
  </script>
</body>
</html>

How it works:

  • Elements are nodes and edges, each with a data object. A node needs a unique id. An edge needs a source and a target, which are node ids. Any other field, such as label or effect, is yours to use.
  • The style is a list of rules, like CSS. The selector node picks every node, and edge[effect = "inhibits"] picks the edges whose data say so. data(label) reads a field.
  • The layout places the nodes. cose, which is built in, pulls linked nodes together and pushes all nodes apart. Also built in are grid, circle, concentric, breadthfirst, random, and preset, which uses positions you supply.
  • The container needs a height in CSS. Without one, the network does not appear at all.
  • The mouse wheel zooms the network unless you set userZoomingEnabled: false, which lets the wheel scroll the page instead. That suits a small diagram that already fits its box.

Draw a pathway with a source for every interaction

In a teaching diagram, each arrow is a claim, so each needs a source. This example draws the response to DNA damage through ATM, CHK2 and p53, including the negative feedback loop between p53 and MDM2. Each of its 12 interactions links to the papers that support it. Click a molecule to read what it does and where the evidence comes from; everything not connected to it fades.

The table under the diagram holds the same information as text. Open it with the arrow beside its title.

Code p53-pathway.html
<!doctype html>
<html lang="en-GB">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>The p53–MDM2 feedback loop, with a source for every interaction</title>
  <script src="https://cdn.jsdelivr.net/npm/cytoscape@3.34.3/dist/cytoscape.min.js"
          integrity="sha384-qPKQxl9uMXOw7vSTUDAnpUilhLuulovw6P5Z4db4bqxW5VhumS7przEmHX0iM0Oc"
          crossorigin="anonymous"></script>
  <!-- The dagre layout. Version 4 includes the dagre library and registers itself with Cytoscape.js. -->
  <script src="https://cdn.jsdelivr.net/npm/cytoscape-dagre@4.0.1/dist/cytoscape-dagre.min.js"
          integrity="sha384-CIu9EFddQ0GplFvevqHZ1sIPo1IRXFvxHd0A6jaJ3m052fy+mDfer/ItnvIS5uY3"
          crossorigin="anonymous"></script>
  <style>
    body { font: 15px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; margin: 0; padding: 12px 16px 16px; color: #172033; background: #fff; }
    h1 { font-size: 1.05rem; margin: 0 0 4px; }
    h2 { font-size: 0.95rem; margin: 10px 0 4px; }
    p { margin: 0 0 6px; }
    .key { font-size: 13px; color: #3b4659; }
    /* minmax(0, ...) lets a column shrink below the width of its content, so nothing pushes it past the screen edge */
    .layout { display: grid; grid-template-columns: minmax(0, 2fr) minmax(220px, 1fr); gap: 14px; }
    @media (max-width: 600px) { .layout { grid-template-columns: minmax(0, 1fr); } }
    #cy { height: 420px; border: 1px solid #c3cbd8; border-radius: 6px; }
    @media (min-width: 601px) { #cy { height: 450px; } }
    button, input { font: inherit; font-size: 14px; }
    button { padding: 4px 9px; }
    .toolbar { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
    #details { border-left: 4px solid #0072b2; padding: 2px 0 2px 10px; font-size: 14px; min-height: 4.5em; }
    #details h3 { font-size: 0.95rem; margin: 0 0 4px; }
    #details ul { margin: 4px 0 0; padding-left: 1.1rem; }
    #details li { margin-bottom: 4px; }
    fieldset { margin: 10px 0 0; border: 1px solid #c3cbd8; border-radius: 6px; padding: 4px 10px 6px; font-size: 14px; }
    fieldset label { display: block; }
    legend { font-weight: 600; padding: 0 4px; }
    #molecules { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 4px; }
    #molecules button[aria-pressed="true"] { outline: 3px solid #172033; outline-offset: 1px; }
    details { margin-top: 10px; font-size: 14px; }
    summary { cursor: pointer; font-weight: 600; }
    .table-wrap { overflow-x: auto; }
    table { border-collapse: collapse; width: 100%; margin-top: 6px; }
    caption { text-align: left; color: #3b4659; padding-bottom: 4px; }
    th, td { border: 1px solid #dde2ea; padding: 4px 6px; text-align: left; vertical-align: top; }
  </style>
</head>
<body>
  <h1>DNA damage, p53 and the p53–MDM2 feedback loop</h1>
  <p class="key">Arrowhead: activates. Flat bar: inhibits. Dashed line: switches on the gene. Solid line: acts on the protein.</p>

  <div class="layout">
    <div>
      <div id="cy" role="img"
           aria-label="Pathway diagram of the response to DNA damage through ATM, CHK2, p53 and MDM2. The table under the diagram gives the same information."></div>
      <div class="toolbar">
        <button type="button" id="fit">Fit to view</button>
        <button type="button" id="png">Download PNG</button>
      </div>
    </div>
    <div>
      <div id="details" aria-live="polite">
        <p>Click a molecule in the diagram, or use the buttons below, to see its interactions and the papers behind them.</p>
      </div>
      <fieldset>
        <legend>Show interactions by mechanism</legend>
        <label><input type="checkbox" name="mech" value="phosphorylation" checked> Phosphorylation</label>
        <label><input type="checkbox" name="mech" value="transcription" checked> Switching on a gene</label>
        <label><input type="checkbox" name="mech" value="binding" checked> Binding or degradation</label>
        <label><input type="checkbox" name="mech" value="other" checked> Other</label>
      </fieldset>
      <h2 id="molecules-heading">Molecules</h2>
      <ul id="molecules" aria-labelledby="molecules-heading"></ul>
    </div>
  </div>

  <details>
    <summary>Table of interactions (the same information as the diagram)</summary>
    <div class="table-wrap">
      <table>
        <caption>All 12 interactions in the diagram, with the papers that support them</caption>
        <thead><tr><th scope="col">From</th><th scope="col">Effect</th><th scope="col">To</th><th scope="col">How</th><th scope="col">Sources</th></tr></thead>
        <tbody id="edge-table"></tbody>
      </table>
    </div>
  </details>

  <script>
    // ----- The data: everything below is built from these three lists -----

    // Papers, by PubMed ID. Each one was checked in PubMed.
    const PAPERS = {
      12556884: "Bakkenist and Kastan (2003) Nature 421:499",
      10973490: "Matsuoka et al. (2000) PNAS 97:10389",
      9733514:  "Banin et al. (1998) Science 281:1674",
      9733515:  "Canman et al. (1998) Science 281:1677",
      10710310: "Hirao et al. (2000) Science 287:1824",
      10673500: "Chehab et al. (2000) Genes Dev 14:278",
      11331603: "Maya et al. (2001) Genes Dev 15:1067",
      8440237:  "Barak et al. (1993) EMBO J 12:461",
      8319905:  "Wu et al. (1993) Genes Dev 7:1126",
      1535557:  "Momand et al. (1992) Cell 69:1237",
      9153395:  "Haupt et al. (1997) Nature 387:296",
      9153396:  "Kubbutat et al. (1997) Nature 387:299",
      9450543:  "Honda et al. (1997) FEBS Lett 420:25",
      8242752:  "el-Deiry et al. (1993) Cell 75:817",
      8242751:  "Harper et al. (1993) Cell 75:805",
      7739542:  "Ohtsubo et al. (1995) Mol Cell Biol 15:2612",
      7834749:  "Miyashita and Reed (1995) Cell 80:293",
      8358790:  "Oltvai et al. (1993) Cell 74:609"
    };

    // Molecules. "kind" sets the shape: stimulus, protein, complex or outcome.
    const nodes = [
      { id: "damage",    label: "DNA damage",      kind: "stimulus", info: "DNA double-strand breaks, for example after ionising radiation." },
      { id: "ATM",       label: "ATM",             kind: "protein",  info: "A protein kinase activated by double-strand breaks. It phosphorylates CHK2, p53 and MDM2." },
      { id: "CHEK2",     label: "CHK2 (CHEK2)",    kind: "protein",  info: "A checkpoint kinase activated by ATM. It phosphorylates p53 on serine 20." },
      { id: "TP53",      label: "p53 (TP53)",      kind: "protein",  info: "A transcription factor. When it is stabilised, it switches on genes for cell-cycle arrest and apoptosis." },
      { id: "MDM2",      label: "MDM2",            kind: "protein",  info: "An E3 ubiquitin ligase. It binds p53, blocks its activity and marks it for degradation. The MDM2 gene is itself switched on by p53." },
      { id: "CDKN1A",    label: "p21 (CDKN1A)",    kind: "protein",  info: "An inhibitor of cyclin-dependent kinases, switched on by p53." },
      { id: "CDK2",      label: "CDK2–cyclin E",   kind: "complex",  info: "The kinase complex that takes the cell from G1 into S phase." },
      { id: "BAX",       label: "BAX",             kind: "protein",  info: "A BCL-2 family protein that promotes apoptosis, switched on by p53." },
      { id: "g1s",       label: "G1/S transition", kind: "outcome",  info: "Entry into S phase. It is blocked when CDK2–cyclin E is inhibited, so the cell stops in G1." },
      { id: "apoptosis", label: "Apoptosis",       kind: "outcome",  info: "Programmed cell death." }
    ];

    // Interactions. effect: activates or inhibits. mech: how. refs: PubMed IDs of the papers that support it.
    const edges = [
      { id: "e1",  source: "damage", target: "ATM",       effect: "activates", mech: "other",           refs: [12556884],
        detail: "ATM molecules phosphorylate each other on serine 1981, and the inactive dimers separate" },
      { id: "e2",  source: "ATM",    target: "CHEK2",     effect: "activates", mech: "phosphorylation", refs: [10973490],
        detail: "phosphorylation of threonine 68" },
      { id: "e3",  source: "ATM",    target: "TP53",      effect: "activates", mech: "phosphorylation", refs: [9733514, 9733515],
        detail: "phosphorylation of serine 15" },
      { id: "e4",  source: "CHEK2",  target: "TP53",      effect: "activates", mech: "phosphorylation", refs: [10710310, 10673500],
        detail: "phosphorylation of serine 20, which stabilises p53" },
      { id: "e5",  source: "ATM",    target: "MDM2",      effect: "inhibits",  mech: "phosphorylation", refs: [11331603],
        detail: "phosphorylation of serine 395, which reduces the degradation of p53" },
      { id: "e6",  source: "TP53",   target: "MDM2",      effect: "activates", mech: "transcription",   refs: [8440237, 8319905],
        detail: "p53 switches on the MDM2 gene" },
      { id: "e7",  source: "MDM2",   target: "TP53",      effect: "inhibits",  mech: "binding",         refs: [1535557, 9153395, 9153396, 9450543],
        detail: "MDM2 binds p53, blocks its activity and tags it with ubiquitin for degradation" },
      { id: "e8",  source: "TP53",   target: "CDKN1A",    effect: "activates", mech: "transcription",   refs: [8242752],
        detail: "p53 switches on the gene for p21 (also called WAF1 or CIP1)" },
      { id: "e9",  source: "CDKN1A", target: "CDK2",      effect: "inhibits",  mech: "binding",         refs: [8242751],
        detail: "p21 binds and inhibits the G1 cyclin-dependent kinases" },
      { id: "e10", source: "CDK2",   target: "g1s",       effect: "activates", mech: "other",           refs: [7739542],
        detail: "cyclin E is needed for the G1-to-S transition" },
      { id: "e11", source: "TP53",   target: "BAX",       effect: "activates", mech: "transcription",   refs: [7834749],
        detail: "p53 switches on the BAX gene directly" },
      { id: "e12", source: "BAX",    target: "apoptosis", effect: "activates", mech: "other",           refs: [8358790],
        detail: "BAX speeds up programmed cell death" }
    ];

    // ----- Helpers: a molecule by its ID, and links to the papers -----
    const byId = id => nodes.find(n => n.id === id);
    const sources = refs => refs.map(id =>
      `<a href="https://pubmed.ncbi.nlm.nih.gov/${id}/" target="_blank" rel="noopener">${PAPERS[id]}</a>`).join("; ");

    // ----- The text version, from the same lists (built first, so it works even without the diagram) -----
    document.getElementById("edge-table").innerHTML = edges.map(e =>
      `<tr><td>${byId(e.source).label}</td><td>${e.effect}</td><td>${byId(e.target).label}</td>` +
      `<td>${e.detail}</td><td>${sources(e.refs)}</td></tr>`).join("");

    // ----- The diagram -----
    if (!window.cytoscape) {                   // the library did not arrive: say so, and stop here
      document.getElementById("details").textContent = "Cytoscape.js did not load. Check the connection and reload the page.";
      throw new Error("Cytoscape.js did not load");
    }

    const cy = cytoscape({
      container: document.getElementById("cy"),
      elements: [
        ...nodes.map(n => ({ data: n })),
        ...edges.map(e => ({ data: e }))
      ],
      style: [
        { selector: "node", style: {
            "label": "data(label)", "font-size": 14, "color": "#172033",
            "text-valign": "center", "text-halign": "center",
            "shape": "round-rectangle", "height": 30,
            "width": node => 18 + 8 * node.data("label").length,   // fit the box to its label ("width: label" is deprecated)
            "background-color": "#f4f6fa", "border-width": 1.5, "border-color": "#3b4659" } },
        { selector: 'node[kind = "stimulus"]', style: {
            "shape": "star", "width": 52, "height": 52, "background-color": "#ffd966", "border-color": "#8a6d00",
            "text-halign": "right", "text-margin-x": 6 } },
        { selector: 'node[kind = "complex"]', style: { "background-color": "#ece6f5", "border-color": "#6b4fa0" } },
        { selector: 'node[kind = "outcome"]', style: { "shape": "hexagon", "background-color": "#ffffff", "border-style": "dashed" } },
        { selector: "edge", style: {
            "curve-style": "bezier", "width": 2.5, "arrow-scale": 1.3,
            "line-color": "#0072b2", "target-arrow-color": "#0072b2", "target-arrow-shape": "triangle" } },
        // Blue and vermilion rather than green and red, and the arrowheads differ too, so the meaning survives colour blindness and greyscale
        { selector: 'edge[effect = "inhibits"]', style: {
            "line-color": "#d55e00", "target-arrow-color": "#d55e00", "target-arrow-shape": "tee" } },
        { selector: 'edge[mech = "transcription"]', style: { "line-style": "dashed" } },
        { selector: "node:selected", style: { "border-width": 3.5, "border-color": "#172033", "background-color": "#fff3c4" } },
        { selector: ".faded", style: { "opacity": 0.15 } },
        { selector: ".filtered", style: { "display": "none" } }
      ],
      // dagre puts the nodes in rows that follow the arrows: the stimulus at the top, outcomes at the bottom
      layout: { name: "dagre", rankDir: "TB", nodeSep: 30, rankSep: 30, padding: 20 },
      userZoomingEnabled: false        // the whole pathway fits, so let the mouse wheel scroll the page
    });
    cy.edges().unselectify();          // only molecules can be selected

    // ----- Details of one molecule, with its neighbours highlighted -----
    const panel = document.getElementById("details");

    function showNode(node) {
      cy.batch(() => {
        cy.elements().removeClass("faded").unselect();
        cy.elements().not(node.closedNeighborhood()).addClass("faded");   // keep the node, its edges and its neighbours
        node.select();
      });
      const d = node.data();
      // Leave out filtered edges. Select them by class: ":visible" can be out of date straight after a class change.
      const items = node.connectedEdges().not(".filtered").map(e => {
        const x = e.data();
        return `<li>${byId(x.source).label} ${x.effect} ${byId(x.target).label}: ${x.detail}. ${sources(x.refs)}</li>`;
      }).join("");
      panel.innerHTML = `<h3>${d.label}</h3><p>${d.info}</p>` +
        (items ? `<ul>${items}</ul>` : "<p>None of its interactions match the mechanisms ticked.</p>");
      document.querySelectorAll("#molecules button").forEach(b =>
        b.setAttribute("aria-pressed", String(b.dataset.id === d.id)));
    }

    function clearHighlight() {
      cy.elements().removeClass("faded").unselect();
      document.querySelectorAll("#molecules button").forEach(b => b.setAttribute("aria-pressed", "false"));
    }

    cy.on("tap", "node", event => showNode(event.target));
    cy.on("tap", event => { if (event.target === cy) clearHighlight(); });   // a click on the background

    // Keyboard users cannot click the canvas, so each molecule also gets a real button
    const list = document.getElementById("molecules");
    for (const n of nodes) {
      const button = document.createElement("button");
      button.type = "button";
      button.dataset.id = n.id;
      button.textContent = n.label;
      button.setAttribute("aria-pressed", "false");
      button.addEventListener("click", () => showNode(cy.getElementById(n.id)));
      const li = document.createElement("li");
      li.appendChild(button);
      list.appendChild(li);
    }

    // ----- Filter the edges by mechanism -----
    document.querySelectorAll('input[name="mech"]').forEach(box => box.addEventListener("change", () => {
      cy.edges(`[mech = "${box.value}"]`).toggleClass("filtered", !box.checked);
      const selected = cy.nodes(":selected");
      if (selected.nonempty()) showNode(selected);     // update the list of interactions
    }));

    // ----- Fit, and save a picture -----
    document.getElementById("fit").addEventListener("click", () => cy.fit(undefined, 20));
    document.getElementById("png").addEventListener("click", () => {
      const a = document.createElement("a");
      a.href = cy.png({ full: true, scale: 2, bg: "#ffffff" });   // the whole network, at twice screen resolution, on white
      a.download = "p53-pathway.png";
      a.click();
    });

  </script>
</body>
</html>

How the code works:

  • One set of data. Two lists, nodes and edges, hold everything. Each edge has an effect (activates or inhibits), a mech (how it acts) and refs (PubMed IDs). The diagram, the details panel and the table are all built from these lists, so they cannot disagree.
  • Style from data. Edges that inhibit end in a flat bar (tee) instead of an arrowhead, and edges that switch on a gene are dashed. The colours are blue and vermilion rather than green and red, and the shapes and line styles carry the same meaning, so the diagram still reads in greyscale and for colour-blind readers [4].
  • The layout is dagre, from the cytoscape-dagre extension. It puts the nodes in rows that follow the arrows, so DNA damage is at the top and the outcomes are at the bottom.
  • Clicks. cy.on("tap", "node", …) runs when a node is clicked or tapped. node.closedNeighborhood() returns the node with its edges and neighbours, and everything else gets a faded class.
  • Filters. Unticking a mechanism adds a filtered class to those edges, and a style rule hides that class with display: none.
  • Keyboard. Nobody can move to a node on a canvas with the Tab key, so each molecule also has a real button that does the same as clicking it.

The interactions and the papers that support them, each checked in PubMed:

Interaction What happens Sources
DNA damage activates ATM ATM molecules phosphorylate each other on serine 1981, and the inactive dimers separate Bakkenist and Kastan 2003
ATM activates CHK2 Phosphorylation of threonine 68 Matsuoka et al. 2000
ATM activates p53 Phosphorylation of serine 15 Banin et al. 1998, Canman et al. 1998
CHK2 activates p53 Phosphorylation of serine 20, which stabilises p53 Hirao et al. 2000, Chehab et al. 2000
ATM inhibits MDM2 Phosphorylation of serine 395, which reduces the degradation of p53 Maya et al. 2001
p53 activates MDM2 p53 switches on the MDM2 gene Barak et al. 1993, Wu et al. 1993
MDM2 inhibits p53 MDM2 binds p53, blocks its activity and tags it with ubiquitin for degradation Momand et al. 1992, Haupt et al. 1997, Kubbutat et al. 1997, Honda et al. 1997
p53 activates p21 p53 switches on the gene for p21 (CDKN1A) el-Deiry et al. 1993
p21 inhibits CDK2–cyclin E p21 binds and inhibits the G1 cyclin-dependent kinases Harper et al. 1993
CDK2–cyclin E activates the G1/S transition Cyclin E is needed for the cell to enter S phase Ohtsubo et al. 1995
p53 activates BAX p53 switches on the BAX gene directly Miyashita and Reed 1995
BAX activates apoptosis BAX speeds up programmed cell death Oltvai et al. 1993

To draw another pathway, replace the two lists and keep the same fields. Check each interaction against a primary paper, and keep the PubMed IDs with the data, so that students and colleagues can check them too. If a pathway grows much bigger, consider splitting it into several diagrams [4].

Turn the pathway into a step-by-step activity

A whole pathway is a lot to take in at once. Revealing it a step at a time, with a question to answer before each step, lets students predict what comes next and build up the picture in a sensible order. The educators’ page describes how to run it in class.

Code p53-step-by-step.html
<!doctype html>
<html lang="en-GB">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Step by step: the p53–MDM2 feedback loop</title>
  <script src="https://cdn.jsdelivr.net/npm/cytoscape@3.34.3/dist/cytoscape.min.js"
          integrity="sha384-qPKQxl9uMXOw7vSTUDAnpUilhLuulovw6P5Z4db4bqxW5VhumS7przEmHX0iM0Oc"
          crossorigin="anonymous"></script>
  <script src="https://cdn.jsdelivr.net/npm/cytoscape-dagre@4.0.1/dist/cytoscape-dagre.min.js"
          integrity="sha384-CIu9EFddQ0GplFvevqHZ1sIPo1IRXFvxHd0A6jaJ3m052fy+mDfer/ItnvIS5uY3"
          crossorigin="anonymous"></script>
  <style>
    body { font: 15px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; margin: 0; padding: 12px 16px 16px; color: #172033; background: #fff; }
    h1 { font-size: 1.05rem; margin: 0 0 4px; }
    p { margin: 0 0 6px; }
    .key { font-size: 13px; color: #3b4659; }
    .toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 6px 0 8px; }
    button { font: inherit; font-size: 14px; padding: 4px 9px; }
    #count { font-size: 14px; color: #3b4659; margin-left: 4px; }
    #narration { min-height: 4.6em; }
    #step-text { font-weight: 600; }
    #ask { color: #3b4659; }
    #cy { height: 430px; border: 1px solid #c3cbd8; border-radius: 6px; }
  </style>
</head>
<body>
  <h1>Step by step: DNA damage, p53 and the p53–MDM2 feedback loop</h1>
  <p class="key">Arrowhead: activates. Flat bar: inhibits. Dashed line: switches on the gene. Solid line: acts on the protein.</p>

  <div class="toolbar" role="group" aria-label="Steps">
    <button type="button" id="prev">Previous step</button>
    <button type="button" id="next">Next step</button>
    <button type="button" id="all">Show the whole pathway</button>
    <span id="count"></span>
  </div>
  <div id="narration" aria-live="polite">
    <p id="step-text"></p>
    <p id="ask"></p>
  </div>
  <div id="cy" role="img"
       aria-label="Pathway diagram that grows one step at a time. The text above the diagram describes each step."></div>

  <script>
    // The same pathway as p53-pathway.html, without the sources (see that file for them)
    const nodes = [
      { id: "damage", label: "DNA damage", kind: "stimulus" },
      { id: "ATM", label: "ATM" },
      { id: "CHEK2", label: "CHK2 (CHEK2)" },
      { id: "TP53", label: "p53 (TP53)" },
      { id: "MDM2", label: "MDM2" },
      { id: "CDKN1A", label: "p21 (CDKN1A)" },
      { id: "CDK2", label: "CDK2–cyclin E", kind: "complex" },
      { id: "BAX", label: "BAX" },
      { id: "g1s", label: "G1/S transition", kind: "outcome" },
      { id: "apoptosis", label: "Apoptosis", kind: "outcome" }
    ];
    const edges = [
      { id: "e1",  source: "damage", target: "ATM",       effect: "activates", mech: "other" },
      { id: "e2",  source: "ATM",    target: "CHEK2",     effect: "activates", mech: "phosphorylation" },
      { id: "e3",  source: "ATM",    target: "TP53",      effect: "activates", mech: "phosphorylation" },
      { id: "e4",  source: "CHEK2",  target: "TP53",      effect: "activates", mech: "phosphorylation" },
      { id: "e5",  source: "ATM",    target: "MDM2",      effect: "inhibits",  mech: "phosphorylation" },
      { id: "e6",  source: "TP53",   target: "MDM2",      effect: "activates", mech: "transcription" },
      { id: "e7",  source: "MDM2",   target: "TP53",      effect: "inhibits",  mech: "binding" },
      { id: "e8",  source: "TP53",   target: "CDKN1A",    effect: "activates", mech: "transcription" },
      { id: "e9",  source: "CDKN1A", target: "CDK2",      effect: "inhibits",  mech: "binding" },
      { id: "e10", source: "CDK2",   target: "g1s",       effect: "activates", mech: "other" },
      { id: "e11", source: "TP53",   target: "BAX",       effect: "activates", mech: "transcription" },
      { id: "e12", source: "BAX",    target: "apoptosis", effect: "activates", mech: "other" }
    ];

    // Each step lists the elements it adds, what to say, and a question to answer before going on
    const STEPS = [
      { show: ["damage", "ATM", "e1"],
        text: "1. DNA double-strand breaks activate the protein kinase ATM.",
        ask: "Before you go on: ATM is a kinase. What does a kinase do to the proteins it acts on?" },
      { show: ["CHEK2", "TP53", "e2", "e3", "e4"],
        text: "2. ATM phosphorylates the kinase CHK2 and p53, and CHK2 also phosphorylates p53. Phosphorylated p53 is more stable, so it builds up.",
        ask: "Before you go on: something keeps p53 low in an undamaged cell. What kind of protein could do that?" },
      { show: ["MDM2", "e7", "e5"],
        text: "3. MDM2 binds p53, blocks its activity and tags it for degradation. ATM phosphorylates MDM2, which weakens this.",
        ask: "Before you go on: predict what would happen to the amount of p53 in a cell with no MDM2." },
      { show: ["e6"],
        text: "4. p53 switches on the MDM2 gene. The more p53 there is, the more of its own inhibitor it makes: a negative feedback loop.",
        ask: "Follow the loop from p53 to MDM2 and back. What happens to p53 once ATM stops signalling?" },
      { show: ["CDKN1A", "CDK2", "g1s", "e8", "e9", "e10"],
        text: "5. p53 switches on p21, which inhibits CDK2–cyclin E. The cell needs CDK2–cyclin E to enter S phase, so it stops in G1.",
        ask: "Count the flat bars on the path from p53 to the G1/S transition. Does p53 speed up or hold back entry into S phase?" },
      { show: ["BAX", "apoptosis", "e11", "e12"],
        text: "6. p53 also switches on BAX, a protein that promotes apoptosis (programmed cell death).",
        ask: "Find the dashed lines. Which three genes does p53 switch on, and what does each one lead to?" }
    ];

    const cy = cytoscape({
      container: document.getElementById("cy"),
      elements: [...nodes.map(n => ({ data: n })), ...edges.map(e => ({ data: e }))],
      style: [
        { selector: "node", style: {
            "label": "data(label)", "font-size": 14, "color": "#172033", "text-valign": "center",
            "shape": "round-rectangle", "height": 30, "width": node => 18 + 8 * node.data("label").length,
            "background-color": "#f4f6fa", "border-width": 1.5, "border-color": "#3b4659" } },
        { selector: 'node[kind = "stimulus"]', style: {
            "shape": "star", "width": 52, "height": 52, "background-color": "#ffd966", "border-color": "#8a6d00",
            "text-halign": "right", "text-margin-x": 6 } },
        { selector: 'node[kind = "complex"]', style: { "background-color": "#ece6f5", "border-color": "#6b4fa0" } },
        { selector: 'node[kind = "outcome"]', style: { "shape": "hexagon", "background-color": "#ffffff", "border-style": "dashed" } },
        { selector: "edge", style: {
            "curve-style": "bezier", "width": 2.5, "arrow-scale": 1.3,
            "line-color": "#0072b2", "target-arrow-color": "#0072b2", "target-arrow-shape": "triangle" } },
        { selector: 'edge[effect = "inhibits"]', style: { "line-color": "#d55e00", "target-arrow-color": "#d55e00", "target-arrow-shape": "tee" } },
        { selector: 'edge[mech = "transcription"]', style: { "line-style": "dashed" } },
        // What the latest step added: a yellow glow under it
        { selector: ".new", style: { "underlay-color": "#ffd400", "underlay-opacity": 0.35, "underlay-padding": 5 } },
        { selector: ".hidden", style: { "display": "none" } }
      ],
      // The layout runs once, on the whole pathway, so nodes stay in place as the steps add them
      layout: { name: "dagre", rankDir: "TB", nodeSep: 30, rankSep: 30 },
      maxZoom: 1.25,                   // stops the first step, with only two nodes, filling the box
      userZoomingEnabled: false,       // let the mouse wheel scroll the page
      autoungrabify: true,             // nodes cannot be dragged out of place
      autounselectify: true
    });

    const motion = !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    let step = 0;

    function showStep(k) {
      step = Math.max(1, Math.min(STEPS.length, k));
      const shown = new Set(STEPS.slice(0, step).flatMap(s => s.show));
      const newest = new Set(STEPS[step - 1].show);
      cy.batch(() => {                 // one redraw for all the changes
        cy.elements().forEach(el => {
          el.toggleClass("hidden", !shown.has(el.id()));
          el.toggleClass("new", newest.has(el.id()));
        });
      });
      // Zoom to what is shown. Select by class: ":visible" can be out of date straight after cy.batch().
      cy.animate({ fit: { eles: cy.elements().not(".hidden"), padding: 30 }, duration: motion ? 400 : 0 });
      document.getElementById("step-text").textContent = STEPS[step - 1].text;
      document.getElementById("ask").textContent = STEPS[step - 1].ask;
      document.getElementById("count").textContent = "Step " + step + " of " + STEPS.length;
    }

    document.getElementById("next").addEventListener("click", () => showStep(step + 1));
    document.getElementById("prev").addEventListener("click", () => showStep(step - 1));
    document.getElementById("all").addEventListener("click", () => {
      showStep(STEPS.length);
      cy.elements().removeClass("new");
      document.getElementById("step-text").textContent = "The whole pathway: a negative feedback loop between p53 and MDM2, and two outcomes of p53 activity.";
    });

    cy.ready(() => showStep(1));
  </script>
</body>
</html>

What the code does:

  • Positions come first. The layout runs once on the whole pathway, so nodes stay where they are as the steps add them. Elements not yet shown have a hidden class, which a style rule turns into display: none.
  • Each step lists the ids it adds, a sentence and a question. cy.batch() makes all the class changes in one redraw, and the newest elements get a yellow glow from the underlay-color style.
  • Zoom. cy.animate({ fit: … }) zooms to what is shown, and maxZoom stops the first step, with only two nodes, from filling the box. Readers who have asked their system for reduced motion get the change without the animation.
  • Text. The sentence and the question go into an element with aria-live="polite", so screen readers read each step aloud.

Load a network from STRING

STRING collects known and predicted links between proteins, drawn from experiments, computational predictions and curated knowledge, and gives each link a score [2]. Its API answers requests from any website, needs no key, and returns JSON that becomes Cytoscape.js elements in a few lines. This example asks for ten proteins around p53 and shows which kind of evidence supports each link.

If STRING does not answer within 20 seconds, the example shows a saved copy of the same request, made on 15 October 2026, and says so. Add ?offline=1 to the example’s address to use the saved copy from the start.

Code string-network.html
<!doctype html>
<html lang="en-GB">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>A protein network from STRING, with its evidence</title>
  <script src="https://cdn.jsdelivr.net/npm/cytoscape@3.34.3/dist/cytoscape.min.js"
          integrity="sha384-qPKQxl9uMXOw7vSTUDAnpUilhLuulovw6P5Z4db4bqxW5VhumS7przEmHX0iM0Oc"
          crossorigin="anonymous"></script>
  <!-- The fcose layout needs layout-base, then cose-base, then cytoscape-fcose, in this order -->
  <script src="https://cdn.jsdelivr.net/npm/layout-base@2.0.1/layout-base.js"
          integrity="sha384-5E2lB9AIGE6LRCnOOSTnZRlYZFZ01iMeN2fw97Z1r4Z/kXALxKw2AC+ZzQqoeDsG"
          crossorigin="anonymous"></script>
  <script src="https://cdn.jsdelivr.net/npm/cose-base@2.2.0/cose-base.js"
          integrity="sha384-RswRBkrMsPUYpJLbZ1CVA08zbNzAkykE2oGJTujBwfjWNdfxv2WVjLJNqv1LhAOp"
          crossorigin="anonymous"></script>
  <script src="https://cdn.jsdelivr.net/npm/cytoscape-fcose@2.2.0/cytoscape-fcose.js"
          integrity="sha384-uk5Wbjq1+KqUdHO30w7N7GrEGdzBhaJeW9o/ANF6v9+yx3M/cBmoX51C000JNCUf"
          crossorigin="anonymous"></script>
  <style>
    body { font: 15px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; margin: 0; padding: 12px 16px 16px; color: #172033; background: #fff; }
    h1 { font-size: 1.05rem; margin: 0 0 8px; }
    p { margin: 0 0 6px; }
    form { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 6px 14px; margin: 0 0 6px; font-size: 14px; }
    form label { display: flex; flex-direction: column; gap: 2px; }
    button, input, select, textarea { font: inherit; font-size: 14px; }
    button { padding: 4px 9px; }
    input[type="number"] { width: 5em; }
    #status { font: 13px/1.4 ui-monospace, Menlo, Consolas, monospace; color: #3b4659; min-height: 1.4em; }
    /* minmax(0, ...) lets a column shrink below the width of its content, so nothing pushes it past the screen edge */
    .layout { display: grid; grid-template-columns: minmax(0, 2fr) minmax(220px, 1fr); gap: 14px; }
    @media (max-width: 600px) { .layout { grid-template-columns: minmax(0, 1fr); } }
    #cy { height: 380px; border: 1px solid #c3cbd8; border-radius: 6px; }
    .toolbar { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
    #details { border-left: 4px solid #7b3fa0; padding: 2px 0 2px 10px; font-size: 14px; min-height: 4.5em; }
    #details h3 { font-size: 0.95rem; margin: 0 0 4px; }
    fieldset { margin: 10px 0 0; border: 1px solid #c3cbd8; border-radius: 6px; padding: 4px 10px 6px; font-size: 14px; }
    fieldset label { display: block; }
    legend { font-weight: 600; padding: 0 4px; }
    .swatch { display: inline-block; width: 2em; vertical-align: middle; margin: 0 4px; border-top: 3px solid #7b3fa0; }
    .swatch.db { border-top: 3px dashed #1f77b4; }
    .swatch.other { border-top: 3px dotted #6b6b6b; }
    details { margin-top: 10px; font-size: 14px; }
    summary { cursor: pointer; font-weight: 600; }
    .table-wrap { overflow-x: auto; }
    table { border-collapse: collapse; width: 100%; margin-top: 6px; }
    caption { text-align: left; color: #3b4659; padding-bottom: 4px; }
    th, td { border: 1px solid #dde2ea; padding: 3px 6px; text-align: left; }
    td.n { text-align: right; font-variant-numeric: tabular-nums; }
    .credit { font-size: 13px; color: #3b4659; margin-top: 10px; }
  </style>
</head>
<body>
  <h1>Who does p53 interact with? Evidence from STRING</h1>

  <form id="query">
    <label>Proteins (gene symbols)
      <textarea id="ids" rows="3" cols="30" spellcheck="false">TP53 MDM2 MDM4 ATM CHEK2 CDKN1A BAX BBC3 CDKN2A EP300</textarea></label>
    <label>Network
      <select id="type">
        <option value="functional" selected>functional</option>
        <option value="physical">physical</option>
      </select></label>
    <label>Minimum score
      <select id="score">
        <option>0.15</option><option>0.4</option><option selected>0.7</option><option>0.9</option>
      </select></label>
    <label>Partners to add
      <input id="add" type="number" min="0" max="20" value="0"></label>
    <button type="submit">Load network</button>
  </form>
  <p id="status" role="status" aria-live="polite"></p>

  <div class="layout">
    <div>
      <div id="cy" role="img" aria-label="Protein network from STRING. The table under the network lists the same links and their scores."></div>
      <div class="toolbar">
        <button type="button" id="relayout">Run the layout again</button>
        <button type="button" id="png">Download PNG</button>
      </div>
    </div>
    <div>
      <div id="details" aria-live="polite"><p>Click a protein to read STRING's description of it and see its partners.</p></div>
      <fieldset>
        <legend>Show links supported by</legend>
        <label><input type="checkbox" name="ev" value="exp" checked><span class="swatch"></span>experiments</label>
        <label><input type="checkbox" name="ev" value="db" checked><span class="swatch db"></span>curated databases</label>
        <label><input type="checkbox" name="ev" value="other" checked><span class="swatch other"></span>other evidence only</label>
      </fieldset>
      <p class="credit">Line width shows the combined score. A link counts as supported by experiments or databases when that score alone is at least 0.4.</p>
    </div>
  </div>

  <details>
    <summary>Table of links and their evidence</summary>
    <div class="table-wrap">
      <table>
        <caption>Links returned by STRING, highest combined score first (scores from 0 to 1)</caption>
        <thead><tr><th scope="col">Protein</th><th scope="col">Protein</th><th scope="col">Combined</th>
          <th scope="col">Experiments</th><th scope="col">Databases</th><th scope="col">Text mining</th></tr></thead>
        <tbody id="rows"></tbody>
      </table>
    </div>
  </details>
  <p class="credit">Data: <a href="https://string-db.org/" target="_blank" rel="noopener">STRING</a> 12.5, under CC BY 4.0.
    Szklarczyk et al. (2025) <a href="https://doi.org/10.1093/nar/gkae1113" target="_blank" rel="noopener">Nucleic Acids Research 53:D730</a>.</p>

  <script>
    const STRING = "https://version-12-5.string-db.org";   // a fixed version, so the answers do not change under you
    const CALLER = "www.genomics.tel";                     // STRING asks callers to say who they are
    const SPECIES = 9606;                                  // NCBI taxon ID for human
    const EVIDENCE = 0.4;                                  // our choice: a channel "supports" a link from this score up

    const statusLine = document.getElementById("status");
    const panel = document.getElementById("details");
    const motion = !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
    const esc = text => String(text).replace(/[&<>"]/g, c => "&#" + c.charCodeAt(0) + ";");   // the replies are data, not HTML

    // Wheel and pinch zoom stay on here: with added partners, a network can be too dense to read without zooming
    const cy = cytoscape({
      container: document.getElementById("cy"),
      style: [
        { selector: "node", style: {
            "label": "data(label)", "font-size": 12, "color": "#172033", "text-valign": "center",
            "width": 54, "height": 54, "background-color": "#e9dff3", "border-width": 2, "border-color": "#7b3fa0" } },
        { selector: "node[?first]", style: { "background-color": "#7b3fa0", "color": "#ffffff" } },        // the first protein in the list
        { selector: "node[!query]", style: { "background-color": "#ffffff", "border-style": "dashed" } },  // partners STRING added
        { selector: "edge", style: {
            "curve-style": "straight", "width": "mapData(score, 0.4, 1, 1, 6)",
            "line-color": "#6b6b6b", "line-style": "dotted", "opacity": 0.9 } },
        { selector: "edge[?db]", style: { "line-color": "#1f77b4", "line-style": "dashed" } },
        { selector: "edge[?exp]", style: { "line-color": "#7b3fa0", "line-style": "solid" } },
        { selector: "node:selected", style: { "border-color": "#172033", "border-width": 4 } },
        { selector: ".faded", style: { "opacity": 0.12 } },
        { selector: ".filtered", style: { "display": "none" } }
      ]
    });
    cy.edges().unselectify();

    // ----- Asking STRING -----
    async function ask(method, params) {
      const query = new URLSearchParams({ ...params, species: SPECIES, caller_identity: CALLER });
      const stop = new AbortController();
      const timer = setTimeout(() => stop.abort(), 20000);          // give up after 20 seconds
      try {
        const reply = await fetch(STRING + "/api/json/" + method + "?" + query, { signal: stop.signal });
        const json = await reply.json();
        if (!reply.ok || !Array.isArray(json)) throw new Error((json[0] && json[0].Error) || "HTTP " + reply.status);
        return json;
      } catch (err) {
        throw err.name === "AbortError" ? new Error("no answer in 20 seconds") : err;
      } finally {
        clearTimeout(timer);
      }
    }

    async function fromString(names, type, score, add) {
      // 1. Match the names to STRING proteins. Names STRING does not know are left out of the reply.
      //    Several identifiers go in one parameter, separated by %0D (a carriage return).
      const found = await ask("get_string_ids", { identifiers: names.join("\r"), limit: 1 });
      if (!found.length) {
        const err = new Error("STRING did not recognise any of these names.");
        err.keepEmpty = true;
        throw err;
      }
      await wait(1000);                                            // STRING asks for one second between calls

      // 2. The links among these proteins. required_score runs from 0 to 1000.
      const params = { identifiers: found.map(p => p.stringId).join("\r"),
                       required_score: Math.round(score * 1000), network_type: type };
      if (add > 0) params.add_nodes = add;
      const links = await ask("network", params);

      // 3. Descriptions of any partners STRING added (it adds 10 by itself when given one protein)
      const known = new Set(found.map(p => p.stringId));
      const extra = [...new Set(links.flatMap(l => [l.stringId_A, l.stringId_B]))].filter(id => !known.has(id));
      let added = [];
      if (extra.length) {
        await wait(1000);
        added = await ask("get_string_ids", { identifiers: extra.join("\r"), limit: 1 });
      }
      return { found, added, links };
    }

    // The saved copy in data/: the same two calls for the starting list, made on 15 October 2026
    async function savedCopy() {
      const [found, links] = await Promise.all(["data/string-p53-ids.json", "data/string-p53-network.json"]
        .map(file => fetch(file).then(r => r.json())));
      return { found, added: [], links };
    }

    // ----- Drawing the reply -----
    function draw({ found, added, links }) {
      const proteins = [...found.map(p => ({ ...p, query: true })), ...added.map(p => ({ ...p, query: false }))];
      cy.elements().remove();
      cy.add(proteins.map(p => ({ data: {
        id: p.stringId, label: p.preferredName, about: p.annotation,
        query: p.query, first: p.query && p.queryIndex === 0 } })));
      cy.add(links.map(l => ({ data: {
        id: l.stringId_A + "~" + l.stringId_B, source: l.stringId_A, target: l.stringId_B,
        a: l.preferredName_A, b: l.preferredName_B,
        score: l.score, escore: l.escore, dscore: l.dscore, tscore: l.tscore,
        exp: l.escore >= EVIDENCE, db: l.dscore >= EVIDENCE } })));
      applyFilter();
      runLayout();
      panel.innerHTML = "<p>Click a protein to read STRING's description of it and see its partners.</p>";

      document.getElementById("rows").innerHTML = [...links].sort((x, y) => y.score - x.score).map(l =>
        `<tr><td>${esc(l.preferredName_A)}</td><td>${esc(l.preferredName_B)}</td><td class="n">${l.score.toFixed(3)}</td>` +
        `<td class="n">${l.escore.toFixed(2)}</td><td class="n">${l.dscore.toFixed(2)}</td><td class="n">${l.tscore.toFixed(2)}</td></tr>`).join("");
    }

    // fcose starts from random positions, so every run gives a different picture of the same links
    function runLayout() {
      cy.layout({ name: "fcose", randomize: true, animate: motion, animationDuration: 600,
                  nodeRepulsion: 9000, idealEdgeLength: 90, padding: 20 }).run();
    }

    function applyFilter() {
      const on = {};
      document.querySelectorAll('input[name="ev"]').forEach(box => { on[box.value] = box.checked; });
      cy.batch(() => cy.edges().forEach(e => {
        const d = e.data();
        const show = (on.exp && d.exp) || (on.db && d.db) || (on.other && !d.exp && !d.db);
        e.toggleClass("filtered", !show);
      }));
    }
    document.querySelectorAll('input[name="ev"]').forEach(box => box.addEventListener("change", applyFilter));

    cy.on("tap", "node", event => {
      const n = event.target;
      cy.batch(() => {
        cy.elements().addClass("faded");
        n.closedNeighborhood().removeClass("faded");
      });
      const partners = n.connectedEdges().not(".filtered").connectedNodes().not(n).map(m => m.data("label")).sort();
      panel.innerHTML = `<h3>${esc(n.data("label"))}</h3><p>${esc(n.data("about"))}</p>` +
        `<p><strong>Partners shown:</strong> ${esc(partners.join(", ") || "none")}</p>` +
        `<p><a href="${STRING}/cgi/network?identifiers=${encodeURIComponent(n.id())}&amp;species=${SPECIES}" target="_blank" rel="noopener">Open ${esc(n.data("label"))} in STRING</a></p>`;
    });
    cy.on("tap", event => { if (event.target === cy) cy.elements().removeClass("faded").unselect(); });

    document.getElementById("relayout").addEventListener("click", runLayout);
    document.getElementById("png").addEventListener("click", () => {
      const a = document.createElement("a");
      a.href = cy.png({ full: true, scale: 2, bg: "#ffffff" });
      a.download = "string-network.png";
      a.click();
    });

    // ----- Loading -----
    async function load(event) {
      if (event) event.preventDefault();
      const names = document.getElementById("ids").value.split(/[\s,;]+/).filter(Boolean);
      const type = document.getElementById("type").value;
      const score = Number(document.getElementById("score").value);
      const add = Math.max(0, Math.min(20, Number(document.getElementById("add").value) || 0));
      try {
        if (new URLSearchParams(location.search).has("offline")) throw new Error("you asked for the saved copy");
        statusLine.textContent = "Asking STRING…";
        const reply = await fromString(names, type, score, add);
        draw(reply);
        // Say which names STRING did not know, and which it read as a different protein
        const matched = new Set(reply.found.map(p => p.queryIndex));
        const missing = names.filter((name, i) => !matched.has(i));
        const renamed = reply.found.filter(p => p.preferredName.toUpperCase() !== p.queryItem.toUpperCase())
                                   .map(p => p.queryItem + " as " + p.preferredName);
        const count = (n, word) => n + " " + word + (n === 1 ? "" : "s");
        statusLine.textContent = `STRING 12.5, ${type} network, score at least ${score}: ` +
          `${count(cy.nodes().length, "protein")} and ${count(cy.edges().length, "link")}.` +
          (missing.length ? ` Not found: ${missing.join(", ")}.` : "") +
          (renamed.length ? ` Read ${renamed.join(", ")}.` : "");
      } catch (err) {
        if (err.keepEmpty) { cy.elements().remove(); statusLine.textContent = err.message; return; }
        console.warn("Showing the saved copy:", err.message);
        try {
          draw(await savedCopy());
          statusLine.textContent = `Showing the saved copy from 15 October 2026 (${err.message}): ` +
            "the starting ten proteins, functional network, score at least 0.7.";
        } catch (copyErr) {     // the saved copy needs a web server: browsers will not read it from a file on disk
          statusLine.textContent = `STRING did not answer (${err.message}), and the saved copy could not be read either.`;
        }
      }
    }
    document.getElementById("query").addEventListener("submit", load);
    load();
  </script>
</body>
</html>

Points to note in the code:

  • Two calls, sometimes three. get_string_ids matches the names to STRING’s proteins. Names it does not know are left out of the reply, so the page lists them. network then returns the links among those proteins, and a third call fetches descriptions of any partners STRING added. STRING asks for one second between calls, and for a caller_identity that names your site.
  • A fixed version. https://version-12-5.string-db.org answers from STRING 12.5, so the network does not change when STRING is updated. STRING’s API documentation recommends a version-specific address like this for anything in production.
  • Evidence. Each link has a combined score and one score per kind of evidence, including escore (experiments), dscore (curated databases) and tscore (text mining), all from 0 to 1. The example draws a link as a solid line when experiments alone score it at least 0.4, dashed when curated databases do, and dotted otherwise. The cut-off of 0.4 is our own choice, not STRING’s.
  • Every protein appears, even one with no links, because the nodes come from the first call rather than from the links. When students raise the minimum score, they see links disappear while the proteins stay.

The network call takes these parameters:

Parameter What it does
identifiers Protein names or STRING IDs, separated by %0d (a carriage return)
species The NCBI taxon ID, such as 9606 for human
required_score The minimum combined score, from 0 to 1000, so 700 matches a score of 0.7 in the replies
network_type functional (the default), physical or regulatory
add_nodes Adds this many of the proteins’ best-scoring partners
caller_identity The name of your site or tool

Three things caught us out:

  • Ask for every protein you need. TP53 with 10 added partners gave AURKA, CSNK2A1, CUL9, DDX5, MAPK1, MAPK14, RPA1, SIRT1, STUB1 and TBP, but not MDM2. At least 60 of p53’s partners share the top score of 0.999, and MDM2 came eleventh in STRING’s list. STRING also adds 10 partners by itself when you give it a single protein. So name every protein you want, rather than relying on STRING to add them.
  • Use gene symbols. STRING read p21 as TCEAL1, a different protein, not CDKN1A. The example reports any name that STRING read as another protein.
  • Scores are confidence, not strength. STRING’s help on scores says the score “represents the confidence that a protein–protein relationship exists”, and does “not measure interaction strength, binding affinity, or the magnitude of the biological effect”. Even a physical link “does not necessarily mean that the two proteins directly bind each other”.

Show a network made in Cytoscape desktop

Cytoscape desktop is an open-source program for building, arranging and styling networks [3]. It can save a network, with its positions and table columns, in the JSON format that Cytoscape.js reads, and it can save the style too. A page can then show the same picture, and readers can click it.

Code cytoscape-desktop.html
<!doctype html>
<html lang="en-GB">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>A network and style saved from Cytoscape desktop</title>
  <script src="https://cdn.jsdelivr.net/npm/cytoscape@3.34.3/dist/cytoscape.min.js"
          integrity="sha384-qPKQxl9uMXOw7vSTUDAnpUilhLuulovw6P5Z4db4bqxW5VhumS7przEmHX0iM0Oc"
          crossorigin="anonymous"></script>
  <style>
    body { font: 15px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; margin: 0; padding: 12px 16px 16px; color: #172033; background: #fff; }
    h1 { font-size: 1.05rem; margin: 0 0 4px; }
    h2 { font-size: 0.95rem; margin: 10px 0 4px; }
    p { margin: 0 0 6px; font-size: 14px; color: #3b4659; }
    #cy { height: 330px; border: 1px solid #c3cbd8; border-radius: 6px; }
    #info { min-height: 1.5em; margin-top: 6px; color: #172033; }
    ul { margin: 0; padding-left: 1.3rem; font-size: 14px; }
    @media (min-width: 600px) { ul { columns: 2; column-gap: 2rem; } }
  </style>
</head>
<body>
  <h1>Six proteins of the p53 pathway, laid out in Cytoscape desktop</h1>
  <p>The network comes from <code>data/p53-pathway.cyjs</code> and its look from <code>data/p53-pathway-style.json</code>.
     Click a protein to see the description stored with it.</p>
  <div id="cy" role="img" aria-label="Six proteins of the p53 pathway. The list under the network gives the same interactions."></div>
  <p id="info" aria-live="polite"></p>
  <h2>Interactions</h2>
  <ul id="list"></ul>

  <script>
    async function getJSON(url) {
      const reply = await fetch(url);
      if (!reply.ok) throw new Error(url + ": HTTP " + reply.status);
      return reply.json();
    }

    Promise.all([getJSON("data/p53-pathway.cyjs"), getJSON("data/p53-pathway-style.json")])
      .then(([network, styles]) => {
        // A style file holds a list of named styles. Each has a Cytoscape.js stylesheet in "style".
        const chosen = styles.find(s => s.title === "p53 teaching style") || styles[0];

        const cy = cytoscape({
          container: document.getElementById("cy"),
          elements: network.elements,                 // nodes and edges; each node carries its saved position
          style: chosen.style.concat([
            // Desktop styles name Java fonts such as "SansSerif", which browsers do not have
            { selector: "node", style: { "font-family": "system-ui, -apple-system, sans-serif" } }
          ]),
          layout: { name: "preset", padding: 30 },    // use the saved positions instead of a new layout
          userZoomingEnabled: false                   // let the mouse wheel scroll the page
        });

        // Columns of Cytoscape's tables arrive as fields in data, here name, Description, interaction and PubMed
        const name = id => cy.getElementById(id).data("name");
        document.getElementById("list").innerHTML = cy.edges().map(e =>
          `<li>${name(e.data("source"))} ${e.data("interaction")} ${name(e.data("target"))} ` +
          `(<a href="https://pubmed.ncbi.nlm.nih.gov/${e.data("PubMed")}/" target="_blank" rel="noopener">PubMed ${e.data("PubMed")}</a>)</li>`).join("");

        cy.on("tap", "node", event => {
          document.getElementById("info").textContent = event.target.data("name") + ": " + event.target.data("Description") + ".";
        });
      })
      .catch(err => {
        document.getElementById("info").textContent = "Could not read the files (" + err.message + "). Open this page from a web server.";
        console.error(err);
      });
  </script>
</body>
</html>

To export the two files, as the Cytoscape manual describes for version 3.10:

  1. Choose File → Export → Network to File…, then the format Cytoscape.js JSON (*.cyjs).
  2. Choose File → Export → Styles to File…, then the format Style for cytoscape.js (*.json).

A few points about the code:

  • The network file has an elements object with nodes and edges, which go straight into cytoscape(). Each node keeps its saved position, and the preset layout uses them.
  • The style file holds a list of named styles, each with a Cytoscape.js stylesheet in style. The example picks one by its title.
  • Table columns become fields in data. Cytoscape uses each element’s SUID, its internal number, as the id, and replaces characters such as spaces in column names with underscores, so “Gene Name” becomes Gene_Name.
  • Fonts. Desktop styles name Java fonts such as SansSerif, which browsers do not have, so the example adds a rule that sets font-family.
  • What is lost. Custom graphics, edge bends, tooltips, node label width and the line type of node borders are not exported, and arrows always take the colour of their edge.

Our two files were written by hand in the format that Cytoscape 3.10 writes, because we could not run Cytoscape desktop where we tested. To check the code against a real export, we also loaded a 331-node network that Cytoscape had exported (gal.cyjs, from the Cytoscape team’s cyjs-sample repository on GitHub) with its styles, and it drew without errors.

Choose a layout, and say what it means

A layout places nodes so that the picture is readable. Unless you choose a layout that encodes something, the positions mean nothing: two proteins drawn close together are not necessarily related, and the protein in the middle is not necessarily the most important. Readers still read closeness as relatedness and the centre as importance [4], so say in the caption what the layout does. This example draws the same ten proteins and 31 links from STRING with six layouts.

Code network-layouts.html
<!doctype html>
<html lang="en-GB">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>One network, six layouts</title>
  <script src="https://cdn.jsdelivr.net/npm/cytoscape@3.34.3/dist/cytoscape.min.js"
          integrity="sha384-qPKQxl9uMXOw7vSTUDAnpUilhLuulovw6P5Z4db4bqxW5VhumS7przEmHX0iM0Oc"
          crossorigin="anonymous"></script>
  <!-- fcose needs layout-base, then cose-base, then cytoscape-fcose, in this order -->
  <script src="https://cdn.jsdelivr.net/npm/layout-base@2.0.1/layout-base.js"
          integrity="sha384-5E2lB9AIGE6LRCnOOSTnZRlYZFZ01iMeN2fw97Z1r4Z/kXALxKw2AC+ZzQqoeDsG"
          crossorigin="anonymous"></script>
  <script src="https://cdn.jsdelivr.net/npm/cose-base@2.2.0/cose-base.js"
          integrity="sha384-RswRBkrMsPUYpJLbZ1CVA08zbNzAkykE2oGJTujBwfjWNdfxv2WVjLJNqv1LhAOp"
          crossorigin="anonymous"></script>
  <script src="https://cdn.jsdelivr.net/npm/cytoscape-fcose@2.2.0/cytoscape-fcose.js"
          integrity="sha384-uk5Wbjq1+KqUdHO30w7N7GrEGdzBhaJeW9o/ANF6v9+yx3M/cBmoX51C000JNCUf"
          crossorigin="anonymous"></script>
  <style>
    body { font: 15px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; margin: 0; padding: 12px 16px 16px; color: #172033; background: #fff; }
    h1 { font-size: 1.05rem; margin: 0 0 4px; }
    p { margin: 0 0 6px; }
    .note { font-size: 14px; color: #3b4659; }
    .toolbar { display: flex; flex-wrap: wrap; gap: 6px; margin: 6px 0; }
    button { font: inherit; font-size: 14px; padding: 4px 9px; }
    button[aria-pressed="true"] { background: #172033; color: #fff; border: 1px solid #172033; border-radius: 3px; }
    #about { min-height: 3em; font-size: 14px; }
    #cy { height: 360px; border: 1px solid #c3cbd8; border-radius: 6px; }
    #degrees { font-size: 14px; color: #3b4659; margin-top: 6px; }
  </style>
</head>
<body>
  <h1>The same ten proteins, drawn six ways</h1>
  <p class="note">Ten proteins and the 31 links between them in STRING 12.5 (combined score at least 0.7). Only the positions change.</p>

  <div class="toolbar" role="group" aria-label="Layouts">
    <button type="button" data-layout="fcose">fcose</button>
    <button type="button" data-layout="cose">cose</button>
    <button type="button" data-layout="concentric">concentric</button>
    <button type="button" data-layout="breadthfirst">breadthfirst</button>
    <button type="button" data-layout="circle">circle</button>
    <button type="button" data-layout="random">random</button>
  </div>
  <p id="about" aria-live="polite"></p>
  <div id="cy" role="img" aria-label="Network of ten proteins. The line under the network gives each protein's number of links."></div>
  <p id="degrees"></p>

  <script>
    const PROTEINS = ["TP53", "MDM2", "MDM4", "ATM", "CHEK2", "CDKN1A", "BAX", "BBC3", "CDKN2A", "EP300"];
    // [protein, protein, STRING combined score], from data/string-p53-network.json
    const LINKS = [
      ["MDM2", "BAX", 0.732], ["MDM2", "CHEK2", 0.994], ["MDM2", "ATM", 0.995], ["MDM2", "EP300", 0.999],
      ["MDM2", "TP53", 0.999], ["MDM2", "MDM4", 0.999], ["MDM2", "CDKN1A", 0.999], ["MDM2", "CDKN2A", 0.999],
      ["EP300", "ATM", 0.72], ["EP300", "CDKN2A", 0.794], ["EP300", "CDKN1A", 0.991], ["EP300", "TP53", 0.999],
      ["TP53", "BBC3", 0.982], ["TP53", "ATM", 0.999], ["TP53", "BAX", 0.999], ["TP53", "MDM4", 0.999],
      ["TP53", "CHEK2", 0.999], ["TP53", "CDKN1A", 0.999], ["TP53", "CDKN2A", 0.999], ["ATM", "CDKN2A", 0.829],
      ["ATM", "CDKN1A", 0.861], ["ATM", "MDM4", 0.976], ["ATM", "CHEK2", 0.999], ["BAX", "CDKN1A", 0.732],
      ["BAX", "BBC3", 0.914], ["MDM4", "CDKN1A", 0.895], ["MDM4", "CHEK2", 0.968], ["MDM4", "CDKN2A", 0.972],
      ["CHEK2", "CDKN1A", 0.85], ["CDKN1A", "BBC3", 0.798], ["CDKN1A", "CDKN2A", 0.961]
    ];

    // What the positions mean in each layout
    const LAYOUTS = {
      fcose: { options: { name: "fcose", randomize: true, nodeRepulsion: 9000, idealEdgeLength: 90 },
               about: "fcose (an extension): linked proteins pull together and all proteins push apart, starting from random positions. Press it again: the picture changes, the links do not." },
      cose: { options: { name: "cose", randomize: true },
              about: "cose (built in): another layout of pulling and pushing, also from a random start. Distances between proteins are not measurements." },
      concentric: { options: { name: "concentric", concentric: node => node.degree(), minNodeSpacing: 20 },
                    about: "concentric (built in): rings by number of links, most in the middle. Here position does show something, because we told it to." },
      breadthfirst: { options: { name: "breadthfirst", roots: "#TP53", spacingFactor: 1.1 },
                      about: "breadthfirst (built in): rows by the number of steps from TP53, which we chose as the root. Every other protein is one step away." },
      circle: { options: { name: "circle" },
                about: "circle (built in): the proteins in the order of our list, round a circle. The positions mean nothing." },
      random: { options: { name: "random" },
                about: "random (built in): positions picked at random. The links are the same as in every other layout." }
    };

    const cy = cytoscape({
      container: document.getElementById("cy"),
      elements: [
        ...PROTEINS.map(id => ({ data: { id } })),
        ...LINKS.map(([a, b, score]) => ({ data: { id: a + "-" + b, source: a, target: b, score } }))
      ],
      style: [
        { selector: "node", style: {
            "label": "data(id)", "font-size": 12, "color": "#172033", "text-valign": "center",
            "width": 56, "height": 56, "background-color": "#e9dff3", "border-width": 2, "border-color": "#7b3fa0" } },
        { selector: "edge", style: {
            "curve-style": "straight", "line-color": "#8a93a3", "width": "mapData(score, 0.7, 1, 1, 4)" } }
      ],
      layout: { name: "preset" },      // positions are set by the first button press below
      userZoomingEnabled: false,       // let the mouse wheel scroll the page
      autounselectify: true
    });

    const motion = !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    const buttons = document.querySelectorAll("button[data-layout]");

    function use(name) {
      const layout = LAYOUTS[name];
      cy.layout({ ...layout.options, animate: motion, animationDuration: 500, padding: 24 }).run();
      document.getElementById("about").textContent = layout.about;
      buttons.forEach(b => b.setAttribute("aria-pressed", String(b.dataset.layout === name)));
    }
    buttons.forEach(b => b.addEventListener("click", () => use(b.dataset.layout)));

    // The number of links is a property of the network, so it is the same in every layout
    document.getElementById("degrees").textContent = "Links per protein, the same in every layout: " +
      cy.nodes().sort((x, y) => y.degree() - x.degree()).map(n => n.id() + " " + n.degree()).join(", ") + ".";

    use("fcose");
  </script>
</body>
</html>
Layout Where it comes from What the positions mean
fcose cytoscape-fcose 2.2.0 [5] Little: linked nodes pull together and all nodes push apart, from random starting positions
cose Built in The same idea, also from a random start
dagre cytoscape-dagre 4.0.1 Rows follow the direction of the edges; the order within a row only tidies the drawing
breadthfirst Built in Rows by the number of steps from the root nodes you choose
concentric Built in Rings by a number you choose, such as the number of links
circle, grid Built in The order of your list, and nothing else
random Built in Nothing
preset Built in Positions you supply, for example from Cytoscape desktop

Load the extensions after Cytoscape.js. fcose needs two helper libraries, loaded first in this order. cytoscape-dagre 4 is a single file that includes dagre itself, so the separate dagre script in older tutorials is no longer needed. Both extensions register themselves when they are loaded with a script tag.

<script src="https://cdn.jsdelivr.net/npm/cytoscape@3.34.3/dist/cytoscape.min.js"
        integrity="sha384-qPKQxl9uMXOw7vSTUDAnpUilhLuulovw6P5Z4db4bqxW5VhumS7przEmHX0iM0Oc"
        crossorigin="anonymous"></script>

<!-- fcose: layout-base, then cose-base, then cytoscape-fcose -->
<script src="https://cdn.jsdelivr.net/npm/layout-base@2.0.1/layout-base.js"
        integrity="sha384-5E2lB9AIGE6LRCnOOSTnZRlYZFZ01iMeN2fw97Z1r4Z/kXALxKw2AC+ZzQqoeDsG"
        crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/cose-base@2.2.0/cose-base.js"
        integrity="sha384-RswRBkrMsPUYpJLbZ1CVA08zbNzAkykE2oGJTujBwfjWNdfxv2WVjLJNqv1LhAOp"
        crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/cytoscape-fcose@2.2.0/cytoscape-fcose.js"
        integrity="sha384-uk5Wbjq1+KqUdHO30w7N7GrEGdzBhaJeW9o/ANF6v9+yx3M/cBmoX51C000JNCUf"
        crossorigin="anonymous"></script>

<!-- dagre: one file -->
<script src="https://cdn.jsdelivr.net/npm/cytoscape-dagre@4.0.1/dist/cytoscape-dagre.min.js"
        integrity="sha384-CIu9EFddQ0GplFvevqHZ1sIPo1IRXFvxHd0A6jaJ3m052fy+mDfer/ItnvIS5uY3"
        crossorigin="anonymous"></script>

The three fcose files add 324 kB (69 kB compressed), and dagre adds 46 kB (16 kB compressed). Run a layout with cy.layout({ name: "fcose" }).run(), adding any options to the object. In fcose, randomize: true is the default, so each run gives a different picture; quality: "proof" takes longer for a tidier result. When students will compare screenshots, use a layout that gives the same picture every time, such as dagre, or save the positions and use preset.

Save the network as a picture

cy.png() returns the drawing as a PNG image for slides and handouts. Ask for the whole network rather than the part on screen, at twice the screen resolution, and on a white background, because the image is otherwise transparent:

const link = document.createElement("a");
link.href = cy.png({ full: true, scale: 2, bg: "#ffffff" });   // a data: address that holds the image
link.download = "p53-pathway.png";
link.click();

The pathway example’s “Download PNG” button does this, and gave an image of about 85 kB in our tests. For large networks, output: "blob-promise" returns a Blob instead of a data address.

Give a text version of the network

Cytoscape.js draws on a canvas, so screen readers find nothing in the drawing, and keyboard users cannot reach the nodes. Give the container a short description with role="img" and aria-label, and put the full information in a table near it [6]. Build the table from the same lists as the diagram, so that the two always agree:

// one row per interaction, from the same lists of nodes and edges that draw the diagram
const name = id => nodes.find(n => n.id === id).label;
document.getElementById("edge-table").innerHTML = edges.map(e =>
  `<tr><td>${name(e.source)}</td><td>${e.effect}</td><td>${name(e.target)}</td><td>${e.detail}</td></tr>`
).join("");

Keep the table small enough to read with a keyboard: Smits and colleagues cite a study in which blind screen-reader users had trouble with tables of up to 6 columns and 61 rows [6]. The pathway example also has a real button for each molecule, and both the pathway and STRING examples put the details of the selected node in an element with aria-live="polite", so screen readers read them out.

Things that catch people out

  • Phones. We checked every example at 358 and 412 pixels wide. Side panels move under the network on narrow screens, and wide tables sit in a box with overflow-x: auto, so nothing scrolls sideways. On a touch screen, dragging on the network moves the network.
  • The scroll wheel. Over a network, the mouse wheel zooms the network and the page stops scrolling. With userZoomingEnabled: false, the wheel scrolled the page in our test. All the examples here do this except the STRING one, where a dense network may need zooming.
  • Pictures that change. fcose, cose and random start from random positions, so every load gives a new picture. That makes a good demonstration, but it confuses students comparing screenshots.
  • :visible can be out of date. After we hid 3 of 12 edges inside cy.batch(), the :visible selector still counted 12, even after the batch had ended. Select by your own classes instead, as the examples do with .not(".hidden").
  • Warnings. Setting width: "label", which sized a node to its label, is deprecated and prints a warning, as does a custom wheelSensitivity. The pathway example sizes nodes with a function instead.
  • Hairballs. Cytoscape.js can draw hundreds of nodes, but nobody can read them. Show less detail, fade what matters less, or split the network into several figures [4].
  • STRING in class. STRING’s help pages ask heavy users to download the full data set rather than call the API many times. For a large class, point students at the saved copy (?offline=1) or save your own copy of the replies.
  • Versions. Each example names an exact version of every library, and STRING’s address names its version. When you update, read the release notes and test the page again.

Licences and credits

Cytoscape.js, cytoscape-dagre, cytoscape-fcose, layout-base and cose-base are all under the MIT licence. If you publish teaching material built on them, cite the Cytoscape.js paper [1] and, if you use fcose, its paper [5]. STRING’s data are free to use under the Creative Commons Attribution 4.0 licence, on condition that you credit STRING and cite it [2]; the STRING example does this under the network. If you use Cytoscape desktop, cite its paper [3]. The sources for the p53 pathway are in the table in the pathway section.

References

  1. Franz M, Lopes CT, Fong D, et al. (2023). Cytoscape.js 2023 update: a graph theory library for visualization and analysis. Bioinformatics 39(1): btad031. doi:10.1093/bioinformatics/btad031
  2. Szklarczyk D, Nastou K, Koutrouli M, et al. (2025). The STRING database in 2025: protein networks with directionality of regulation. Nucleic Acids Research 53(D1): D730–D737. doi:10.1093/nar/gkae1113
  3. Shannon P, Markiel A, Ozier O, et al. (2003). Cytoscape: a software environment for integrated models of biomolecular interaction networks. Genome Research 13(11): 2498–2504. doi:10.1101/gr.1239303
  4. Marai GE, Pinaud B, Bühler K, Lex A, Morris JH (2019). Ten simple rules to create biological network figures for communication. PLOS Computational Biology 15(9): e1007244. doi:10.1371/journal.pcbi.1007244
  5. Balci H, Dogrusoz U (2022). fCoSE: a fast compound graph layout algorithm with constraint support. IEEE Transactions on Visualization and Computer Graphics 28(12): 4582–4593. doi:10.1109/TVCG.2021.3095303
  6. Smits TC, Weru L, Gehlenborg N, L’Yi S (2025). Ten simple rules for making biomedical data resources accessible. PLOS Computational Biology 21(11): e1013657. doi:10.1371/journal.pcbi.1013657