Wiki Discussion: Generating Cytoscape.js graphs with SWI-Prolog

While learning how to use ChatGPT with coding created a better base set of code and files for my Cytoscape.js with SWI-Prolog example.

The code is not in an expandable text section as that hides it from search indexing and the code needs to be found via a search.

Note: The directory C:/Users/Groot is just an example you can choose another directory just make sure that files can be created and written in the directory by the user.

This examples needs an HTML server. To start the server:

Welcome to SWI-Prolog (threaded, 64 bits, version 8.5.15)
...

?- working_directory(_,'C:/Users/Groot').
true.

?- [server].
% Started server at http://localhost:8080/
true.

Then using an internet browser: http://localhost:8080/london_tube.html


Here are the needed files.

File: server.pl

:- module(server, [start/0]).

% -----------------------------------------------------------------------------

:- use_module(library(http/thread_httpd)).
:- use_module(library(http/http_dispatch)).

% ----------------------------------------------------------------------------

% Start the server when the code is loaded.
:- initialization(start).

% Start the server on port 8080
start :-
    http_server(http_dispatch, [port(8080)]).

% ----------------------------------------------------------------------------

%               URL                                File name
:- http_handler('/london_tube.html', http_reply_file('london_tube.html', []), []).
:- http_handler('/elements.json'   , http_reply_file('elements.json',    []), []).
:- http_handler('/style.json'      , http_reply_file('style.json',       []), []).
:- http_handler('/script.js'       , http_reply_file('script.js',        []), []).
:- http_handler('/style.css'       , http_reply_file('style.css',        []), []).
:- http_handler('/favicon.ico'     , http_reply_file('favicon.ico',      []), []).

File: london_tube.html

<html>
<head>
  <link rel="icon" type="image/x-icon" href="./favicon.ico">
  <link rel="stylesheet" href="style.css">
  <script src="https://cdn.jsdelivr.net/npm/cytoscape/dist/cytoscape.min.js"></script>
  <script src="script.js"></script>
</head>
<body>
  <!-- final -->
  <div id="cy"></div>
</body>
</html>

File: script.js

async function initCytoscape() {
  try {
    const elementsPromise = fetch("elements.json")
      .then(res => {
        if (!res.ok) {
          throw new Error(`Unable to fetch elements.json: ${res.status} ${res.statusText}`);
        }
        return res.json();
      })
      .catch(error => {
        console.error(error);
      });

    const stylePromise = fetch("style.json")
      .then(res => {
        if (!res.ok) {
          throw new Error(`Unable to fetch style.json: ${res.status} ${res.statusText}`);
        }
        return res.json();
      })
      .catch(error => {
        console.error(error);
      });

    const layoutPromise = fetch("layout.json")
      .then(res => {
        if (!res.ok) {
          throw new Error(`Unable to fetch layout.json: ${res.status} ${res.statusText}`);
        }
        return res.json();
      })
      .catch(error => {
        console.error(error);
      });

    const [elements, style, layout] = await Promise.all([elementsPromise, stylePromise, layoutPromise]);

    var cy = cytoscape({
      container: document.getElementById("cy"),
      elements: elements,
      style: style,
      layout: layout
    });
  } catch (error) {
    console.error(error);
  }
}

initCytoscape();

File: style.css

#cy {
    width: 100%;
    height: 100%;
    position: absolute;
    top: 0px;
    left: 0px;
}

File: favicon.ico
Just copy one here. There is one installed for SWI-Prolog that works if you need one.

There are three JSON files used with Cytoscape.js and are created using london_tube.pl and cytoscape.pl.

To create the three JSON files

Welcome to SWI-Prolog (threaded, 64 bits, version 8.5.15)
...

?- working_directory(_,'C:/Users/Groot').
true.

?- [london_tube].
true.

?- halt.

File: london_tube.pl

:- module(london_tube,[]).

% -----------------------------------------------------------------------------

:- prolog_load_context(directory, Dir),
asserta(user:file_search_path(myapp, Dir)).

user:file_search_path(data,myapp('Data')).

% ----------------------------------------------------------------------------

:- use_module(myapp(cytoscape)).

% ----------------------------------------------------------------------------

% Using the London Tube Map from: https://cdn.londonandpartners.com/-/media/files/london/visit/maps-and-guides/tube-maps/tube_map_november_2022.pdf?la=en

% ----------------------------------------------------------------------------

% Automatically run the code. No need to load the code then run a query.
:- initialization(main).

main :-
    % Convert the line/3 predicates into connection/2 predicates.
    forall(line(name(Line), _, _), makeConnections(Line)),
    create_json_files.

% ----------------------------------------------------------------------------

