HTML displaying live a graph whilist the user is annotating

I am implementing a relations custom recipe with relations_span_labels as well.
I want below the relations task to render an HTML page where whenever an annotation is made (either span or relation) it will display a PyVis graph based on the annotations. My question, more specifically, is:

  1. How can I get the annotations to feed on the JS, whenever the user makes a new annotation?
  2. Where do I insert the JS logic?

Do the above make sense? Thanks in advance!

Hi! In general, that should be possible – I guess the only concern would be that depending on the complexity of the annotations, the graph could make the UI too complex. But this could probably be solved by choosing the right layout for it so it's helpful but not distracting.

You can provide custom JavaScript via the "javascript" config setting: https://prodi.gy/docs/custom-interfaces#javascript

The prodigyupdate event is fired whenever the current example is updated. So you can do something like this to listen to it and access the current example JSON via window.prodigy.content:

document.addEventListener('prodigyupdate', event => {
    const eg = window.prodigy.content
    // Do something with the annotations here
    console.log('Updated', eg)
})

For example, if your UI contains a HTML block with the container for the visualization, you could re-generate it and update it here. I don't know how the PyVis JavaScript API works in detail but ideally, you'd want to generate the visualization (static if possible, e.g. an SVG) and then place it into a HTML container you've defined in the UI, e.g. as a html block.

If you need to include a larger JavaScript library, the easiest way is to modify the static/index.html included with Prodigy. This thread has some more background: Import HTML Template And React

Hello @ines! Thanks a lot! You understood the concept, the JS event and the thread you sent me are the things I was looking for!

It took me some time to figure out as I am not front-end developer. Anyways, how I did it was the following, in case anyone else is interested.

In the Prodigy's static/index.html file I included the following:

<html lang="en">
 <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <link rel="shortcut icon" href="favicon.ico">
    <style type="text/css">
       #mynetwork {
        width: 1520px;
        height: 900px;
        border: 1px solid lightgray;
      }
    </style>
    <title>Prodigy</title>
</head>

<body>
    <div id="root"></div>
    <script src="bundle.js"></script>
    <script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js">
</script>
</body>
</html>

Essentially the CSS (<style>) #network thingy in the head to create the border for the visualisation. Then to import the VisJS lib is the second script under the default bundle.js.

Then in my custom recipe.py, I import both the HTML template and the Javascript in the config.

...
with open('local_index.html') as f:
    html_template = f.read()

with open('script.js') as f:
    javascript = f.read()
...
return {
    "dataset": dataset,          # the dataset to save annotations to
    "view_id": "blocks",         # set the view_id to "blocks"
    "stream": stream,            # the stream of incoming examples
    "config": {
        "relations_span_labels": [...],
        "labels": [...],
        'html_template': html_template,
        'javascript': javascript,
        "blocks": blocks         # add the blocks to the config
    }

In my HTML template file local_index.html, I just inserted the following from VisJS (you can find that in the VisJS):

<div id="mynetwork"><div class="vis-network" tabindex="...></canvas></div></div>

Lastly, in my JS scirpt.js file, I inserted the event listener, as you suggested, and my code logic:

document.addEventListener('prodigyupdate', event => {
  const eg = window.prodigy.content

  // create an array with nodes
  var node_list = []
  for (let i = 0; i < eg.spans.length; i++) {
  node_list.push({id: eg.spans[i].token_end, label:eg.spans[i].label})
  }
  var nodes = new vis.DataSet(node_list);

  var edge_list = []
  for (let i = 0; i < eg.relations.length; i++) {

  edge_list.push({from: eg.relations[i].child, to: eg.relations[i].head})
  }
  // create an array with edges
  var edges = new vis.DataSet(edge_list);

  // create a network
  var container = document.getElementById("mynetwork");
  var data = {
    nodes: nodes,
    edges: edges,
  };
  var options = {};
  var network = new vis.Network(container, data, options);
})

Voila, beneath, a simple graph emerges and changes, every time I make an annotation!

The only issue is that there is one annotation delay, either span or relation. For instance, if I span two words it will only show one in the graph, if I span three then will show two etc. I am not too bothered about it. Nonetheless, if this could be fixed, with another event listener perhaps, it would be cool.

Update:
I inserted a Refresh button, which takes successfully the latest content.

1 Like

That looks great, thanks for sharing your full solution!

2 Likes