javascriptleafletcytoscape.js

Integrating CytoscapeJs with LeafletJs


I have made a graph component in CytoscapeJS and want to have a map as an overlay. Basically I want to represent the nodes based on their co-ordinates on Map (using LeafletJs). I have looked into the plugin (cytoscape-mapbox-gl by zakjan) and cytoscape-leaf extension (using this currently).

I am getting this error:

Uncaught TypeError: obj.attachEvent is not a function Uncaught TypeError: Cannot read properties of undefined (reading 'lat') Uncaught Error: Map container is already initialized.

I only want to integrate CytoscapeJs with LeafletJS. I don't want to add any tile layer.

Followed all the steps on the Cytoscape-leaf plugin documentation. I have registered the function, created the instance, added the co-ordinates in the data object,rendered the Map component alongside cytoscape component, Added the lat,lng field in the data object for cytoscape node(Node Position).

cytoscape.use( leaflet );
   const map = useRef();
data: {
          id: ele.nodeAddr,
          label: `IP-${ele.nodeAddr}`,
          icon:Server,
          type:'parentNode',
          status:ele.status,
          lat:19.5,
          lng:72.8777,
          
        }
 const options = {
          
          container: map.current,
        
          // the data field for latitude
          latitude: 'lat',
        
          // the data field for longitude
          longitude: 'lng'
        };
        
        const leaf = cy.leaflet(options);
<div
          ref={divRef}
          style={{
            border: "1px solid",
            backgroundColor: "#f5f6fe",
            height: '600px',
          }}
        >
           <div ref={map}></div>