create_json_files :-
    absolute_file_name(myapp(elements), Elements_path, [extensions([json]), access(write)]),
    write_elements_json(nodes,edges,Elements_path),
    absolute_file_name(myapp(style), Style_path, [extensions([json]), access(write)]),
    write_style_json(graph_style,Style_path),
    absolute_file_name(myapp(layout), Layout_path, [extensions([json]), access(write)]),
    write_layout_json(graph_layout,Layout_path).

% ----------------------------------------------------------------------------

line(name('Elizabeth'),color('Purple'),stations(['Bond Street','Tottenham Court Road'])).
line(name('Central'),color('Red'),stations(['Bond Street','Oxford Circus','Tottenham Court Road','Holborn'])).
line(name('Piccadilly'),color('Dark Blue'),stations(['Hyde Park Corner','Green Park','Piccadilly Circus','Leicester Square','Covent Garden','Holborn'])).
line(name('Circle'),color('Yellow'),stations(['Sloane Square','Victoria','St James\'s Park','Westminster','Embankment','Temple'])).
line(name('District'),color('Green'),stations(['Sloane Square','Victoria','St James\'s Park','Westminster','Embankment','Temple'])).
line(name('Jubilee'),color('Gray'),stations(['Bond Street','Green Park','Westminster'])).
line(name('Bakerloo'),color('Brown'),stations(['Regent\'s Park','Oxford Circus','Piccadilly Circus','Charing Cross','Embankment'])).
line(name('Victoria'),color('Light Blue'),stations(['Oxford Circus','Green Park','Victoria'])).
line(name('Northern'),color('Black'),stations(['Goddge Street','Tottenham Court Road','Leicester Square','Charing Cross','Embankment'])).

makeConnections(Line) :-
    line(name(Line), _, stations(Stations)),
    makeConnections(Stations).

:- dynamic connection/2.

makeConnections([_]).
makeConnections([Start,End|Rest]) :-
    (
        connection(Start,End), !
    ;
        assert(connection(Start, End))
    ),
    makeConnections([End|Rest]).

% -------

% https://js.cytoscape.org/#notation/elements-json

nodes(Nodes) :-
    findall(node(X), (connection(X, _); connection(_, X)), TempNodes),
    sort(TempNodes, Nodes).

edges(Edges) :-
    findall(edge(X, Y), connection(X, Y), TempEdges),
    sort(TempEdges, Edges).

% -------

% https://js.cytoscape.org/#style

% Note:
%   graph_style(selector(node),property('label'           , 'data(id)' )).
%   added automatically.
graph_style(selector(node),property('background-color', '#666'     )).
graph_style(selector(edge),property('curve-style'     , haystack   )).
graph_style(selector(edge),property('line-color'      , '#ccc'     )).

% -------

% https://js.cytoscape.org/#layouts

graph_layout(property('name',grid)).

File: cytoscape.pl

:- module(cytoscape,
    [
        write_elements_json/3,
        write_style_json/2,
        write_layout_json/2
    ]).

% -----------------------------------------------------------------------------

:- use_module(library(http/json)).
:- use_module(library(dict)).

% ----------------------------------------------------------------------------

% https://js.cytoscape.org/#notation/elements-json

node_properties_dict(node(Name), Dict) :-
    dict_create(Dict, _, ['id'-Name]).

node_dict(Name, Dict) :-
    node_properties_dict(Name, Node_properties),
    dict_create(Dict, _, ['data'-Node_properties]).

edge_properties_dict(edge(Source, Target), Dict) :-
    dict_create(Dict, _, ['source'-Source, 'target'-Target]).

edge_dict(Edge, Dict) :-
    edge_properties_dict(Edge, Edge_properties),
    dict_create(Dict, _, ['data'-Edge_properties]).

nodes_dict(Nodes_goal,Nodes_dict) :-
    call(Nodes_goal,Nodes),
    maplist(node_dict,Nodes,Node_dicts),
    dict_create(Nodes_dict,_,['nodes'-Node_dicts]).

edges_dict(Edges_goal,Edges_dict) :-
    call(Edges_goal,Edges),
    maplist(edge_dict,Edges,Edge_dicts),
    dict_create(Edges_dict,_,['edges'-Edge_dicts]).

elements_dict(Nodes_goal,Edges_goal,Elements_dict) :-
    nodes_dict(Nodes_goal,Nodes_dict),
    edges_dict(Edges_goal,Edges_dict),
    put_dict(Nodes_dict,Edges_dict,Elements_dict).

:- meta_predicate write_elements_json(:,:,?).

write_elements_json(Nodes_goal,Edges_goal,Path) :-
    elements_dict(Nodes_goal,Edges_goal,Elements),
    setup_call_cleanup(
        open(Path, write, Stream, [encoding(utf8)]),
        json_write_dict(Stream, Elements),
        close(Stream)
    ).

