forked from security_vision/semantic_graph
Compare commits
10 Commits
Author | SHA1 | Date |
---|---|---|
Ruben van de Ven | d457c8eea0 | |
Ruben van de Ven | 3c0184fe97 | |
Ruben van de Ven | 815bb200be | |
Ruben van de Ven | 4c325af655 | |
Ruben van de Ven | ea9862eddc | |
Ruben van de Ven | a4c73be6a6 | |
Ruben van de Ven | 42472e9663 | |
Ruben van de Ven | c97af5bc01 | |
Ruben van de Ven | 1650efae49 | |
Ruben van de Ven | a43c320af6 |
33
README.md
33
README.md
|
@ -22,31 +22,6 @@ python wiki_relations.py
|
||||||
|
|
||||||
_Ask_ SMW with the following query:
|
_Ask_ SMW with the following query:
|
||||||
|
|
||||||
```
|
|
||||||
[[Category:Deployments||Institution]] OR [[Category:Technologies]] [[Developed by (institutions)::+]] OR [[Category:Technologies]] [[-Software Deployed::+]] OR [[Category:City]]
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
?Category
|
|
||||||
?Geolocation
|
|
||||||
?City
|
|
||||||
?City.Has Coordinates=City Coordinates
|
|
||||||
?City.Is in Country=City Country
|
|
||||||
?City.Is in Country.Has Coordinates=Country Coordinates
|
|
||||||
?Clients
|
|
||||||
?Managed by
|
|
||||||
?Used by
|
|
||||||
?Funded by
|
|
||||||
?Provided by
|
|
||||||
?Software Deployed
|
|
||||||
?Software Deployed.Developped by (institutions)=Software Developer
|
|
||||||
?Datasets Used
|
|
||||||
?Datasets Used.Developed by Institution=Dataset Developer
|
|
||||||
```
|
|
||||||
|
|
||||||
----
|
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
({{#ask: [[Category:Deployments||Institution]]
|
({{#ask: [[Category:Deployments||Institution]]
|
||||||
OR [[Category:Technologies]] [[Developed by (institutions)::+]] // TODO: + should give a subquery <q></q> with all institutions in EU
|
OR [[Category:Technologies]] [[Developed by (institutions)::+]] // TODO: + should give a subquery <q></q> with all institutions in EU
|
||||||
|
@ -75,16 +50,16 @@ OR [[Category:Technologies]] [[-Software Deployed::+]] // TODO: + should give a
|
||||||
|class=sortable wikitable smwtable
|
|class=sortable wikitable smwtable
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
TODO: see: https://www.semantic-mediawiki.org/wiki/Help:Querying_for_the_absence_of_a_property
|
||||||
|
|
||||||
{{#ask: [[Category:Deployments||Institution]] [[Not in graph::!Greens Report 2021]]
|
{{#ask: [[Category:Deployments||Institution]] [[Is in Greens Report 2021::True]]
|
||||||
OR [[Category:Technologies]] [[Developed by (institutions)::+]]
|
|
||||||
OR [[Category:Technologies]] [[-Software Deployed::+]]
|
|
||||||
|?Category
|
|?Category
|
||||||
|?Geolocation
|
|?Geolocation
|
||||||
|?City
|
|?City
|
||||||
|?City.Has Coordinates=City Coordinates
|
|?City.Has Coordinates=City Coordinates
|
||||||
|?City.Is in Country=City Country
|
|?City.Is in Country=City Country
|
||||||
|?City Country.Has Coordinates=Country Coordinates
|
|?City Country.Has Coordinates=Country Coordinates
|
||||||
|
|?Institution Type
|
||||||
|?Clients
|
|?Clients
|
||||||
|?Managed by
|
|?Managed by
|
||||||
|?Used by
|
|?Used by
|
||||||
|
@ -94,6 +69,8 @@ OR [[Category:Technologies]] [[-Software Deployed::+]]
|
||||||
|?Software Deployed.Developped by (institutions)=Software Developer
|
|?Software Deployed.Developped by (institutions)=Software Developer
|
||||||
|?Datasets Used
|
|?Datasets Used
|
||||||
|?Datasets Used.Developed by Institution=Dataset Developer
|
|?Datasets Used.Developed by Institution=Dataset Developer
|
||||||
|
|?Related Institutions
|
||||||
|
|?Involved Entities
|
||||||
|format=broadtable
|
|format=broadtable
|
||||||
|limit=50
|
|limit=50
|
||||||
|offset=0
|
|offset=0
|
||||||
|
|
|
@ -4,6 +4,7 @@ import requests
|
||||||
import argparse
|
import argparse
|
||||||
import datetime
|
import datetime
|
||||||
import tqdm
|
import tqdm
|
||||||
|
import csv
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -12,11 +13,12 @@ logger = logging.getLogger('wiki')
|
||||||
default_categories = [
|
default_categories = [
|
||||||
# 'Person',
|
# 'Person',
|
||||||
'Institution',
|
'Institution',
|
||||||
'Technology',
|
'Products',
|
||||||
'Deployments',
|
'Deployments',
|
||||||
'Dataset',
|
'Dataset',
|
||||||
'City',
|
'City',
|
||||||
# 'Country',# for deployments without city we should configure Geolocation
|
#'Country',# for deployments without city we should configure Geolocation
|
||||||
|
'Technology Type',
|
||||||
]
|
]
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description='Turn wiki into nodes & links, usable by d3-force.')
|
parser = argparse.ArgumentParser(description='Turn wiki into nodes & links, usable by d3-force.')
|
||||||
|
@ -28,6 +30,8 @@ parser.add_argument('--output', default="semantic_data.json",
|
||||||
help='Output JSON file')
|
help='Output JSON file')
|
||||||
parser.add_argument('--credentials', default="no_credentials.json",
|
parser.add_argument('--credentials', default="no_credentials.json",
|
||||||
help="JSON file containing the Bot's credentials")
|
help="JSON file containing the Bot's credentials")
|
||||||
|
parser.add_argument('--generate-csv', action='store_true',
|
||||||
|
help="generate edge.csv & nodes.csv")
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
@ -257,3 +261,17 @@ if __name__ == "__main__":
|
||||||
with open(args.output, 'w') as fp:
|
with open(args.output, 'w') as fp:
|
||||||
json.dump(collection, fp)
|
json.dump(collection, fp)
|
||||||
|
|
||||||
|
if args.generate_csv:
|
||||||
|
with open('nodes.csv', 'w') as csvfile:
|
||||||
|
all_keys = set().union(*(d.keys() for d in collection['nodes']))
|
||||||
|
# all_keys = ['@id']
|
||||||
|
dict_writer = csv.DictWriter(csvfile, fieldnames=all_keys, extrasaction='ignore', restval='')
|
||||||
|
dict_writer.writeheader()
|
||||||
|
dict_writer.writerows(collection['nodes'])
|
||||||
|
|
||||||
|
with open('edges.csv', 'w') as csvfile:
|
||||||
|
all_keys = set().union(*(d.keys() for d in collection['links']))
|
||||||
|
dict_writer = csv.DictWriter(csvfile, fieldnames=all_keys, extrasaction='ignore', restval='')
|
||||||
|
dict_writer.writeheader()
|
||||||
|
dict_writer.writerows(collection['links'])
|
||||||
|
|
247
www/graph.css
247
www/graph.css
|
@ -7,12 +7,13 @@
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--color1: #9741f9;
|
--color1: #9741f9;
|
||||||
--color2: #f3722c;
|
--color2: #f04a2d;
|
||||||
--color3: #f8961e;
|
--color3: #0083ff;
|
||||||
/* --color4: #f9844a; */
|
/* --color4: #f9844a; */
|
||||||
--color5: #f9c74f;
|
--color5: #24C3B2;
|
||||||
--color6: #90be6d;
|
--color6: #7bc748;
|
||||||
--color7: #43aa8b;
|
--color7: #e4a02b;/*#fff239;*/
|
||||||
|
--color7-2: #e4a02b;
|
||||||
--color8: #4d908e;
|
--color8: #4d908e;
|
||||||
--color9: #577590;
|
--color9: #577590;
|
||||||
--color10: #277da1;
|
--color10: #277da1;
|
||||||
|
@ -22,11 +23,13 @@
|
||||||
--selected-color: var(--color1);
|
--selected-color: var(--color1);
|
||||||
--link-color: rgba(255,255,255,0.5);
|
--link-color: rgba(255,255,255,0.5);
|
||||||
--link-hover-color: var(--hover-color);
|
--link-hover-color: var(--hover-color);
|
||||||
--link-hover-related-color: var(--hover-related-color);
|
--link-hover-related-color: var(--hover-related-color);
|
||||||
--link-focus-color: var(--hover-color);
|
--link-focus-color: var(--hover-color);
|
||||||
|
|
||||||
--body-back: #96a7b7; /*#9cb3c9; /*#8195a7; /*#9cb3c9; #b9cada*/
|
--body-back: #96a7b7; /*#9cb3c9; /*#8195a7; /*#9cb3c9; #b9cada*/
|
||||||
--title-color: #1c1c1c;
|
--title-color: #1c1c1c;
|
||||||
|
|
||||||
|
--zoom: 1; /* to be overriden by js */
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
@ -63,33 +66,41 @@ svg .links line, svg .links path {
|
||||||
/* stroke: #f3722c; */
|
/* stroke: #f3722c; */
|
||||||
/* stroke: #9df32c; */
|
/* stroke: #9df32c; */
|
||||||
stroke: var(--link-color);
|
stroke: var(--link-color);
|
||||||
stroke-width: 6;
|
stroke-width: calc(12px / var(--zoom));
|
||||||
fill: none;
|
fill: none;
|
||||||
transition: stroke-width 1s;
|
/* transition: stroke-width 1s; */
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
stroke-linecap: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg .selectedNode .links line, svg .selectedNode .links path {
|
||||||
|
opacity: .3;
|
||||||
}
|
}
|
||||||
|
|
||||||
svg .links line.hover, svg .links path.hover {
|
svg .links line.hover, svg .links path.hover {
|
||||||
stroke: var(--link-hover-color);
|
stroke: var(--link-hover-color) !important;
|
||||||
|
opacity: 1;
|
||||||
/* stroke-width: 12; */
|
/* stroke-width: 12; */
|
||||||
marker-end: url(#arrowHeadSelected);
|
/* marker-end: url(#arrowHeadSelected) !important; */
|
||||||
}
|
}
|
||||||
|
|
||||||
svg .links .linkedHover path{
|
svg .links .linkedHover path{
|
||||||
stroke: var(--link-hover-related-color);
|
stroke: var(--link-hover-related-color);
|
||||||
stroke-width: 12;
|
opacity: 1;
|
||||||
marker-end: url(#arrowHeadSelectedRelated);
|
stroke-width: calc(12px / var(--zoom));
|
||||||
|
/* marker-end: url(#arrowHeadSelectedRelated); */
|
||||||
}
|
}
|
||||||
|
|
||||||
svg .links .linkedSelected path{
|
svg .links .linkedSelected path{
|
||||||
stroke: var(--link-focus-color);
|
stroke: var(--link-hover-related-color);
|
||||||
stroke-width: 12;
|
opacity: 1;
|
||||||
marker-end: url(#arrowHeadSelected);
|
stroke-width: calc(12px / var(--zoom));
|
||||||
|
/* marker-end: url(#arrowHeadSelectedRelated); */
|
||||||
}
|
}
|
||||||
|
|
||||||
svg.zoomed .links line, svg.zoomed .links path {
|
/* svg.zoomed .links line, svg.zoomed .links path {
|
||||||
stroke-width: 2;
|
stroke-width: 2;
|
||||||
}
|
} */
|
||||||
|
|
||||||
/* svg.zoomed .links line, svg.zoomed .links path.hover {
|
/* svg.zoomed .links line, svg.zoomed .links path.hover {
|
||||||
stroke-width: 4;
|
stroke-width: 4;
|
||||||
|
@ -154,16 +165,18 @@ svg #header text#subtitle {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.node text.nodeTitle {
|
.node text.nodeTitle {
|
||||||
text-anchor: start;
|
text-anchor: start;
|
||||||
dominant-baseline: hanging;
|
dominant-baseline: hanging;
|
||||||
|
font-size: calc(20pt / var(--zoom)) !important;
|
||||||
/*achieves a 'text-anchor: top'*/
|
/*achieves a 'text-anchor: top'*/
|
||||||
/* font-size: 16pt; */
|
/* font-size: 16pt; */
|
||||||
/*Set this in JS*/
|
/*Set this in JS*/
|
||||||
transition: font-size .4s, opacity 1s;
|
transition: opacity 1s;
|
||||||
fill: #5d5d5f; /*also when hovering node*/
|
fill: black; /*also when hovering node*/
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
pointer-events: none;
|
/* pointer-events: none; */
|
||||||
/*prevent mouse glitches*/
|
/*prevent mouse glitches*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,6 +184,13 @@ svg #header text#subtitle {
|
||||||
.node text.nodeTitle.overlapping {
|
.node text.nodeTitle.overlapping {
|
||||||
/* used to be shown on hover, but disabled now that we have a tooltip */
|
/* used to be shown on hover, but disabled now that we have a tooltip */
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectedNode .node:not(.linkedSelected):not(.selected) text.nodeTitle{
|
||||||
|
/* used to be shown on hover, but disabled now that we have a tooltip */
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
svg.zoomed .node text.nodeTitle {
|
svg.zoomed .node text.nodeTitle {
|
||||||
|
@ -183,6 +203,12 @@ svg.zoomed.zoomed2 .node text.nodeTitle {
|
||||||
|
|
||||||
.node circle, .node path {
|
.node circle, .node path {
|
||||||
fill: lightgray;
|
fill: lightgray;
|
||||||
|
transform: scale(calc(1.5 / var(--zoom-sqrt)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#filters .node circle, #filters .node path {
|
||||||
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Whenever a connected link is hovered */
|
/* Whenever a connected link is hovered */
|
||||||
|
@ -190,10 +216,14 @@ svg.zoomed.zoomed2 .node text.nodeTitle {
|
||||||
.node.linkHover circle, .node.linkHover path, .node.linkedHover path, label:hover .node path {
|
.node.linkHover circle, .node.linkHover path, .node.linkedHover path, label:hover .node path {
|
||||||
fill: var(--hover-related-color) !important;
|
fill: var(--hover-related-color) !important;
|
||||||
stroke: var(--hover-related-color);
|
stroke: var(--hover-related-color);
|
||||||
stroke-width: 5px;
|
stroke-width: 2px;
|
||||||
}
|
}
|
||||||
.node.linkedSelected path {
|
.node.linkHover text, .node.linkedHover text{
|
||||||
fill: var(--hover-related-color) !important;
|
fill: var(--hover-related-color) !important;
|
||||||
|
}
|
||||||
|
.selectedNode .node:not(.linkedSelected) path {
|
||||||
|
fill: lightgray !important;
|
||||||
|
opacity: .5;
|
||||||
/* same as linkHover/linkedHover but without border */
|
/* same as linkHover/linkedHover but without border */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,7 +234,12 @@ svg.zoomed.zoomed2 .node text.nodeTitle {
|
||||||
.node:hover circle, .node:hover path, .node.selected path {
|
.node:hover circle, .node:hover path, .node.selected path {
|
||||||
fill: var(--hover-color) !important;
|
fill: var(--hover-color) !important;
|
||||||
stroke: var(--hover-color);
|
stroke: var(--hover-color);
|
||||||
stroke-width: 5px;
|
stroke-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.node:hover text, .node.selected text {
|
||||||
|
fill: var(--hover-color) !important;
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
.node:hover text {
|
.node:hover text {
|
||||||
|
@ -212,7 +247,11 @@ svg.zoomed.zoomed2 .node text.nodeTitle {
|
||||||
fill: var(--hover-color);
|
fill: var(--hover-color);
|
||||||
} */
|
} */
|
||||||
|
|
||||||
.node.selected circle, .node.selected path {
|
|
||||||
|
/* .selectedNode .node circle, .selectedNode .node path {
|
||||||
|
fill: lightgray !important;
|
||||||
|
} */
|
||||||
|
.selectedNode .node.selected circle, .selectedNode .node.selected path {
|
||||||
fill: var(--selected-color) !important;
|
fill: var(--selected-color) !important;
|
||||||
stroke: var(--selected-color);
|
stroke: var(--selected-color);
|
||||||
stroke-width: 5px;
|
stroke-width: 5px;
|
||||||
|
@ -245,7 +284,8 @@ svg.zoomed.zoomed2 .node text.nodeTitle {
|
||||||
|
|
||||||
.node.Institution circle, .node.Institution path {
|
.node.Institution circle, .node.Institution path {
|
||||||
/* fill: lightcoral; */
|
/* fill: lightcoral; */
|
||||||
fill: red;
|
/* fill: #11F999; */
|
||||||
|
fill: darkgray;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node.Dataset circle, .node.Dataset path {
|
.node.Dataset circle, .node.Dataset path {
|
||||||
|
@ -257,6 +297,49 @@ svg.zoomed.zoomed2 .node text.nodeTitle {
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.node.Institution.Institution-company path, .node.Institution.Institution-company text{
|
||||||
|
fill: var(--color2);
|
||||||
|
}
|
||||||
|
.node.Institution.Institution-government path, .node.Institution.Institution-government text{
|
||||||
|
fill: var(--color3);
|
||||||
|
}
|
||||||
|
.node.Institution.Institution-local-government path{
|
||||||
|
fill: #75F06D;
|
||||||
|
}
|
||||||
|
.node.Institution.Institution-law-enforcement path{
|
||||||
|
fill: var(--color7);
|
||||||
|
}
|
||||||
|
.node.Institution.Institution-law-enforcement text{
|
||||||
|
fill: var(--color7-2);
|
||||||
|
}
|
||||||
|
.node.Institution.Institution-ngo path, .node.Institution.Institution-ngo text{
|
||||||
|
fill: var(--color5);
|
||||||
|
}
|
||||||
|
.node.Institution.Institution-university path{
|
||||||
|
fill: #A6E447;
|
||||||
|
}
|
||||||
|
.node.Institution.Institution-research path, .node.Institution.Institution-research text{
|
||||||
|
fill: var(--color6);
|
||||||
|
}
|
||||||
|
.node.Institution.Institution-project path{
|
||||||
|
fill: #C1DB31;
|
||||||
|
}
|
||||||
|
.node.Institution.Institution-watchdog path{
|
||||||
|
fill: #CED628;
|
||||||
|
}
|
||||||
|
.node.Institution.Institution-expert-group path{
|
||||||
|
fill: #DAD121;
|
||||||
|
}
|
||||||
|
.node.Institution.Institution-foundation path{
|
||||||
|
fill: #E5CB1E;
|
||||||
|
}
|
||||||
|
.node.Institution.Institution-international-organization path{
|
||||||
|
fill: gray;
|
||||||
|
}
|
||||||
|
.node.Institution.Institution-art-project path{
|
||||||
|
/* fill: blue; */
|
||||||
|
}
|
||||||
|
|
||||||
/* .node.Person circle {
|
/* .node.Person circle {
|
||||||
fill: var(--color2)
|
fill: var(--color2)
|
||||||
}
|
}
|
||||||
|
@ -308,6 +391,17 @@ svg.zoomed.zoomed2 .node text.nodeTitle {
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
box-shadow: 2px 2px 5px rgba(0, 0, 0, .5);
|
box-shadow: 2px 2px 5px rgba(0, 0, 0, .5);
|
||||||
}
|
}
|
||||||
|
#tooltip.deploymentTooltip{
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
#tooltip.link{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#tooltip .entity{
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
#tooltip:not(.visible){
|
#tooltip:not(.visible){
|
||||||
position:absolute;
|
position:absolute;
|
||||||
|
@ -325,6 +419,9 @@ svg.zoomed.zoomed2 .node text.nodeTitle {
|
||||||
color: black;
|
color: black;
|
||||||
text-align: center;;
|
text-align: center;;
|
||||||
}
|
}
|
||||||
|
#tooltip.deploymentTooltip .category{
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
#tooltip .category::before{
|
#tooltip .category::before{
|
||||||
content:'· '
|
content:'· '
|
||||||
}
|
}
|
||||||
|
@ -336,6 +433,12 @@ svg.zoomed.zoomed2 .node text.nodeTitle {
|
||||||
color: gray;
|
color: gray;
|
||||||
text-align: center;;
|
text-align: center;;
|
||||||
}
|
}
|
||||||
|
#tooltip .node_sources::before{
|
||||||
|
content: 'source ';
|
||||||
|
}
|
||||||
|
#tooltip .node_sources{
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
#closeInfo {
|
#closeInfo {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -363,6 +466,7 @@ header {
|
||||||
background: white;
|
background: white;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-top-left-radius: 5px;
|
border-top-left-radius: 5px;
|
||||||
|
min-width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
|
@ -387,7 +491,7 @@ p.subtitle {
|
||||||
#filters label {
|
#filters label {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: block;
|
display: block;
|
||||||
padding: 10px;
|
padding: 5px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#filters label svg {
|
#filters label svg {
|
||||||
|
@ -418,6 +522,43 @@ p.subtitle {
|
||||||
/* box-shadow: inset 0 0 5px black; */
|
/* box-shadow: inset 0 0 5px black; */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#filters .institution_types{
|
||||||
|
margin-left: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#filter-items.filter-institution .institution_types{
|
||||||
|
/* display:none */
|
||||||
|
color: darkgray;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#filter-items.filter-institution .institution_types span{
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
#filters h3{
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
#filters h3::before{
|
||||||
|
display: inline-block;
|
||||||
|
float: left;
|
||||||
|
content: '\fe3f'; /*⬆*/
|
||||||
|
transform: rotate(180deg);
|
||||||
|
transition: transform .8s;
|
||||||
|
}
|
||||||
|
#filters.hide h3::before{
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#filter-items{
|
||||||
|
max-height: 1000px;
|
||||||
|
transition: max-height .8s;
|
||||||
|
}
|
||||||
|
#filters.hide #filter-items{
|
||||||
|
max-height: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#map .borders {
|
#map .borders {
|
||||||
stroke-width: 6px;
|
stroke-width: 6px;
|
||||||
stroke: rgb(221, 210, 210);
|
stroke: rgb(221, 210, 210);
|
||||||
|
@ -434,3 +575,57 @@ p.subtitle {
|
||||||
font-size: 30;
|
font-size: 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#closeSelection{
|
||||||
|
pointer-events: none;
|
||||||
|
position: fixed;
|
||||||
|
bottom: -100px;
|
||||||
|
left: 0;
|
||||||
|
background-color: white;
|
||||||
|
font-size: 50px;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
text-align:center;
|
||||||
|
z-index: 10;
|
||||||
|
line-height: 80px;
|
||||||
|
transition: bottom .2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectedNode #closeSelection{
|
||||||
|
pointer-events: all;
|
||||||
|
cursor: pointer;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sources{
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 80px;
|
||||||
|
height: 47px;
|
||||||
|
padding: 20px 20px 13px 20px;
|
||||||
|
background: black;
|
||||||
|
color: white;
|
||||||
|
transition: bottom .2s;
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sources:not(.visible){
|
||||||
|
bottom:-100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sources h3{
|
||||||
|
margin: 5px 0;
|
||||||
|
text-align: left;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 100%;
|
||||||
|
}
|
||||||
|
#sources h3::after{
|
||||||
|
content:':';
|
||||||
|
}
|
||||||
|
|
||||||
|
#sources .node_sources{
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sources a{
|
||||||
|
color: lightblue;
|
||||||
|
}
|
457
www/graph.js
457
www/graph.js
|
@ -4,9 +4,9 @@ const CONFIG = {
|
||||||
'subtitle': "A survey of the European Union",
|
'subtitle': "A survey of the European Union",
|
||||||
// 'nodeSize': 8,
|
// 'nodeSize': 8,
|
||||||
'nodeRadius': 5,
|
'nodeRadius': 5,
|
||||||
'nodeRepositionPadding': 10,
|
'nodeRepositionPadding': 12,
|
||||||
'baseUrl': 'https://www.securityvision.io/wiki/index.php/',
|
'baseUrl': 'https://www.securityvision.io/wiki/index.php/',
|
||||||
'dataUrl': 'result2.json',
|
'dataUrl': 'remote_biometric_identification.json',
|
||||||
'preSimulate': false, // run simulation before starting, so we don't start with lines jumping around
|
'preSimulate': false, // run simulation before starting, so we don't start with lines jumping around
|
||||||
'labels': {
|
'labels': {
|
||||||
'rotate': true,
|
'rotate': true,
|
||||||
|
@ -17,7 +17,92 @@ const CONFIG = {
|
||||||
'lonMax': 35,
|
'lonMax': 35,
|
||||||
'center': [11, 47],
|
'center': [11, 47],
|
||||||
},
|
},
|
||||||
"filters": ["Institution", "Deployments",/* "Technology", "Dataset"*/],
|
"institution_map": {
|
||||||
|
"Local Government": "Government",
|
||||||
|
"Regional Government": "Government",
|
||||||
|
"International Organization": "Government",
|
||||||
|
"Watchdog": "Government",
|
||||||
|
// "NGO": "Organisation",
|
||||||
|
"Foundation": "NGO",
|
||||||
|
"University": "Research",
|
||||||
|
"Expert Group": "Research",
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"Institution": {
|
||||||
|
"label": "Institution",
|
||||||
|
"type": "categories",
|
||||||
|
},
|
||||||
|
// TODO: nested filters
|
||||||
|
// TODO:restructure, allow for groups of types:
|
||||||
|
// [12:08, 07-05-2021] Francesco Ragazzi: Government / Regional / local can be the same
|
||||||
|
// [12:08, 07-05-2021] Francesco Ragazzi: Also IO
|
||||||
|
// [12:08, 07-05-2021] Francesco Ragazzi: NGO, Foundation can be grouped
|
||||||
|
// [12:08, 07-05-2021] Francesco Ragazzi: University, Research, Expert group can be grouped
|
||||||
|
// [12:08, 07-05-2021] Francesco Ragazzi: Watchdog and can also be grouped with government
|
||||||
|
"Government": {
|
||||||
|
// "label": "Institution",
|
||||||
|
"type": "institution_types",
|
||||||
|
},
|
||||||
|
"Law Enforcement": {
|
||||||
|
// "label": "Institution",
|
||||||
|
"type": "institution_types",
|
||||||
|
},
|
||||||
|
"Company": {
|
||||||
|
// "label": "Institution",
|
||||||
|
"type": "institution_types",
|
||||||
|
},
|
||||||
|
"NGO": {
|
||||||
|
// "label": "Institution",
|
||||||
|
"type": "institution_types",
|
||||||
|
},
|
||||||
|
|
||||||
|
// "Regional Government": {
|
||||||
|
// // "label": "Institution",
|
||||||
|
// "type": "institution_types",
|
||||||
|
// },
|
||||||
|
// "Local Government": {
|
||||||
|
// // "label": "Institution",
|
||||||
|
// "type": "institution_types",
|
||||||
|
// },
|
||||||
|
// "Foundation": {
|
||||||
|
// // "label": "Institution",
|
||||||
|
// "type": "institution_types",
|
||||||
|
// },
|
||||||
|
// "University": {
|
||||||
|
// // "label": "Institution",
|
||||||
|
// "type": "institution_types",
|
||||||
|
// },
|
||||||
|
"Research": {
|
||||||
|
// "label": "Institution",
|
||||||
|
"type": "institution_types",
|
||||||
|
},
|
||||||
|
// "Labour Union": {
|
||||||
|
// // "label": "Institution",
|
||||||
|
// "type": "institution_types",
|
||||||
|
// },
|
||||||
|
// "Watchdog": {
|
||||||
|
// // "label": "Institution",
|
||||||
|
// "type": "institution_types",
|
||||||
|
// },
|
||||||
|
// "Expert Group": {
|
||||||
|
// // "label": "Institution",
|
||||||
|
// "type": "institution_types",
|
||||||
|
// },
|
||||||
|
// "International Organization": {
|
||||||
|
// // "label": "Institution",
|
||||||
|
// "type": "institution_types",
|
||||||
|
// },
|
||||||
|
// "Art Project": {
|
||||||
|
// // "label": "Institution",
|
||||||
|
// "type": "institution_types",
|
||||||
|
// },
|
||||||
|
"Deployments": {
|
||||||
|
"label": "Deployment",
|
||||||
|
"type": "categories",
|
||||||
|
},
|
||||||
|
|
||||||
|
/* "Technology", "Dataset"*/
|
||||||
|
},
|
||||||
|
|
||||||
"link_properties": [
|
"link_properties": [
|
||||||
"Clients",
|
"Clients",
|
||||||
|
@ -28,8 +113,53 @@ const CONFIG = {
|
||||||
"Software Deployed",
|
"Software Deployed",
|
||||||
"Software Developer",
|
"Software Developer",
|
||||||
"Dataset Developer",
|
"Dataset Developer",
|
||||||
|
"Related Institutions",
|
||||||
|
"Involved Entities",
|
||||||
],
|
],
|
||||||
|
|
||||||
|
"link_labels": {
|
||||||
|
"Clients": {
|
||||||
|
"label": "is client of",
|
||||||
|
"swap": true,
|
||||||
|
},
|
||||||
|
"Managed by": {
|
||||||
|
"label": "manages",
|
||||||
|
"swap": true,
|
||||||
|
},
|
||||||
|
"Used by": {
|
||||||
|
"label": "uses",
|
||||||
|
"swap": true,
|
||||||
|
},
|
||||||
|
"Funded by": {
|
||||||
|
"label": "is funded by",
|
||||||
|
"swap": false,
|
||||||
|
},
|
||||||
|
"Provided by": {
|
||||||
|
"label": "provides",
|
||||||
|
"swap": true,
|
||||||
|
},
|
||||||
|
"Software Deployed": {
|
||||||
|
"label": "is used in",
|
||||||
|
"swap": true,
|
||||||
|
},
|
||||||
|
"Software Developer": {
|
||||||
|
"label": "is developed by",
|
||||||
|
"swap": false,
|
||||||
|
},
|
||||||
|
"Dataset Developer": {
|
||||||
|
"label": "develops dataset for",
|
||||||
|
"swap": true,
|
||||||
|
},
|
||||||
|
"Related Institutions": {
|
||||||
|
"label": "is related to",
|
||||||
|
"swap": true,
|
||||||
|
},
|
||||||
|
"Involved Entities": {
|
||||||
|
"label": "is involved in",
|
||||||
|
"swap": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
"geo_properties": [
|
"geo_properties": [
|
||||||
"Geolocation",
|
"Geolocation",
|
||||||
"City Coordinates",
|
"City Coordinates",
|
||||||
|
@ -46,7 +176,7 @@ const CONFIG = {
|
||||||
"Country": ["Country"],
|
"Country": ["Country"],
|
||||||
"City": ["City"],
|
"City": ["City"],
|
||||||
// ["Deployment type"], // TODO: select this
|
// ["Deployment type"], // TODO: select this
|
||||||
// ["Institution type"], // TODO: select this (local gov, etc.)
|
"Institution": ["Institution Type"], // TODO: select this (local gov, etc.)
|
||||||
"Dataset": ["Datasets used"],
|
"Dataset": ["Datasets used"],
|
||||||
"Company": ["Managed by", "Provided by", "Developped by (institutions)"],
|
"Company": ["Managed by", "Provided by", "Developped by (institutions)"],
|
||||||
"Tech": ["Technologies Used", "Software Deployed"],
|
"Tech": ["Technologies Used", "Software Deployed"],
|
||||||
|
@ -54,7 +184,7 @@ const CONFIG = {
|
||||||
},
|
},
|
||||||
"zoom": {
|
"zoom": {
|
||||||
"scale_min": .2,
|
"scale_min": .2,
|
||||||
"scale_max": 20,
|
"scale_max": 10,
|
||||||
},
|
},
|
||||||
|
|
||||||
"cases": [
|
"cases": [
|
||||||
|
@ -63,6 +193,17 @@ const CONFIG = {
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function getLinkLabelConfig(linkName){
|
||||||
|
if(CONFIG.link_labels.hasOwnProperty(linkName)){
|
||||||
|
return CONFIG.link_labels[linkName];
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
'label': linkName,
|
||||||
|
'swap': false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// let width = window.innerWidth;
|
// let width = window.innerWidth;
|
||||||
// let height = window.innerHeight;
|
// let height = window.innerHeight;
|
||||||
|
|
||||||
|
@ -196,7 +337,17 @@ function getTitle(obj) {
|
||||||
}
|
}
|
||||||
function getCategories(obj) {
|
function getCategories(obj) {
|
||||||
// console.log(obj);
|
// console.log(obj);
|
||||||
return obj.printouts['Category'].map(n => n.fulltext.split(':')[1]);
|
let cats = obj.printouts['Category'].map(n => n.fulltext.split(':')[1]);
|
||||||
|
if (obj.printouts.hasOwnProperty("Institution Type") && obj.printouts['Institution Type'].length) {
|
||||||
|
obj.printouts['Institution Type'].forEach(type => {
|
||||||
|
cats.push(getInstitutionClass(type.fulltext));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// return
|
||||||
|
return cats;
|
||||||
|
}
|
||||||
|
function getInstitutionClass(name) {
|
||||||
|
return "Institution-" + slugify(name);
|
||||||
}
|
}
|
||||||
function getClasses(obj) {
|
function getClasses(obj) {
|
||||||
const classes = getCategories(obj);
|
const classes = getCategories(obj);
|
||||||
|
@ -215,7 +366,10 @@ class NodeMap {
|
||||||
this.root = d3.select(parent);
|
this.root = d3.select(parent);
|
||||||
this.resizeEvent = window.addEventListener('resize', this.resize.bind(this));
|
this.resizeEvent = window.addEventListener('resize', this.resize.bind(this));
|
||||||
this.tooltipEl = document.getElementById('tooltip');
|
this.tooltipEl = document.getElementById('tooltip');
|
||||||
|
this.sourcesEl = document.getElementById('sources');
|
||||||
this.selectedNode = null;
|
this.selectedNode = null;
|
||||||
|
|
||||||
|
document.getElementById('closeSelection').addEventListener('click', (ev) => this.deselectNode());
|
||||||
}
|
}
|
||||||
|
|
||||||
resize() {
|
resize() {
|
||||||
|
@ -235,8 +389,16 @@ class NodeMap {
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// calculate which text labels overlap.
|
||||||
calculateLabels() {
|
calculateLabels() {
|
||||||
const els = document.querySelectorAll('.node text')
|
let els;
|
||||||
|
if(this.selectedNode === null) {
|
||||||
|
els = document.querySelectorAll('.node text')
|
||||||
|
} else {
|
||||||
|
// only related texts are visible
|
||||||
|
// so only these need to be considered
|
||||||
|
els = document.querySelectorAll('.node.linkedSelected text, .node.selected text')
|
||||||
|
}
|
||||||
for (let i = 0; i < els.length; i++) {
|
for (let i = 0; i < els.length; i++) {
|
||||||
const el = els[i];
|
const el = els[i];
|
||||||
let overlapping = false;
|
let overlapping = false;
|
||||||
|
@ -263,7 +425,7 @@ class NodeMap {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
this.svg = this.root.append('svg')
|
this.svg = this.root.append('svg')
|
||||||
this.svg.append('defs').html(`<marker markerHeight="4" markerWidth="4" refY="0" refX="6" viewBox="0 -3 8 6" preserveAspectRatio="none" orient="auto" id="arrowHead" ><path d="M0,-3L8,0L0,3"></path></marker><marker markerHeight="4" markerWidth="4" refY="0" refX="6" viewBox="0 -3 8 6" preserveAspectRatio="none" orient="auto" id="arrowHeadSelected"><path d="M0,-3L8,0L0,3"></path></marker><marker markerHeight="4" markerWidth="4" refY="0" refX="6" viewBox="0 -3 8 6" preserveAspectRatio="none" orient="auto" id="arrowHeadSelectedRelated"><path d="M0,-3L8,0L0,3"></path></marker>
|
this.svg.append('defs').html(`<marker markerHeight="3" markerWidth="3" refY="0" refX="6" viewBox="0 -3 8 6" preserveAspectRatio="none" orient="auto" id="arrowHead" ><path d="M0,-3L8,0L0,3"></path></marker><marker markerHeight="3" markerWidth="3" refY="0" refX="6" viewBox="0 -3 8 6" preserveAspectRatio="none" orient="auto" id="arrowHeadSelected"><path d="M0,-3L8,0L0,3"></path></marker><marker markerHeight="3" markerWidth="3" refY="0" refX="6" viewBox="0 -3 8 6" preserveAspectRatio="none" orient="auto" id="arrowHeadSelectedRelated"><path d="M0,-3L8,0L0,3"></path></marker>
|
||||||
<!--Sketching:-->
|
<!--Sketching:-->
|
||||||
<defs>
|
<defs>
|
||||||
<filter id="tint">
|
<filter id="tint">
|
||||||
|
@ -282,7 +444,7 @@ class NodeMap {
|
||||||
</filter>
|
</filter>
|
||||||
</defs>`);
|
</defs>`);
|
||||||
|
|
||||||
this.svg.on('click', (e) => { console.log(e); this.deselectNode()})
|
this.svg.on('click', (e) => { console.log(e); this.deselectNode() })
|
||||||
|
|
||||||
// const noise = 0.001;
|
// const noise = 0.001;
|
||||||
// this.svg.append('defs').append('filter').attr('id', 'splotch').html( `${
|
// this.svg.append('defs').append('filter').attr('id', 'splotch').html( `${
|
||||||
|
@ -345,7 +507,7 @@ class NodeMap {
|
||||||
return "country";
|
return "country";
|
||||||
})
|
})
|
||||||
.attr("d", this.proj)
|
.attr("d", this.proj)
|
||||||
// .attr("filter", 'url(#splotch)')
|
// .attr("filter", 'url(#splotch)')
|
||||||
// .attr("fill", );
|
// .attr("fill", );
|
||||||
|
|
||||||
this.g_borders
|
this.g_borders
|
||||||
|
@ -367,6 +529,8 @@ class NodeMap {
|
||||||
if (zoomTimeout) {
|
if (zoomTimeout) {
|
||||||
clearTimeout(zoomTimeout)
|
clearTimeout(zoomTimeout)
|
||||||
}
|
}
|
||||||
|
document.querySelector(':root').style.setProperty('--zoom', evt.transform.k);
|
||||||
|
document.querySelector(':root').style.setProperty('--zoom-sqrt', Math.pow(evt.transform.k, 1/3));
|
||||||
zoomTimeout = setTimeout(() => {
|
zoomTimeout = setTimeout(() => {
|
||||||
this.g_nodes.attr('style', `font-size:${22000 / this.height / evt.transform.k}pt`)
|
this.g_nodes.attr('style', `font-size:${22000 / this.height / evt.transform.k}pt`)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -441,7 +605,6 @@ class NodeMap {
|
||||||
|
|
||||||
this.graph.nodes.forEach((d) => {
|
this.graph.nodes.forEach((d) => {
|
||||||
for (const prop of CONFIG.geo_properties) {
|
for (const prop of CONFIG.geo_properties) {
|
||||||
|
|
||||||
// console.log(this,d.printouts, prop)
|
// console.log(this,d.printouts, prop)
|
||||||
if (d.printouts[prop].length) {
|
if (d.printouts[prop].length) {
|
||||||
// console.log("fix node", d);
|
// console.log("fix node", d);
|
||||||
|
@ -591,10 +754,24 @@ class NodeMap {
|
||||||
}
|
}
|
||||||
|
|
||||||
showTooltip(el, node, links) {
|
showTooltip(el, node, links) {
|
||||||
// TODO: make links optional (otherwise collect links here)
|
if(el.tagName != 'path'){
|
||||||
|
let parentEl = el.parentNode;
|
||||||
|
if(parentEl.tagName != 'g'){
|
||||||
|
parentEl = parentEl.parentNode;
|
||||||
|
}
|
||||||
|
el = parentEl.querySelector('path');
|
||||||
|
}
|
||||||
|
|
||||||
|
const categories = getCategories(node);
|
||||||
|
if (categories.includes('Deployments')){
|
||||||
|
this.tooltipEl.classList.add('deploymentTooltip');
|
||||||
|
} else {
|
||||||
|
this.tooltipEl.classList.remove('deploymentTooltip');
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: make links optional (otherwise collect links here)
|
||||||
this.tooltipEl.innerHTML = `
|
this.tooltipEl.innerHTML = `
|
||||||
<span class='category'>${getCategories(node)[0]}</span>
|
<span class='category'>${categories[0]}</span>
|
||||||
<h3>${node.fulltext}</h3>
|
<h3>${node.fulltext}</h3>
|
||||||
`;
|
`;
|
||||||
if (links.length) {
|
if (links.length) {
|
||||||
|
@ -603,17 +780,54 @@ class NodeMap {
|
||||||
<span class='clickForMore'>Click to examine ${links.length} ${rels}</span>
|
<span class='clickForMore'>Click to examine ${links.length} ${rels}</span>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rect = el.getBoundingClientRect()
|
const rect = el.getBoundingClientRect()
|
||||||
const rectTT = this.tooltipEl.getBoundingClientRect();
|
const rectTT = this.tooltipEl.getBoundingClientRect();
|
||||||
this.tooltipEl.style.top = (rect.top - rectTT.height) + 'px';
|
this.tooltipEl.style.top = (rect.top - rectTT.height) + 'px';
|
||||||
this.tooltipEl.style.left = (rect.left + rect.width / 2 - rectTT.width / 2) + 'px';
|
this.tooltipEl.style.left = (rect.left + rect.width / 2 - rectTT.width / 2) + 'px';
|
||||||
// console.log(el, node, rect.top);
|
// console.log(el, node, rect.top);
|
||||||
|
|
||||||
this.tooltipEl.classList.add('visible');
|
this.tooltipEl.classList.add('visible', 'node');
|
||||||
|
}
|
||||||
|
|
||||||
|
showRelationTooltip(link, evt) {
|
||||||
|
const {label, swap} = getLinkLabelConfig(link.name);
|
||||||
|
this.tooltipEl.classList.remove('deploymentTooltip');
|
||||||
|
if(swap){
|
||||||
|
this.tooltipEl.innerHTML = `
|
||||||
|
<span class='relation'>
|
||||||
|
<span class='entity'>${link.target.fulltext}</span>
|
||||||
|
${label}
|
||||||
|
<span class='entity'>${link.source.fulltext}</span>
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
this.tooltipEl.innerHTML = `
|
||||||
|
<span class='relation'>
|
||||||
|
<span class='entity'>${link.source.fulltext}</span>
|
||||||
|
${label}
|
||||||
|
<span class='entity'>${link.target.fulltext}</span>
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rectTT = this.tooltipEl.getBoundingClientRect();
|
||||||
|
|
||||||
|
this.trackerEv = (evt) => {
|
||||||
|
this.tooltipEl.style.top = (evt.clientY - rectTT.height - 10) + 'px';
|
||||||
|
this.tooltipEl.style.left = (evt.clientX - rectTT.width / 2) + 'px';
|
||||||
|
};
|
||||||
|
window.addEventListener('mousemove', this.trackerEv);
|
||||||
|
|
||||||
|
this.tooltipEl.classList.add('visible', 'link');
|
||||||
}
|
}
|
||||||
|
|
||||||
hideTooltip() {
|
hideTooltip() {
|
||||||
this.tooltipEl.classList.remove('visible');
|
this.tooltipEl.classList.remove('visible', 'node', 'link');
|
||||||
|
if(this.trackerEv){
|
||||||
|
window.removeEventListener('mousemove', this.trackerEv);
|
||||||
|
this.trackerEv = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
selectNode(node) {
|
selectNode(node) {
|
||||||
|
@ -637,14 +851,19 @@ class NodeMap {
|
||||||
document.getElementById(node.id).classList.add('selected');
|
document.getElementById(node.id).classList.add('selected');
|
||||||
connectedNodes.forEach(n => document.getElementById(n.id).classList.add('linkedSelected'));
|
connectedNodes.forEach(n => document.getElementById(n.id).classList.add('linkedSelected'));
|
||||||
links.forEach(l => document.getElementById(getLinkId(l)).classList.add('linkedSelected'));
|
links.forEach(l => document.getElementById(getLinkId(l)).classList.add('linkedSelected'));
|
||||||
|
|
||||||
|
|
||||||
|
this.container.classed('selectedNode', true);
|
||||||
|
|
||||||
|
document.body.classList.add('selectedNode');
|
||||||
|
|
||||||
|
this.showSources(node);
|
||||||
// TODO: show details;
|
// TODO: show details;
|
||||||
|
|
||||||
// alert('not yet implemented');
|
// alert('not yet implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
deselectNode() {
|
deselectNode() {
|
||||||
|
this.hideSources();
|
||||||
this.selectedNode = null;
|
this.selectedNode = null;
|
||||||
let nodeEls = document.getElementsByClassName('selected');
|
let nodeEls = document.getElementsByClassName('selected');
|
||||||
while (nodeEls.length) {
|
while (nodeEls.length) {
|
||||||
|
@ -654,6 +873,39 @@ class NodeMap {
|
||||||
while (els.length) {
|
while (els.length) {
|
||||||
els[0].classList.remove('linkedSelected');
|
els[0].classList.remove('linkedSelected');
|
||||||
}
|
}
|
||||||
|
this.container.classed('selectedNode', false);
|
||||||
|
document.body.classList.remove('selectedNode');
|
||||||
|
}
|
||||||
|
|
||||||
|
showSources(node){
|
||||||
|
|
||||||
|
const categories = getCategories(node);
|
||||||
|
if (!categories.includes('Deployments')){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(!node.printouts['Source'].length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
setTimeout(() => { // give potential visible sources time to hide
|
||||||
|
let sources = [];
|
||||||
|
for(let source of node.printouts['Source']){
|
||||||
|
const url = document.createElement('a');
|
||||||
|
url.href = source;
|
||||||
|
const hostname = url.hostname.startsWith('www.') ? url.hostname.substring(4) : url.hostname;
|
||||||
|
sources .push(`<a href="${source}" target="_blank">${hostname}</a>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const title = node.printouts['Source'].length > 1 ? "Sources" : "Source"
|
||||||
|
this.sourcesEl.innerHTML = `<h3>${title}</h3> ` + sources.join(', ');
|
||||||
|
this.sourcesEl.classList.add('visible');
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
hideSources(){
|
||||||
|
this.sourcesEl.classList.remove('visible');
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
|
@ -664,6 +916,7 @@ class NodeMap {
|
||||||
// see also: https://www.createwithdata.com/enter-exit-with-d3-join/
|
// see also: https://www.createwithdata.com/enter-exit-with-d3-join/
|
||||||
this.node = this.node.data(this.graph.nodes, d => d.id)
|
this.node = this.node.data(this.graph.nodes, d => d.id)
|
||||||
.join((enter) => {
|
.join((enter) => {
|
||||||
|
// let group = enter.insert("g", ":first-child")
|
||||||
let group = enter.append("g")
|
let group = enter.append("g")
|
||||||
.attr("class", getClasses)
|
.attr("class", getClasses)
|
||||||
.attr("id", (n) => getIdForTitle(n.fulltext));
|
.attr("id", (n) => getIdForTitle(n.fulltext));
|
||||||
|
@ -703,7 +956,10 @@ class NodeMap {
|
||||||
.attr('d', (n) => {
|
.attr('d', (n) => {
|
||||||
return getSymbolForNode(n)(n);
|
return getSymbolForNode(n)(n);
|
||||||
})
|
})
|
||||||
var nodeTitle = group.append('text').attr("class", "nodeTitle").attr("y", "3").attr('x', 5);
|
var nodeTitle = group.append('text').attr("class", "nodeTitle").attr("y", "4").attr('x', 5);
|
||||||
|
// nodeTitle.on('mouseover', (evt, n) =>{
|
||||||
|
// console.log(evt,n)
|
||||||
|
// });
|
||||||
nodeTitle
|
nodeTitle
|
||||||
.each(function (node, i, nodes) {
|
.each(function (node, i, nodes) {
|
||||||
var textLength = void 0;
|
var textLength = void 0;
|
||||||
|
@ -714,7 +970,7 @@ class NodeMap {
|
||||||
titleTexts = splitText(titleText);
|
titleTexts = splitText(titleText);
|
||||||
}
|
}
|
||||||
if (titleTexts !== false) {
|
if (titleTexts !== false) {
|
||||||
const tspan1 = self.append("tspan").text(titleTexts[0]).attr("y", "3").attr("x", "5");
|
const tspan1 = self.append("tspan").text(titleTexts[0]).attr("y", "4").attr("x", "5");
|
||||||
const tspan = self.append("tspan").text(titleTexts[1]).attr("dy", "1em").attr("x", "5");
|
const tspan = self.append("tspan").text(titleTexts[1]).attr("dy", "1em").attr("x", "5");
|
||||||
// const textLength1 = tspan.node().getComputedTextLength();
|
// const textLength1 = tspan.node().getComputedTextLength();
|
||||||
// const textLength2 = tspan.node().getComputedTextLength();
|
// const textLength2 = tspan.node().getComputedTextLength();
|
||||||
|
@ -729,64 +985,6 @@ class NodeMap {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// const labelPadding = 1;
|
|
||||||
|
|
||||||
// // // the component used to render each label
|
|
||||||
// var textLabel = fc.layoutTextLabel()
|
|
||||||
// .padding(labelPadding)
|
|
||||||
// //.value(function(d) { return map_data.properties.iso; });
|
|
||||||
// //.value(function(d) { return d.properties.iso; });
|
|
||||||
// .value( (d) => getTitle(d));
|
|
||||||
|
|
||||||
// // a strategy that combines simulated annealing with removal
|
|
||||||
// // of overlapping labels
|
|
||||||
// // */fc.layoutGreedy
|
|
||||||
// const strategy = fc.layoutRemoveOverlaps(fc.layoutGreedy());
|
|
||||||
|
|
||||||
// // create the layout that positions the labels
|
|
||||||
// this.layoutLabels = fc.layoutLabel(strategy)
|
|
||||||
// .size((d, i, g) => {
|
|
||||||
// // measure the label and add the required padding
|
|
||||||
// const textSize = g[i].getElementsByTagName('text')[0].getBBox();
|
|
||||||
// console.log(textSize);
|
|
||||||
// // return [30, 20];
|
|
||||||
// return [textSize.width + labelPadding * 2, textSize.height + labelPadding * 2];
|
|
||||||
// })
|
|
||||||
// .position(d => [d.x, d.y])
|
|
||||||
// .component(textLabel);
|
|
||||||
|
|
||||||
// // render!
|
|
||||||
// // this.node.datum(this.graph.nodes,).call(labels)
|
|
||||||
// this.labels = this.container.append('g').attr('class','labels');
|
|
||||||
// this.labels.datum(this.graph.nodes)
|
|
||||||
// // // this.node
|
|
||||||
// .call(this.layoutLabels);
|
|
||||||
|
|
||||||
|
|
||||||
// // use simulate annealing to find minimum overlapping text label positions
|
|
||||||
// //https://github.com/d3fc/d3fc-label-layout/blob/master/README.md
|
|
||||||
// var strategy = fc.layoutGreedy();
|
|
||||||
// //var strategy = fc.layoutAnnealing();
|
|
||||||
|
|
||||||
// // create the layout that positions the labels
|
|
||||||
// var labels = fc.layoutLabel(strategy)
|
|
||||||
// .size(function (_, i, g) {
|
|
||||||
// // measure the label and add the required padding
|
|
||||||
// var textSize = d3.select(g[i])
|
|
||||||
// .select('text')
|
|
||||||
// .node()
|
|
||||||
// .getBBox();
|
|
||||||
// return [textSize.width + labelPadding * 2, textSize.height + labelPadding * 2];
|
|
||||||
// })
|
|
||||||
// .position((d) => this.projection([d.lon, d.lat]); })
|
|
||||||
// .component(textLabel);
|
|
||||||
|
|
||||||
// // render!
|
|
||||||
// this.container.datum(countries)
|
|
||||||
// .call(labels);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
this.link = this.link
|
this.link = this.link
|
||||||
.data(this.graph.links)
|
.data(this.graph.links)
|
||||||
.join(
|
.join(
|
||||||
|
@ -795,10 +993,10 @@ class NodeMap {
|
||||||
.attr("class", (l) => "link " + slugify(l.name))
|
.attr("class", (l) => "link " + slugify(l.name))
|
||||||
.attr("id", getLinkId);
|
.attr("id", getLinkId);
|
||||||
group.append("path")
|
group.append("path")
|
||||||
.attr("marker-end", "url(#arrowHead)")
|
// .attr("marker-end", "url(#arrowHead)")
|
||||||
.attr('id', (d, i) => 'linkpath_' + i)
|
.attr('id', (d, i) => 'linkpath_' + i)
|
||||||
.on("mouseover", function (ev, link) {
|
.on("mouseover", (ev, link) => {
|
||||||
d3.select(this).classed('hover', true);
|
d3.select(ev.target).classed('hover', true);
|
||||||
const nodes = document.getElementsByClassName('node');
|
const nodes = document.getElementsByClassName('node');
|
||||||
for (let n of nodes) {
|
for (let n of nodes) {
|
||||||
const d = d3.select(n).datum();
|
const d = d3.select(n).datum();
|
||||||
|
@ -806,17 +1004,15 @@ class NodeMap {
|
||||||
n.classList.add('linkHover');
|
n.classList.add('linkHover');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// console.log(l);
|
|
||||||
}).on("mouseout", function (ev, link) {
|
this.showRelationTooltip(link, ev);
|
||||||
d3.select(this).classed('hover', false);
|
}).on("mouseout", (ev, link) => {
|
||||||
|
this.hideTooltip();
|
||||||
|
d3.select(ev.target).classed('hover', false);
|
||||||
const nodes = document.getElementsByClassName('linkHover');
|
const nodes = document.getElementsByClassName('linkHover');
|
||||||
while (nodes.length) {
|
while (nodes.length) {
|
||||||
nodes[0].classList.remove('linkHover');
|
nodes[0].classList.remove('linkHover');
|
||||||
}
|
}
|
||||||
// l.classed('hover',false);
|
|
||||||
// l.target.classed('hover',false);
|
|
||||||
// l.source.classed('hover',false);
|
|
||||||
// console.log(l,'l');
|
|
||||||
}).on("click", (ev, link) => {
|
}).on("click", (ev, link) => {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
this.selectNode(link.source);
|
this.selectNode(link.source);
|
||||||
|
@ -889,28 +1085,25 @@ class NodeMap {
|
||||||
var tgtSize = _mapGraph.getSizeForNode(l.target);
|
var tgtSize = _mapGraph.getSizeForNode(l.target);
|
||||||
|
|
||||||
// Compute the line endpoint such that the arrow
|
// Compute the line endpoint such that the arrow
|
||||||
// is touching the edge of the node rectangle perfectly.
|
// it not in the center, but rather slightly out of it
|
||||||
l.sourceX = sourceX + Math.sin(angle) * srcSize;
|
// use a small ofset for the angle to compensate roughly for the curve
|
||||||
l.targetX = targetX - Math.sin(angle) * tgtSize;
|
l.sourceX = sourceX + Math.sin(angle+.5) * srcSize;
|
||||||
l.sourceY = l.source.y + Math.cos(angle) * srcSize;
|
l.targetX = targetX - Math.sin(angle-.5) * tgtSize;
|
||||||
l.targetY = l.target.y - Math.cos(angle) * tgtSize;
|
l.sourceY = l.source.y + Math.cos(angle+.5) * srcSize;
|
||||||
|
l.targetY = l.target.y - Math.cos(angle-.5) * tgtSize;
|
||||||
|
|
||||||
// const coor_source = _mapGraph.projection.invert([l.source.x, l.source.y]);
|
// const coor_source = _mapGraph.projection.invert([l.source.x, l.source.y]);
|
||||||
// const coor_target = _mapGraph.projection.invert([l.target.x, l.target.y]);
|
// const coor_target = _mapGraph.projection.invert([l.target.x, l.target.y]);
|
||||||
// const middleCoor = [coor_source[0] * .5 + coor_target[0] * .5, coor_source[1] * .5 + coor_target[1] * .5];
|
// const middleCoor = [coor_source[0] * .5 + coor_target[0] * .5, coor_source[1] * .5 + coor_target[1] * .5];
|
||||||
// const middlePoint = _mapGraph.projection(middleCoor);
|
// const middlePoint = _mapGraph.projection(middleCoor);
|
||||||
|
|
||||||
|
// find radius of arc based on distance between points
|
||||||
const dr = Math.sqrt(dx * dx + dy * dy);
|
const dr = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
// "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y
|
// "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y
|
||||||
let rel = d3.select(this);
|
let rel = d3.select(this);
|
||||||
rel.select("path") //${middlePoint[0]},${middlePoint[1]}
|
rel.select("path")
|
||||||
// .attr('d', `M ${l.sourceX},${l.sourceY} L ${l.targetX},${l.targetY}`)
|
|
||||||
.attr('d', `M ${l.sourceX},${l.sourceY} A ${dr},${dr} 0 0,1 ${l.targetX},${l.targetY}`)
|
.attr('d', `M ${l.sourceX},${l.sourceY} A ${dr},${dr} 0 0,1 ${l.targetX},${l.targetY}`)
|
||||||
// .attr("x1", l.sourceX)
|
|
||||||
// .attr("y1", l.sourceY)
|
|
||||||
// .attr("x2", l.targetX)
|
|
||||||
// .attr("y2", l.targetY)
|
|
||||||
|
|
||||||
rel.select('text')
|
rel.select('text')
|
||||||
.attr("transform", function (d) {
|
.attr("transform", function (d) {
|
||||||
|
@ -977,6 +1170,7 @@ class NodeMap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// zoom & translate the graph to fit the provided nodes
|
||||||
zoomFit(nodes, paddingPercent = 0.8, transitionDuration = 2000) {
|
zoomFit(nodes, paddingPercent = 0.8, transitionDuration = 2000) {
|
||||||
// var bounds = root.node().getBBox();
|
// var bounds = root.node().getBBox();
|
||||||
const x0 = Math.min(...nodes.map(n => n.x - CONFIG.nodeRadius));
|
const x0 = Math.min(...nodes.map(n => n.x - CONFIG.nodeRadius));
|
||||||
|
@ -1044,7 +1238,7 @@ class AlluvialMap {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
this.svg = this.root.append('svg')
|
this.svg = this.root.append('svg')
|
||||||
|
|
||||||
this.resize();
|
this.resize();
|
||||||
|
|
||||||
this.sankey = d3.sankey()
|
this.sankey = d3.sankey()
|
||||||
|
@ -1174,6 +1368,17 @@ JsonToGraph = function (data) {
|
||||||
i++;
|
i++;
|
||||||
let node = data.results[node_id];
|
let node = data.results[node_id];
|
||||||
node.id = getIdForTitle(node.fulltext); //node_id;
|
node.id = getIdForTitle(node.fulltext); //node_id;
|
||||||
|
// group institution types
|
||||||
|
if(node.printouts['Institution Type'].length ){
|
||||||
|
for (let idx = 0; idx < node.printouts['Institution Type'].length; idx++) {
|
||||||
|
const type = node.printouts['Institution Type'][idx];
|
||||||
|
if(CONFIG.institution_map.hasOwnProperty(type.fulltext)){
|
||||||
|
node.printouts['Institution Type'][idx].fulltext = CONFIG.institution_map[type.fulltext];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// node.printouts['Institution Type'] = CONFIG.institution_map[node.printouts['Institution Type']];
|
||||||
|
}
|
||||||
|
|
||||||
nodes.push(node);
|
nodes.push(node);
|
||||||
// console.log(node_id, node);
|
// console.log(node_id, node);
|
||||||
|
|
||||||
|
@ -1444,10 +1649,9 @@ class Store {
|
||||||
|
|
||||||
this.filters = {
|
this.filters = {
|
||||||
'categories': [],
|
'categories': [],
|
||||||
|
'institution_types': [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
this.filter();
|
this.filter();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1503,10 +1707,18 @@ class Store {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isFiltered(node) {
|
||||||
|
if (this.filters.categories.includes(node.printouts['Category'][0].fulltext.split(':')[1]))
|
||||||
|
return true;
|
||||||
|
if (node.printouts['Institution Type'].length && this.filters.institution_types.includes(node.printouts['Institution Type'][0].fulltext))
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
filter() {
|
filter() {
|
||||||
// add and remove nodes from data based on type filters
|
// add and remove nodes from data based on type filters
|
||||||
this.nodes.forEach((n) => {
|
this.nodes.forEach((n) => {
|
||||||
if (!this.filters.categories.includes(n.printouts['Category'][0].fulltext.split(':')[1])) {
|
if (!this.isFiltered(n)) {
|
||||||
if (n.filtered || typeof n.filtered === 'undefined') {
|
if (n.filtered || typeof n.filtered === 'undefined') {
|
||||||
n.filtered = false;
|
n.filtered = false;
|
||||||
this.graph.nodes.push(n);
|
this.graph.nodes.push(n);
|
||||||
|
@ -1524,14 +1736,16 @@ class Store {
|
||||||
|
|
||||||
// add and remove links from data based on availability of nodes
|
// add and remove links from data based on availability of nodes
|
||||||
this.links.forEach((l) => {
|
this.links.forEach((l) => {
|
||||||
if (this.graph.nodes.includes(l.source) && this.graph.nodes.includes(l.target)) {
|
// if (this.graph.nodes.includes(l.source) && this.graph.nodes.includes(l.target)) {
|
||||||
|
if (!l.source.filtered && !l.target.filtered) {
|
||||||
if (l.filtered || typeof l.filtered === 'undefined')
|
if (l.filtered || typeof l.filtered === 'undefined')
|
||||||
this.graph.links.push(l);
|
this.graph.links.push(l);
|
||||||
l.filtered = false;
|
l.filtered = false;
|
||||||
} else {
|
} else {
|
||||||
if (l.filtered === false) {
|
if (l.filtered === false) {
|
||||||
|
console.log('filter', l.id);
|
||||||
this.graph.links.forEach((d, i) => {
|
this.graph.links.forEach((d, i) => {
|
||||||
if (l.id === d.id) {
|
if (l.nr === d.nr) {
|
||||||
this.graph.links.splice(i, 1);
|
this.graph.links.splice(i, 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1539,40 +1753,55 @@ class Store {
|
||||||
l.filtered = true;
|
l.filtered = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(this.graph.nodes.length, this.graph.links.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
CONFIG.filters.forEach(f => {
|
Object.keys(CONFIG.filters).forEach(f => {
|
||||||
|
const settings = CONFIG.filters[f];
|
||||||
|
|
||||||
let labelEl = document.createElement('label')
|
// if (settings.type == 'institution_types')
|
||||||
|
// // TODO; For now, skip
|
||||||
|
// return;
|
||||||
|
|
||||||
|
let categories = [f];
|
||||||
|
if (settings.type == 'institution_types')
|
||||||
|
categories = ['Institution', getInstitutionClass(f)];
|
||||||
|
|
||||||
|
let labelEl = document.createElement('label');
|
||||||
|
labelEl.setAttribute('id', 'filter-'+slugify(f));
|
||||||
|
labelEl.classList.add(settings.type)
|
||||||
let inputEl = document.createElement('input')
|
let inputEl = document.createElement('input')
|
||||||
let textEl = document.createElement('span');
|
let textEl = document.createElement('span');
|
||||||
let svg = d3.select(labelEl).append('svg')
|
let svg = d3.select(labelEl).append('svg')
|
||||||
.attr("viewBox", [-12, -12, 24, 24]);
|
.attr("viewBox", [-12, -12, 24, 24]);
|
||||||
svg.append('g')
|
svg.append('g')
|
||||||
.attr("class", "node " + f)
|
.attr("class", "node " + categories.join(' '))
|
||||||
.append('path')
|
.append('path')
|
||||||
.attr('d', getSymbolForCategories(f)());
|
.attr('d', getSymbolForCategories(categories)());
|
||||||
inputEl.type = "checkbox";
|
inputEl.type = "checkbox";
|
||||||
textEl.innerText = f;
|
textEl.innerText = settings.hasOwnProperty('label') ? settings.label : f;
|
||||||
labelEl.appendChild(inputEl);
|
labelEl.appendChild(inputEl);
|
||||||
labelEl.appendChild(textEl);
|
labelEl.appendChild(textEl);
|
||||||
|
|
||||||
if (!this.filters.categories.includes(f)) {
|
if (!this.filters[settings.type].includes(f)) {
|
||||||
inputEl.checked = true;
|
inputEl.checked = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
inputEl.addEventListener('change', (e) => {
|
inputEl.addEventListener('change', (e) => {
|
||||||
if (e.target.checked) {
|
if (e.target.checked) {
|
||||||
this.filters.categories.forEach((d, i) => {
|
this.filters[settings.type].forEach((d, i) => {
|
||||||
if (d == f) {
|
if (d == f) {
|
||||||
this.filters.categories.splice(i, 1);
|
this.filters[settings.type].splice(i, 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
this.root.classList.remove('filter-'+slugify(f))
|
||||||
} else {
|
} else {
|
||||||
if (!this.filters.categories.includes(f)) {
|
if (!this.filters[settings.type].includes(f)) {
|
||||||
this.filters.categories.push(f);
|
this.filters[settings.type].push(f);
|
||||||
}
|
}
|
||||||
|
this.root.classList.add('filter-'+slugify(f))
|
||||||
}
|
}
|
||||||
this.filter();
|
this.filter();
|
||||||
this.update();
|
this.update();
|
||||||
|
@ -1597,7 +1826,7 @@ Promise.all([fetch(req_data), fetch(req_world)])
|
||||||
})
|
})
|
||||||
.then(([data, world]) => {
|
.then(([data, world]) => {
|
||||||
var graph = JsonToGraph(data);
|
var graph = JsonToGraph(data);
|
||||||
var store = new Store(graph, '#filters');
|
var store = new Store(graph, '#filter-items');
|
||||||
|
|
||||||
mapGraph.setWorld(world);
|
mapGraph.setWorld(world);
|
||||||
mapGraph.setStore(store);
|
mapGraph.setStore(store);
|
||||||
|
|
|
@ -3,22 +3,23 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<link rel="stylesheet" href="graph.css">
|
<link rel="stylesheet" href="graph.css">
|
||||||
|
<title>Remote Biometric Identification | A survey of the European Union</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div id='tooltip'></div>
|
<div id='tooltip'></div>
|
||||||
|
<div id='sources'></div>
|
||||||
|
<div id='closeSelection'>×</div>
|
||||||
<div id='map'></div>
|
<div id='map'></div>
|
||||||
<!-- <div id='alluvial'></div> -->
|
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<h1>Remote Biometric Identification</h1>
|
<h1>Remote Biometric Identification</h1>
|
||||||
<p class='subtitle'>A survey of the European Union</p>
|
<p class='subtitle'>A survey of the European Union</p>
|
||||||
|
|
||||||
|
|
||||||
<aside id="filters">
|
<aside id="filters">
|
||||||
<h3>Legend</h3>
|
<h3 onclick="this.parentNode.classList.toggle('hide');">Filter</h3>
|
||||||
|
<div id="filter-items"></div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
|
@ -28,16 +29,8 @@
|
||||||
<script src="https://d3js.org/d3-geo-projection.v3.min.js"></script>
|
<script src="https://d3js.org/d3-geo-projection.v3.min.js"></script>
|
||||||
<script src="https://d3js.org/topojson.v3.min.js"></script>
|
<script src="https://d3js.org/topojson.v3.min.js"></script>
|
||||||
<script src="//unpkg.com/d3-geo-zoom"></script>
|
<script src="//unpkg.com/d3-geo-zoom"></script>
|
||||||
|
|
||||||
<script src="https://unpkg.com/d3-sankey@0"></script>
|
|
||||||
<!-- <script src="//unpkg.com/d3fc@14.0.1"></script> -->
|
|
||||||
<script src="graph.js"></script>
|
<script src="graph.js"></script>
|
||||||
|
|
||||||
<script>
|
|
||||||
if(window.location.hash == '#light') {
|
|
||||||
document.body.classList.add('light');
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
|
@ -150,7 +150,7 @@
|
||||||
Quisque vulputate odio neque, eget laoreet felis molestie sit amet.
|
Quisque vulputate odio neque, eget laoreet felis molestie sit amet.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class='section' data-title="Data-lab Burglary-free Neighbourhood">
|
<div class='section' data-title="Burglary-Free Neighbourhood">
|
||||||
<h2>Burglary-free Neighbourhood</h2>
|
<h2>Burglary-free Neighbourhood</h2>
|
||||||
<p>
|
<p>
|
||||||
Aenean fringilla nisl sed ante congue, in commodo tellus ullamcorper. Nullam sollicitudin, lacus id
|
Aenean fringilla nisl sed ante congue, in commodo tellus ullamcorper. Nullam sollicitudin, lacus id
|
||||||
|
|
Loading…
Reference in New Issue