Solution

  • My answer is independent of your code and your intended plugins you want to use with cytoscape. It may not answer your question, but it works to some extent that I consider it a good start to use cytoscapeJS within leaflet.

    One issue that I found, it wont work with browsers on iOS or ipadOS. I am sorry about that. Help is welcome to solve the issues why Webkit based browsers on iOS/ipadOS do not handle cytoscape's graph like Chrome browsers on android/windows.

    Code:

    const base_dc = 0.027589;
    const lon_ext = 0.5436;
    const lat_ext = 0.5436;
    const lonmin = 100.3257;
    const latmin = 13.4978;
    const lon_cor = lon_ext * base_dc;
    const lonmax = lonmin + lon_ext;
    const lonmid = (lonmin + lonmax) / 2;
    const lonmax2 = lonmax + lon_cor;
    const latmax = latmin + lat_ext;
    const latmid = (latmin + latmax) / 2;
    
    const latlng = { lat: latmin, lng: lonmin };
    const latlng2 = { lat: latmax, lng: lonmax2 };
    const clatlng = { lat: latmid, lng: lonmid };
    const cbottom = { lat: latmin, lng: lonmid };
    
    var zoom = 9;
    const myRenderer = L.canvas({ padding: 0.0 }); //svg or canvas
    
    /* Cytoscape global vars */
    // must match SVG viewbox w h.
    // and width, height of #cyOnSvg DIV
    const cy_size = 1600;
    const params = {
      width: cy_size,
      height: cy_size,
      latmin: latmin,
      latmax: latmax,
      lonmin: lonmin,
      lonmax: lonmax2
    };
    
    // Update CSS to use new cy_size
    let root = document.documentElement;
    root.style.setProperty("--svg-width", cy_size + "px");
    root.style.setProperty("--svg-height", cy_size + "px");
    
    /* choice of maps */
    var mymap = L.map("mapid", {
      renderer: myRenderer,
      preferCanvas: false
    }).setView(clatlng, zoom);
    
    L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
      attribution: "Map data &copy; OpenStreetMap contributors"
    }).addTo(mymap);
    
    const svg_pin =
      '<svg width="24" height="24" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M12,11.5A2.5,2.5 0 0,1 9.5,9A2.5,2.5 0 0,1 12,6.5A2.5,2.5 0 0,1 14.5,9A2.5,2.5 0 0,1 12,11.5M12,2A7,7 0 0,0 5,9C5,14.25 12,22 12,22C12,22 19,14.25 19,9A7,7 0 0,0 12,2Z" fill="firebrick"></path></svg>';
    const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin);
    
    const svgpin_Icon = L.icon({
      iconUrl: svgpin_Url,
      iconSize: [24, 24],
      iconAnchor: [12, 24],
      popupAnchor: [0, -22]
    });
    
    var marker2 = L.marker(latlng2, {
      renderer: myRenderer,
      icon: svgpin_Icon
      //draggable: true,
      //autoPan: true
    }).addTo(mymap);
    marker2.bindPopup("<b>Control_UR</b>").openPopup();
    marker2.openPopup();
    
    var marker0 = L.marker(latlng, {
      renderer: myRenderer,
      icon: svgpin_Icon
    }).addTo(mymap);
    marker0.bindPopup("<b>Control_LL</b>").openPopup();
    marker0.openPopup();
    
    /* Embed SVG Element in the web page */
    let svgElem = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    svgElem.setAttribute("xmlns", "http://www.w3.org/2000/svg");
    svgElem.setAttribute("id", "Svg0");
    
    svgElem.setAttribute("preserveAspectRatio", "xMinYMax slice");
    svgElem.setAttribute("viewBox", `0 0 ${cy_size} ${cy_size}`);
    
    /* SVGOverlay with foreignObject-div-text */
    svgElem.innerHTML = `<foreignObject x="0" y="0" width="100%" height="100%">
    <div id="cyOnSvg" style="background-color:rgba(200,200,200,0.25);padding:0;"></div>
    <div id="upperlefttext">Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</div>
    </foreignObject>`;
    // Note: lonmax2 (=lonmax + correction)
    const svgElementBounds = [
      [latmin, lonmin],
      [latmax, lonmax2]
    ];
    const svgobj = L.svgOverlay(svgElem, svgElementBounds, {
      renderer: myRenderer,
      zIndex: 15,
      opacity: 0.65,
      interactive: false
    }).addTo(mymap);
    
    //svgobj.bindPopup("SVG vector layer");
    //var draggable = new L.Draggable(svgElem);
    //draggable.enable(); //make draggable (must set=> interactive: true)
    /* SVG OK */
    
    /* set events */
    mymap.on("click", onMapClick);
    //svgobj.on("click", onSvgClick);
    
    /* other useful settings */
    mymap.scrollWheelZoom.disable();
    const popup = L.popup(); //leaflet popup object
    
    function onMapClick(e) {
      popup.setLatLng(e.latlng).setContent(e.latlng.toString()).openOn(mymap);
    }
    
    /* -------cytoscape-------- */
    function lnglat2xy(lon, lat, pars) {
      //let w = pars.width;
      //let h = pars.height;
      let L = pars.lonmax - pars.lonmin; //lonmax2-lonmin
      let B = pars.latmax - pars.latmin;
      let y = ((B - (lat - pars.latmin)) * pars.height) / B;
      let x = ((lon - pars.lonmin) * pars.width) / L;
      return { x: x, y: y };
    }
    
    const lngMidLatMid = lnglat2xy(
      (lonmin + lonmax2) / 2,
      (latmin + latmax) / 2,
      params
    );
    
    //console.log("lngMidLatMid: "+lngMidLatMid.x+"; "+lngMidLatMid.y);
    const bkk = lnglat2xy(100.5348327, 13.7567441, params);
    const bda = lnglat2xy(100.4094999, 13.7108552, params);
    const hoc = lnglat2xy(100.6076988, 13.5676449, params);
    const han = lnglat2xy(100.7522551, 13.6982529, params);
    const nay = lnglat2xy(100.4081876, 13.8923587, params);
    
    var cy = cytoscape({
      container: document.querySelector("#cyOnSvg"),
      elements: {
        nodes: [
          {
            data: { id: "LL", name: "LowerLeft" },
            classes: "controlpoint",
            position: { x: 0, y: cy_size }
          },
          {
            data: { id: "UL", name: "UpperLeft" },
            classes: "controlpoint",
            position: { x: 0, y: 0 }
          },
          {
            data: { id: "UR", name: "UpperRight" },
            classes: "controlpoint",
            position: { x: cy_size, y: 0 }
          },
          {
            data: { id: "LR", name: "LowerRight" },
            classes: "controlpoint",
            position: { x: cy_size, y: cy_size }
          },
          /* Nodes with (long,lat) coordinates */
          { data: { id: "bkk", name: "A8" }, position: { x: bkk.x, y: bkk.y } },
          { data: { id: "bda", name: "BL38" }, position: { x: bda.x, y: bda.y } },
          { data: { id: "hoc", name: "E23" }, position: { x: hoc.x, y: hoc.y } },
          { data: { id: "han", name: "A1" }, position: { x: han.x, y: han.y } },
          { data: { id: "nay", name: "PP01" }, position: { x: nay.x, y: nay.y } },
        ],
        edges: [
          {
            data: { id: "LLUR", source: "LL", target: "UR" },
            classes: "controlline"
          },
          {
            data: { id: "ULLR", source: "UL", target: "LR" },
            classes: "controlline"
          },
          {
            data: { id: "bkk_bda", source: "bkk", target: "bda" },
            classes: "edge"
          },
          {
            data: { id: "bkk_han", source: "bkk", target: "han" },
            classes: "edge"
          },
          {
            data: { id: "bkk_hoc", source: "bkk", target: "hoc" },
            classes: "edge"
          },
          {
            data: { id: "bkk_nay", source: "bkk", target: "nay" },
            classes: "edge"
          },
        ]
      },
      style: [
          {
          selector: "node",
          style: {
            shape: "hexagon",
            width: "50px",
            height: "50px",
            "background-color": "blue",
            label: "data(name)",
            opacity: 1,
            "text-background-color": "yellow"
          }
        },
          {
          selector: ".controlline",
          style: {
            width: "2px",
            "line-color": "black",
            opacity: 1
          }
        },
          {
          selector: ".edge",
          style: {
            width: "3px",
            "line-color": "red",
            opacity: 1
          }
        },
      ],
        layout: {
        name: "preset"
      }
      });
    
    cy.pan({ x: 0.0, y: 0.0 }); //match TOP-LEFT corner
    cy.fit(cy.$("#LLUR"));
    :root {
      --svg-width: 1600px;
      --svg-height: 1600px;
    }
    
    #cyOnSvg{
      /* foreignObject, and canvas */
      position: absolute;
      width: 100%; //5%;
      height: 100%; //10%;
      top: 0;
      left: 0;
      background-color: lightgray;
      overflow: visible;
      margin: 0;
      padding: 0;
    }
    
    #Svg0{
      /* svg elem */
      position: absolute;
      width: var(--svg-width);
      height: var(--svg-height);
      top: 0;
      left: 0;
      display: block;
      background-color: lightyellow;
      /*border: 0.5px solid gray;
      border-style: dashed;*/
      overflow: visible;
      margin: 0;
      padding: 0;
    }
    
    #mapid { 
      height: 480px; 
      width: 480px;
    }
    
    #lorem {
      width: var(--svg-width);
      position: absolute;
      background-color: gray;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/leaflet.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.9.2/cytoscape.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-browser/0.1.0/jquery.browser.min.js"></script>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.5.1/dist/leaflet.css" integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==" crossorigin="" />
    
    <div id="mapid"></div>