% -------------------------------------

% https://js.cytoscape.org/#style

style_dicts(M:_,[Dict1,Dict3]) :-
    Node_properties = [label-'data(id)'|T1],
    findall(Key-Value,( M:graph_style(selector(node),property(Key,Value)) ),T1,[]),
    dict_create(Dict0,_,Node_properties),
    dict_create(Dict1,_,[selector-node,style-Dict0]),
    findall(Key-Value,( M:graph_style(selector(edge),property(Key,Value)) ),Edge_properties),
    dict_create(Dict2,_,Edge_properties),
    dict_create(Dict3,_,[selector-edge,style-Dict2]).

:- meta_predicate write_style_json(:,?).

write_style_json(Goal,Path) :-
    style_dicts(Goal,Styles),
    setup_call_cleanup(
        open(Path, write, Stream, [encoding(utf8)]),
        json_write_dict(Stream, Styles),
        close(Stream)
    ).

% -------

% https://js.cytoscape.org/#layouts

layout_dicts(M:_,Dict) :-
    findall(Key-Value,( M:graph_layout(property(Key,Value)) ),Layout_properties),
    dict_create(Dict,_,Layout_properties).

:- meta_predicate write_layout_json(:,?).

write_layout_json(Goal,Path) :-
    layout_dicts(Goal,Layout),
    setup_call_cleanup(
        open(Path, write, Stream, [encoding(utf8)]),
        json_write_dict(Stream,Layout),
        close(Stream)
    ).

Versions of the JSON files, if needed.

File: elements.json

{
  "edges": [
    {"data": {"source":"Bond Street", "target":"Green Park"}},
    {"data": {"source":"Bond Street", "target":"Oxford Circus"}},
    {
      "data": {"source":"Bond Street", "target":"Tottenham Court Road"}
    },
    {"data": {"source":"Charing Cross", "target":"Embankment"}},
    {"data": {"source":"Covent Garden", "target":"Holborn"}},
    {"data": {"source":"Embankment", "target":"Temple"}},
    {
      "data": {"source":"Goddge Street", "target":"Tottenham Court Road"}
    },
    {"data": {"source":"Green Park", "target":"Piccadilly Circus"}},
    {"data": {"source":"Green Park", "target":"Victoria"}},
    {"data": {"source":"Green Park", "target":"Westminster"}},
    {"data": {"source":"Hyde Park Corner", "target":"Green Park"}},
    {"data": {"source":"Leicester Square", "target":"Charing Cross"}},
    {"data": {"source":"Leicester Square", "target":"Covent Garden"}},
    {"data": {"source":"Oxford Circus", "target":"Green Park"}},
    {"data": {"source":"Oxford Circus", "target":"Piccadilly Circus"}},
    {
      "data": {"source":"Oxford Circus", "target":"Tottenham Court Road"}
    },
    {"data": {"source":"Piccadilly Circus", "target":"Charing Cross"}},
    {
      "data": {"source":"Piccadilly Circus", "target":"Leicester Square"}
    },
    {"data": {"source":"Regent's Park", "target":"Oxford Circus"}},
    {"data": {"source":"Sloane Square", "target":"Victoria"}},
    {"data": {"source":"St James's Park", "target":"Westminster"}},
    {"data": {"source":"Tottenham Court Road", "target":"Holborn"}},
    {
      "data": {"source":"Tottenham Court Road", "target":"Leicester Square"}
    },
    {"data": {"source":"Victoria", "target":"St James's Park"}},
    {"data": {"source":"Westminster", "target":"Embankment"}}
  ],
  "nodes": [
    {"data": {"id":"Bond Street"}},
    {"data": {"id":"Charing Cross"}},
    {"data": {"id":"Covent Garden"}},
    {"data": {"id":"Embankment"}},
    {"data": {"id":"Goddge Street"}},
    {"data": {"id":"Green Park"}},
    {"data": {"id":"Holborn"}},
    {"data": {"id":"Hyde Park Corner"}},
    {"data": {"id":"Leicester Square"}},
    {"data": {"id":"Oxford Circus"}},
    {"data": {"id":"Piccadilly Circus"}},
    {"data": {"id":"Regent's Park"}},
    {"data": {"id":"Sloane Square"}},
    {"data": {"id":"St James's Park"}},
    {"data": {"id":"Temple"}},
    {"data": {"id":"Tottenham Court Road"}},
    {"data": {"id":"Victoria"}},
    {"data": {"id":"Westminster"}}
  ]
}

File: style.json

[
  {
    "selector":"node",
    "style": {"background-color":"#666", "label":"data(id)"}
  },
  {
    "selector":"edge",
    "style": {"curve-style":"haystack", "line-color":"#ccc"}
  }
]

File: layout.json

{"name":"grid"}