" + marker_data.name.replace(''', "'") + "
" + @@ -70,22 +73,32 @@ function initMap() { const markers_data = JSON.parse( document.getElementById('map-markers').textContent) + let markers_loaded = false + // For each data entry in the json... for (const marker_data of markers_data) { try { const marker = createMarker(map, marker_data); + markers.push(marker); + bounds.extend(marker.position); + if (markers_data.length === 1) { + selected_marker = marker; + } + + markers_loaded = true + } catch (exc) { // Just skip and move on to next } } map.fitBounds(bounds) - const max_zoom = 10 - if (map.getZoom() > max_zoom) { - map.setZoom(max_zoom) + if (!markers_loaded) { + map.panTo({lat: 0, lng: 0}) } + setMaxZoom() setTimeout(setMaxZoom, 100) @@ -96,8 +109,8 @@ function initMap() { * Zoom to set level if map is zoomed in more than this. */ function setMaxZoom() { - const max_zoom = 10 - if (map.getZoom() > max_zoom) { - map.setZoom(max_zoom) - } + const max_zoom = 4 + const min_zoom = 2 + const zoom = Math.min(Math.max(min_zoom, map.getZoom()), max_zoom) + map.setZoom(zoom) } diff --git a/people/static/js/network.js b/people/static/js/network.js new file mode 100644 index 0000000..3b421a5 --- /dev/null +++ b/people/static/js/network.js @@ -0,0 +1,204 @@ + +// Global reference to Cytoscape graph - needed for `save_image` +var cy; + +var hide_organisations = false; +var organisation_nodes; +var organisation_edges; + +var anonymise_people = false; +var anonymise_organisations = false; + +var network_style = [ + { + selector: 'node[name]', + style: { + label: function (ele) { + var anonymise = anonymise_people; + if (ele.data('kind') == 'organisation') { + anonymise = anonymise_organisations; + } + + return anonymise ? ele.data('id') : ele.data('name') + }, + width: '100px', + height: '100px', + 'text-halign': 'center', + 'text-valign': 'center', + 'text-wrap': 'wrap', + 'text-max-width': '90px', + 'font-size': '12rem', + 'background-color': 'data(nodeColor)', + 'shape': 'data(nodeShape)' + } + }, + { + selector: 'node:selected', + style: { + 'text-max-width': '300px', + 'font-size': '40rem', + 'z-index': 100, + } + }, + { + selector: 'edge', + style: { + 'mid-target-arrow-shape': 'data(lineArrowShape)', + 'curve-style': 'straight', + 'width': 1, + 'line-color': 'data(lineColor)' + } + } +] + +/** + * Save the network as an image using the browser's normal file download flow. + */ +function save_image() { + saveAs(cy.png(), 'graph.png'); +} + +/** + * Hide or restore organisations and relationships with them. + */ +function toggle_organisations() { + hide_organisations = !hide_organisations; + + if (hide_organisations) { + organisation_nodes.remove(); + organisation_edges.remove(); + } else { + organisation_nodes.restore(); + organisation_edges.restore(); + } +} + +/** + * Toggle person node labels between names and ids. + */ +function toggle_anonymise_people() { + anonymise_people = !anonymise_people + cy.elements().remove().restore(); +} + +/** + * Toggle organisation node labels between names and ids. + */ +function toggle_anonymise_organisations() { + anonymise_organisations = !anonymise_organisations + cy.elements().remove().restore(); +} + +/** + * Populate a Cytoscape network from :class:`Person` and :class:`Relationship` JSON embedded in page. + */ +function get_network() { + // Initialise Cytoscape graph + // See https://js.cytoscape.org/ for documentation + cy = cytoscape({ + container: document.getElementById('cy'), + style: network_style + }); + + // Add pan + zoom widget with cytoscape-panzoom + cy.panzoom(); + + // Load people and add to graph + var person_set = JSON.parse(document.getElementById('person-set-data').textContent); + + for (var person of person_set) { + cy.add({ + group: 'nodes', + data: { + id: 'person-' + person.pk.toString(), + name: person.name, + kind: 'person', + nodeColor: '#0099cc', + nodeShape: 'ellipse' + } + }) + } + + // Load organisations and add to graph + var organisation_set = JSON.parse(document.getElementById('organisation-set-data').textContent); + + for (var item of organisation_set) { + cy.add({ + group: 'nodes', + data: { + id: 'organisation-' + item.pk.toString(), + name: item.name, + kind: 'organisation', + nodeColor: '#669933', + nodeShape: 'rectangle' + } + }) + } + organisation_nodes = cy.nodes('[kind = "organisation"]'); + + // Load relationships and add to graph + var relationship_set = JSON.parse(document.getElementById('relationship-set-data').textContent); + + for (var relationship of relationship_set) { + try { + cy.add({ + group: 'edges', + data: { + id: 'relationship-' + relationship.pk.toString(), + source: 'person-' + relationship.source.pk.toString(), + target: 'person-' + relationship.target.pk.toString(), + kind: 'person', + lineColor: { + 'organisation-membership': '#669933' + }[relationship.kind] || 'grey', + lineArrowShape: 'triangle' + } + }) + } catch (exc) { + // Exception thrown if a node in the relationship does not exist + // This is probably because it's been filtered out + } + } + + // Load organisation relationships and add to graph + relationship_set = JSON.parse(document.getElementById('organisation-relationship-set-data').textContent); + + for (var relationship of relationship_set) { + try { + cy.add({ + group: 'edges', + data: { + id: 'organisation-relationship-' + relationship.pk.toString(), + source: 'person-' + relationship.source.pk.toString(), + target: 'organisation-' + relationship.target.pk.toString(), + kind: 'organisation', + lineColor: { + 'organisation-membership': '#669933' + }[relationship.kind] || 'black', + lineArrowShape: 'triangle' + } + }) + } catch (exc) { + // Exception thrown if a node in the relationship does not exist + // This is probably because it's been filtered out + } + } + organisation_edges = cy.edges('[kind = "organisation"]'); + + // Optimise graph layout + var layout = cy.layout({ + name: 'cose', + randomize: true, + animate: false, + idealEdgeLength: function (edge) { return 40; }, + nodeRepulsion: function (node) { return 1e7; } + }); + + layout.run(); + + setTimeout(function () { + document.getElementById('cy').style.height = '100%'; + }, 1000) +} + +$(window).on('load', get_network()); diff --git a/people/templates/people/includes/answer_set.html b/people/templates/people/includes/answer_set.html new file mode 100644 index 0000000..3ab6dc8 --- /dev/null +++ b/people/templates/people/includes/answer_set.html @@ -0,0 +1,25 @@ +| Question | +Answer | +
|---|---|
| {{ question }} | +{{ answers }} | +
| No answers | +|
Last updated: {{ answer_set.timestamp }}
diff --git a/people/templates/people/map.html b/people/templates/people/map.html new file mode 100644 index 0000000..688d32e --- /dev/null +++ b/people/templates/people/map.html @@ -0,0 +1,44 @@ +{% extends 'base.html' %} + +{% block extra_head %} + {{ map_markers|json_script:'map-markers' }} + + {% load staticfiles %} + + + +{% endblock %} + +{% block content %} + + +Map
+ + + +Network View
- -- -