├── .gitignore ├── README.md ├── img ├── ideograoh_eurosceptism.jpg ├── ideograph_environmentalism_Asia.jpg └── ideograph_environmentalism_Asia.png ├── index.html ├── index.js ├── lib ├── papaparse.min.js ├── pixi.min.js ├── pixi.min.js.map ├── pixi@4 │ ├── pixi.min.js │ └── pixi.min.js.map ├── viewport.min.js └── viewport.min.js.map ├── old ├── index01.js ├── index02.js └── index03.js ├── sparql ├── CountryList.rq ├── GraphExtraReq.rq └── GraphReq.rq └── yarn-error.log /.gitignore: -------------------------------------------------------------------------------- 1 | yarn.lock 2 | package.json 3 | node_modules/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | __Ideograph__ is a visual tool for exploring ideologies of political parties. It queries its data directly from the frequenty updated WikiData graph database. You can filter the graph by country. Cliking on the node labels allows you to find further information. 2 | 3 | [Use it online](https://ourednik.info/ideograph). 4 | 5 | [Read the documentation](https://ourednik.info/maps/2021/08/13/ideograph-explore-ideologies-of-political-parties-with-spaqrl-requests-to-wikidata-d3-and-pixijs/). 6 | 7 | Ideograph is licenced under [GNU GPL 3.0](https://www.gnu.org/licenses/gpl-3.0.html). 8 | 9 | ![IdeographExample](img/ideograph_environmentalism_Asia.png) 10 | -------------------------------------------------------------------------------- /img/ideograoh_eurosceptism.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aourednik/ideograph/10c123fb326e27d9e0ea0187ad4ea0619efea999/img/ideograoh_eurosceptism.jpg -------------------------------------------------------------------------------- /img/ideograph_environmentalism_Asia.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aourednik/ideograph/10c123fb326e27d9e0ea0187ad4ea0619efea999/img/ideograph_environmentalism_Asia.jpg -------------------------------------------------------------------------------- /img/ideograph_environmentalism_Asia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aourednik/ideograph/10c123fb326e27d9e0ea0187ad4ea0619efea999/img/ideograph_environmentalism_Asia.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IdeoGraph - explorer of ideologies and political parties on WikiData 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 28 | 29 | 30 | 31 |
32 |

IdeoGraph

33 |
explorer of ideologies and political parties on WikiData
34 |
CC BY André Ourednik
35 |
Source : WikiData (live requests to SPARQL endpoint)
36 |
Documentation
37 |
38 |
39 |
40 |
41 | 43 | 45 | 47 |
48 |
49 | Europe (democracies only) 50 | Sub-Saharan Africa 51 | Asia (without Russia) 52 | USA and Canada 53 | Latin America 54 | Russia and Belarus 55 | North Africa 56 | Oceania 57 |
58 |
59 |
60 |
61 | 62 | 63 | 64 | 65 | 66 |
67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | IDEOGRAPH - explore ideologies of political parties with SPAQRL requests to WikiData, D3 and PixiJS. 4 | 5 | Copyright (C) 2021 André Ourednik 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | */ 21 | 22 | let endpoint = "https://query.wikidata.org/sparql?query="; 23 | // messages: 24 | let loadinginfo = d3.select("#loadinginfo"); 25 | let loadingCountries = d3.select("#loadingCountries"); 26 | let countriesLoaded = d3.select("#countriesLoaded"); 27 | let loadingGraph = d3.select("#loadingGraph"); 28 | let constructingGraph = d3.select("#constructingGraph"); 29 | let updatingGraph = d3.select("#updatingGraph"); 30 | let loadinginfotext = ""; 31 | // neccessary globals 32 | let graph, graphstore, canvas ; 33 | 34 | // Make a list of countries 35 | getCountryList(); 36 | 37 | let Europe = ["wd:Q1246", // Kosovo 38 | "wd:Q142", "wd:Q145", // UK 39 | "wd:Q183", "wd:Q189", // Iceland 40 | "wd:Q191", // Estonia 41 | "wd:Q20", // Norway 42 | "wd:Q25", // Wales 43 | "wd:Q211", // Latvia 44 | "wd:Q212", // Ukraine 45 | "wd:Q213", "wd:Q214", // Slovenia 46 | "wd:Q215", // Slovakia 47 | "wd:Q217", // Moldova 48 | "wd:Q218", // Romania 49 | "wd:Q219", "wd:Q221", // Northern Macedonia 50 | "wd:Q222", // Albania 51 | "wd:Q223", // Greenland 52 | "wd:Q224", "wd:Q225", "wd:Q228", // Andora 53 | "wd:Q229", "wd:Q233", // Malta 54 | "wd:Q235", // Monaco 55 | "wd:Q236", // Montenegro 56 | "wd:Q238", // San Marino 57 | "wd:Q27", // Ireland 58 | "wd:Q28", // Hungary 59 | "wd:Q29", // Spain 60 | "wd:Q31", "wd:Q32", // Luxembourg 61 | "wd:Q33", // Finlannd 62 | "wd:Q34", // Sweden 63 | "wd:Q347", // Lichtenstein 64 | "wd:Q35", "wd:Q36", // Poland 65 | "wd:Q37", // Lituania 66 | "wd:Q38","wd:Q39","wd:Q40","wd:Q403", // Serbia 67 | "wd:Q41" ,// Greece 68 | "wd:Q45", // Portugal 69 | "wd:Q4628", // Faroe Islands 70 | "wd:Q55", // Netherlands 71 | "wd:Q9676" // Isle of Man 72 | ]; 73 | 74 | let subsaharanAfrica = ["wd:Q916","wd:Q962","wd:Q963","wd:Q965","wd:Q967","wd:Q1009","wd:Q929","wd:Q657","wd:Q974","wd:Q977","wd:Q983","wd:Q986","wd:Q1050","wd:Q115","wd:Q1000","wd:Q117","wd:Q1006","wd:Q1007","wd:Q1008","wd:Q114","wd:Q1013","wd:Q1014","wd:Q1019","wd:Q1020","wd:Q912","wd:Q1025","wd:Q1029","wd:Q1030","wd:Q1032","wd:Q1033","wd:Q971","wd:Q1041","wd:Q1045","wd:Q34754","wd:Q258","wd:Q1049","wd:Q924","wd:Q1005","wd:Q945","wd:Q1036","wd:Q953","wd:Q954"] 75 | let Asia = ["wd:Q851","wd:Q40362", "wd:Q244165", "wd:Q1027", "wd:Q826", "wd:Q801", "wd:Q574","wd:Q889", "wd:Q399", "wd:Q619829", "wd:Q227", "wd:Q398", "wd:Q902", "wd:Q917", "wd:Q424", "wd:Q326343", "wd:Q230", "wd:Q8646", "wd:Q668", "wd:Q252", "wd:Q17", "wd:Q810", "wd:Q232", "wd:Q41470", "wd:Q205047", "wd:Q817", "wd:Q813", "wd:Q819", "wd:Q822", "wd:Q14773", "wd:Q833", "wd:Q711", "wd:Q836", "wd:Q837", "wd:Q423", "wd:Q843", "wd:Q148", "wd:Q928", "wd:Q334", "wd:Q884", "wd:Q23427", "wd:Q854", "wd:Q219060", "wd:Q858", "wd:Q865", "wd:Q863", "wd:Q869", "wd:Q43", "wd:Q23681", "wd:Q874", "wd:Q1498", "wd:Q265", "wd:Q881"] 76 | let CanadaAndUS = ["wd:Q16","wd:Q30"]; 77 | let LatinAmerica = ["wd:Q414","wd:Q21203","wd:Q242","wd:Q23635","wd:Q155","wd:Q5785","wd:Q298","wd:Q739","wd:Q800","wd:Q241","wd:Q784","wd:Q786","wd:Q736","wd:Q792","wd:Q769","wd:Q774","wd:Q734","wd:Q790","wd:Q783","wd:Q766","wd:Q96","wd:Q811","wd:Q804","wd:Q733","wd:Q419","wd:Q730","wd:Q754","wd:Q18221","wd:Q77","wd:Q717"]; 78 | let RussiaAndBelarus = ["wd:Q184","wd:Q159"]; 79 | let NorthAfrica =["wd:Q262","wd:Q79","wd:Q1016","wd:Q1028","wd:Q948"]; 80 | let Oceania = [ "wd:Q408", "wd:Q26988", "wd:Q712", "wd:Q697", "wd:Q664", "wd:Q691", "wd:Q683", "wd:Q678", "wd:Q686"]; 81 | 82 | // INITIALISATION 83 | document.getElementById("upgradeGraphButton").disabled = true; 84 | getGraphData(Europe); 85 | 86 | /** Fetches csv data wrom wikidata 87 | * @param req a URI ecoded SPARQL query 88 | */ 89 | async function fetchWikiData(req) { 90 | let response = await fetch(req, {headers: { "Accept": "text/csv"}}); 91 | let text = await response.text(); 92 | let data = Papa.parse(text,{ 93 | header:true, 94 | skipEmptyLines:true, 95 | transformHeader: function(h) {return h.trim();} // remove white spaces from header vars 96 | }); 97 | data = data.data; 98 | return data ; 99 | } 100 | 101 | /** Constructs a list of countnries to choose from. 102 | * On first run, launch graph construction. */ 103 | async function getCountryList() { 104 | loadinginfo.style('display', 'block'); 105 | loadingCountries.style('display', 'block'); 106 | let sparql = await (await fetch('sparql/CountryList.rq')).text(); 107 | let req = endpoint + encodeURIComponent(sparql.replace("/#.*/gm",'')); 108 | let countries = await fetchWikiData(req); 109 | // console.log(countries); 110 | countries.sort((a,b) => (a["countryLabel"] > b["countryLabel"]) ? 1 : ((b["countryLabel"] > a["countryLabel"]) ? -1 : 0)) 111 | let countriesdiv = d3.select("#countryselector"); 112 | countries.forEach(c=>{ 113 | let newdiv = countriesdiv.append("div") 114 | let cval = c.country.replace("http://www.wikidata.org/entity/","wd:"); 115 | let cid = cval.replace("wd:","c") 116 | newdiv 117 | .append("input") 118 | .attr("type","checkbox") 119 | .attr("name", c["countryLabel"]) 120 | .attr("id",cid) 121 | .attr("value", cval) 122 | //.attr("onclick","updateGraph()") 123 | ; 124 | newdiv 125 | .append("label") 126 | .append("a") 127 | .attr("href",c.country) 128 | .attr("target","_blank") 129 | .text(c["countryLabel"]) 130 | ; 131 | }); 132 | Europe.forEach(cval => document.getElementById(cval.replace("wd:","c")).checked = true); 133 | loadingCountries.style('display', 'none'); 134 | countriesLoaded.style('display', 'block'); 135 | } 136 | 137 | let dataExtra; 138 | let parties = [] 139 | /** Fetches the graph data from wikidata 140 | * @param countries An array of countries 141 | */ 142 | async function getGraphData(countries) { 143 | loadinginfo.style('display', 'block'); 144 | loadingGraph.style('display', 'block'); 145 | let sparql1 = await (await fetch('sparql/GraphReq.rq')).text(); 146 | let req = endpoint + encodeURIComponent(sparql1.replace("JSVAR:COUNTRIES",countries.join(" ")).replace("/#.*/gm",'')); 147 | let sparql2 = await (await fetch('sparql/GraphExtraReq.rq')).text(); 148 | let reqExtra = endpoint + encodeURIComponent(sparql2.replace("JSVAR:COUNTRIES",countries.join(" ")).replace("/#.*/gm",'')); 149 | let [data,dataExtra] = await Promise.all([ 150 | fetchWikiData(req), fetchWikiData(reqExtra) 151 | ]); 152 | loadingGraph.text("Fetching extra graph links from WikiData..."); 153 | // console.log(dataExtra); 154 | // let parties = []; // for later filtering out ideology nodes with no incoming parties 155 | let nodes = []; 156 | let links = []; 157 | data.forEach((line)=>{ 158 | if (typeof line.item !== "undefined" & typeof line.linkTo !== "undefined") { 159 | parties.push(line.item.replace("http://www.wikidata.org/entity/","wd:")); 160 | nodes.push({ 161 | id: line.item.replace("http://www.wikidata.org/entity/","wd:"), 162 | label : line.itemLabel + " (" + line.countryLabel + ")" , 163 | group : 1 164 | }) ; 165 | nodes.push({ 166 | id: line.linkTo.replace("http://www.wikidata.org/entity/","wd:"), 167 | label : line["linkToLabel"], 168 | group : 2 169 | }) ; 170 | links.push({ 171 | source: line.item.replace("http://www.wikidata.org/entity/","wd:"), 172 | target: line.linkTo.replace("http://www.wikidata.org/entity/","wd:"), 173 | value: 0.5 174 | }); 175 | } 176 | }); 177 | dataExtra.forEach((line)=>{ 178 | if (typeof line.linkTo !== "undefined" & typeof line.superLinkTo !== "undefined") { 179 | nodes.push({ 180 | id: line.linkTo.replace("http://www.wikidata.org/entity/","wd:"), 181 | label : line.linkToLabel, 182 | group : 2 183 | }) ; 184 | nodes.push({ 185 | id: line.superLinkTo.replace("http://www.wikidata.org/entity/","wd:"), 186 | label : line.superLinkToLabel, // might need to remove \r from the csv in csvToArray, 187 | group : 2 188 | }) ; 189 | links.push({ 190 | source: line.linkTo.replace("http://www.wikidata.org/entity/","wd:"), 191 | target: line.superLinkTo.replace("http://www.wikidata.org/entity/","wd:"), 192 | value: 0.8 193 | }); 194 | } 195 | }); 196 | nodes = nodes.filter((e, i) => nodes.findIndex(a => a.id === e.id) === i); // get only unique nodes. 197 | // graph.links.filter(l => parties.includes(l.source.id)) 198 | // we'll filter the links later with isNaN(radisu) 199 | graph = {links:links,nodes:nodes}; 200 | // store the full graph for later use 201 | graphstore = Object.assign({}, graph); 202 | drawGraph(graph); 203 | } 204 | 205 | 206 | let width = screen.availWidth, height = screen.availHeight; 207 | function colour(num){ 208 | if (num > 1) return 0xD01B1B 209 | return 0x47abd8 ; 210 | } 211 | 212 | /* 213 | let colour = (function() { 214 | let scale = d3.scaleOrdinal(d3.schemeCategory20); 215 | return (num) => parseInt(scale(num).slice(1), 16); 216 | })(); 217 | */ 218 | 219 | let simulation = d3.forceSimulation() 220 | .force('link', d3.forceLink().id((d) => d.id)) 221 | .force('charge', d3.forceManyBody().strength(d => d.group == 2 ? -500 : -5)) 222 | // .force('center', d3.forceCenter(width / 2, height / 2) ) 223 | .force("x", d3.forceX(width / 2).strength(0.5)) 224 | .force("y", d3.forceY(height / 2).strength(0.5)) 225 | .force("collide",d3.forceCollide().radius(4.5)) // d => d.radius is slow 226 | .alphaDecay(0.05) 227 | ; 228 | 229 | let app = new PIXI.Application({ 230 | width : width, 231 | height : height , 232 | antialias: !0, 233 | transparent: !0, 234 | resolution: 1 235 | }); // Convenience class that automatically creates the renderer, ticker and root container. 236 | document.body.appendChild(app.view); 237 | 238 | /** Draws the graph using D3js and PIXIjs 239 | * @param graph A JSON encoded set of nodes and links 240 | */ 241 | function drawGraph(graph) { 242 | constructingGraph.style('display', 'block'); 243 | console.log(graph); 244 | 245 | // TRANSFORM THE DATA INTO A D3 GRAPH 246 | simulation 247 | .nodes(graph.nodes) 248 | .on('tick', ticked) // this d3 ticker can be replaced by PIXI's "requestAnimationFrame" but the system is then too excited 249 | .force('link') 250 | .links(graph.links); 251 | 252 | // count incoming links to set node sizes, and remove nodes with no radius, stemming from super-ideologies 253 | graph.links.forEach(function(link){ 254 | if (!link.target["linkCount"]) link.target["linkCount"] = 0; 255 | link.target["linkCount"]++; 256 | }); 257 | graph.nodes.forEach((node) => { 258 | node.radius = node.group < 2 ? 3 : 3 + Math.sqrt(node.linkCount); 259 | }); 260 | graph.links = graph.links.filter(l => ! isNaN(l.source.radius)); 261 | // remove freely floating nodes 262 | graph.nodes = graph.nodes.filter(n => graph.links.filter(l => 263 | l.source == n | l.target == n 264 | ).length > 0 ); 265 | 266 | // Render with PIXI ------ 267 | 268 | // the LINKS are just one object that actually gets drawn in the ticks: 269 | let containerLinks = new PIXI.Container(); 270 | let links = new PIXI.Graphics(); 271 | containerLinks.addChild(links); 272 | 273 | // render NODES 274 | 275 | let containerParties = new PIXI.Container(); 276 | let containerIdeologies = new PIXI.Container(); 277 | // https://stackoverflow.com/questions/36678727/pixi-js-drag-and-drop-circle 278 | graph.nodes.forEach((node) => { 279 | node.gfx = new PIXI.Graphics(); 280 | node.gfx.lineStyle(0.5, 0xFFFFFF); 281 | node.gfx.beginFill(colour(node.group)); 282 | node.gfx.drawCircle(0, 0, node.radius ); 283 | node.gfx.interactive = true; 284 | node.gfx.hitArea = new PIXI.Circle(0, 0, node.radius); 285 | node.gfx.mouseover = function(ev) { showHoverLabel(node, ev)}; 286 | node.gfx.on("pointerdown", function(ev) { focus(node,ev);}); 287 | node.gfx 288 | .on('mousedown', onDragStart) 289 | .on('touchstart', onDragStart) 290 | .on('mouseup', onDragEnd ) 291 | .on('mouseupoutside', onDragEnd ) 292 | .on('touchend', onDragEnd) 293 | .on('touchendoutside', onDragEnd) 294 | .on('mousemove', onDragMove) 295 | .on('touchmove', onDragMove) 296 | ; 297 | 298 | 299 | if (node.group==1) containerParties.addChild(node.gfx); 300 | if (node.group==2) containerIdeologies.addChild(node.gfx); 301 | // stage.addChild(node.gfx); 302 | 303 | if (node.group == 2) { 304 | node.lgfx = new PIXI.Text( 305 | node.label, { 306 | fontFamily : 'Maven Pro', 307 | fontSize: 9 + node.radius / 2, 308 | fill : colour(node.group), 309 | align : 'center' 310 | } 311 | ); 312 | node.lgfx.resolution = 2; // so that the text isn't blury 313 | containerIdeologies.addChild(node.lgfx); 314 | } 315 | }); 316 | 317 | 318 | containerLinks.zIndex = 0; 319 | containerIdeologies.zIndex = 2; 320 | containerParties.zIndex = 1; 321 | app.stage.addChild(containerLinks); 322 | app.stage.addChild(containerParties); 323 | app.stage.addChild(containerIdeologies); 324 | app.stage.children.sort((itemA, itemB) => itemA.zIndex - itemB.zIndex); 325 | 326 | // dragging the nodes around is perhaps less useful than zooming 327 | canvas = d3.select(app.view) 328 | canvas.call( 329 | d3.zoom().scaleExtent([0.5, 3]).on("zoom", zoomAndPan) 330 | ); 331 | 332 | // ticked() 333 | function ticked() { 334 | // requestAnimationFrame(ticked); //this d3 on.ticker can be replaced by PIXI's "requestAnimationFrame" but the system is then too excited. See above 335 | graph.nodes.forEach((node) => { 336 | let { x, y, gfx, lgfx, radius } = node; 337 | gfx.position = new PIXI.Point(x, y); 338 | if (node.group == 2) lgfx.position = new PIXI.Point(x + radius / 2, y + radius /2); 339 | }); 340 | links.clear(); 341 | links.alpha = 0.6; 342 | graph.links.forEach((link) => { 343 | let { source, target } = link; 344 | links.lineStyle(Math.sqrt(link.value), 0x999999,link.alpha); 345 | links.moveTo(source.x, source.y); 346 | links.lineTo(target.x, target.y); 347 | }); 348 | links.endFill(); 349 | // renderer.render(stage); // not necessary if using app. 350 | 351 | // when this point is reached, the notification about loading can be removed 352 | loadinginfo.style('display', 'none'); 353 | constructingGraph.style('display', 'none'); 354 | document.getElementById("upgradeGraphButton").disabled = false; 355 | } 356 | 357 | simulation.alphaTarget(0.05).restart(); // give it an initial push 358 | } 359 | 360 | // DRAG, PAN AND ZOOM 361 | 362 | var transform = {k:1,x:0,y:0}; 363 | function zoomAndPan() { 364 | // console.log(d3.event.transform); 365 | transform = d3.event.transform; 366 | app.stage.scale.x = app.stage.scale.y = d3.event.transform.k; 367 | if(!draggingNode ) { 368 | app.stage.x = transform.x; 369 | app.stage.y = transform.y; 370 | } 371 | } 372 | 373 | // pixi node drag 374 | let draggingNode = false; 375 | function onDragStart(event){ 376 | simulation.alphaTarget(0.05).restart(); // the higer, the more sensitive and excited. 377 | this.data = event.data; 378 | var newPosition = this.data.getLocalPosition(this.parent); 379 | let node = graph.nodes.filter(n=>n.gfx == this)[0]; 380 | node.fx = newPosition.x; 381 | node.fy = newPosition.y; 382 | this.dragging = true; 383 | draggingNode = true; 384 | } 385 | 386 | function onDragEnd(){ 387 | this.dragging = false; 388 | draggingNode = false; 389 | this.data = null; 390 | let node = graph.nodes.filter(n=>n.gfx == this)[0]; 391 | node.fx = null; 392 | node.fy = null; 393 | } 394 | 395 | function onDragMove(){ 396 | if (this.dragging){ 397 | var newPosition = this.data.getLocalPosition(this.parent); 398 | let node = graph.nodes.filter(n=>n.gfx == this)[0]; 399 | node.fx = newPosition.x; 400 | node.fy = newPosition.y; 401 | } 402 | } 403 | 404 | 405 | function unSelectAllCountries(){ 406 | let allBoxes = d3.selectAll("input[type='checkbox']"); 407 | allBoxes._groups[0].forEach(b=>{b.checked = false}); 408 | } 409 | 410 | function selectGroupAndUpdate(group){ 411 | console.log(group); 412 | unSelectAllCountries(); 413 | let allBoxes = d3.selectAll("input[type='checkbox']"); 414 | allBoxes._groups[0].forEach(b=>{ 415 | if (group.includes(b.value)) b.checked = true 416 | }); 417 | updateGraph(); 418 | } 419 | 420 | // Graph hover and highlight ------- 421 | 422 | let rootSelectedNode = {}; 423 | 424 | // https://observablehq.com/@d3/drag-zoom 425 | 426 | function showHoverLabel(node,ev) { 427 | let nodex = (ev.data.global.x + 15) ; 428 | let nodey = (ev.data.global.y - 15) ; 429 | d3.select("#label") 430 | .attr("style", "left:"+nodex+"px;top:"+nodey+"px;") 431 | .select("a") 432 | .attr("href",node.id.replace("wd:","http://www.wikidata.org/entity/")) 433 | .attr("target","_blank") 434 | .text(node.label) 435 | } 436 | 437 | function focus(d,ev) { 438 | console.log(d); 439 | showHoverLabel(d,ev); // nececessary for touch screen 440 | if (rootSelectedNode == d) { 441 | unfocus(); 442 | } else { 443 | rootSelectedNode = d; 444 | markSelected(d); 445 | } 446 | updateColor(); 447 | } 448 | 449 | function unfocus() { 450 | graph.nodes.forEach(n => {n.marked = true}); 451 | graph.links.forEach(l => {l.marked = true}); 452 | rootSelectedNode = {}; 453 | } 454 | 455 | function markSelected(d){ 456 | graph.nodes.forEach(n => {n.marked = false}) 457 | graph.links.forEach(l => {l.marked = false}) 458 | d.marked = true; 459 | let linked = []; 460 | graph.links.filter(l => 461 | l.source == d | l.target == d 462 | ).forEach(l => { 463 | l.marked = true; 464 | linked.push(l.source.id); 465 | linked.push(l.target.id) 466 | }); 467 | graph.nodes.forEach(n => n.marked = linked.includes(n.id) ? true : false) 468 | } 469 | 470 | function updateColor() { 471 | graph.nodes.filter(n => !n.marked).forEach(n => { 472 | n.gfx.alpha = 0.2; 473 | if (n.group == 2) n.lgfx.alpha=0.2 474 | }); 475 | graph.links.filter(l => !l.marked).forEach(l => l.alpha = 0.1 ); 476 | graph.nodes.filter(n => n.marked).forEach(n => { 477 | n.gfx.alpha = 1; 478 | if (n.group == 2) n.lgfx.alpha =1 479 | }); 480 | graph.links.filter(l => l.marked).forEach(l => l.alpha = 1); 481 | } 482 | 483 | 484 | // Graph updates ------------ 485 | 486 | /** Updates the graph with data from a new set of countries */ 487 | function updateGraph(){ 488 | document.getElementById("upgradeGraphButton").disabled = true; 489 | simulation.stop(); 490 | graph = graphstore = null; 491 | loadinginfo.style('display', 'block'); 492 | updatingGraph.style('display', 'block'); 493 | let checked = []; 494 | let boxes = d3.selectAll("input[type='checkbox']:checked") 495 | boxes._groups[0].forEach(b=>{ 496 | checked.push(b.value) 497 | }); 498 | console.log(checked); 499 | app.stage.removeChildren(); 500 | // wait before launching 501 | getGraphData(checked); 502 | } 503 | 504 | // TODO add element without destroying everything 505 | function restoreGraph(){ 506 | // add all elements to graph removed by previous filter 507 | graphstore.nodes.forEach(sn => { 508 | if (graph.nodes.filter(n=> n.id == sn.id).length==0) graph.nodes.push(Object.assign({}, sn)); 509 | }) 510 | // TODO : something's wrong with attaching those links 511 | graphstore.links.forEach(sl => { 512 | if (graph.links.filter(l=> l.id == sl.id).length==0) graph.links.push(Object.assign({}, sl)); 513 | }) 514 | // relink nodes correcly 515 | graph.links.forEach(l => { 516 | l.source = graph.nodes.filter(n=> n.id == l.source.id)[0]; 517 | l.target = graph.nodes.filter(n=> n.id == l.target.id)[0]; 518 | }); 519 | } 520 | -------------------------------------------------------------------------------- /lib/papaparse.min.js: -------------------------------------------------------------------------------- 1 | /* @license 2 | Papa Parse 3 | v5.0.2 4 | https://github.com/mholt/PapaParse 5 | License: MIT 6 | */ 7 | !function(e,t){"function"==typeof define&&define.amd?define([],t):"object"==typeof module&&"undefined"!=typeof exports?module.exports=t():e.Papa=t()}(this,function s(){"use strict";var f="undefined"!=typeof self?self:"undefined"!=typeof window?window:void 0!==f?f:{};var n=!f.document&&!!f.postMessage,o=n&&/blob:/i.test((f.location||{}).protocol),a={},h=0,b={parse:function(e,t){var r=(t=t||{}).dynamicTyping||!1;q(r)&&(t.dynamicTypingFunction=r,r={});if(t.dynamicTyping=r,t.transform=!!q(t.transform)&&t.transform,t.worker&&b.WORKERS_SUPPORTED){var i=function(){if(!b.WORKERS_SUPPORTED)return!1;var e=(r=f.URL||f.webkitURL||null,i=s.toString(),b.BLOB_URL||(b.BLOB_URL=r.createObjectURL(new Blob(["(",i,")();"],{type:"text/javascript"})))),t=new f.Worker(e);var r,i;return t.onmessage=_,t.id=h++,a[t.id]=t}();return i.userStep=t.step,i.userChunk=t.chunk,i.userComplete=t.complete,i.userError=t.error,t.step=q(t.step),t.chunk=q(t.chunk),t.complete=q(t.complete),t.error=q(t.error),delete t.worker,void i.postMessage({input:e,config:t,workerId:i.id})}var n=null;b.NODE_STREAM_INPUT,"string"==typeof e?n=t.download?new l(t):new p(t):!0===e.readable&&q(e.read)&&q(e.on)?n=new m(t):(f.File&&e instanceof File||e instanceof Object)&&(n=new c(t));return n.stream(e)},unparse:function(e,t){var i=!1,_=!0,g=",",v="\r\n",n='"',s=n+n,r=!1,a=null;!function(){if("object"!=typeof t)return;"string"!=typeof t.delimiter||b.BAD_DELIMITERS.filter(function(e){return-1!==t.delimiter.indexOf(e)}).length||(g=t.delimiter);("boolean"==typeof t.quotes||Array.isArray(t.quotes))&&(i=t.quotes);"boolean"!=typeof t.skipEmptyLines&&"string"!=typeof t.skipEmptyLines||(r=t.skipEmptyLines);"string"==typeof t.newline&&(v=t.newline);"string"==typeof t.quoteChar&&(n=t.quoteChar);"boolean"==typeof t.header&&(_=t.header);if(Array.isArray(t.columns)){if(0===t.columns.length)throw new Error("Option columns is empty");a=t.columns}void 0!==t.escapeChar&&(s=t.escapeChar+n)}();var o=new RegExp(U(n),"g");"string"==typeof e&&(e=JSON.parse(e));if(Array.isArray(e)){if(!e.length||Array.isArray(e[0]))return u(null,e,r);if("object"==typeof e[0])return u(a||h(e[0]),e,r)}else if("object"==typeof e)return"string"==typeof e.data&&(e.data=JSON.parse(e.data)),Array.isArray(e.data)&&(e.fields||(e.fields=e.meta&&e.meta.fields),e.fields||(e.fields=Array.isArray(e.data[0])?e.fields:h(e.data[0])),Array.isArray(e.data[0])||"object"==typeof e.data[0]||(e.data=[e.data])),u(e.fields||[],e.data||[],r);throw new Error("Unable to serialize unrecognized input");function h(e){if("object"!=typeof e)return[];var t=[];for(var r in e)t.push(r);return t}function u(e,t,r){var i="";"string"==typeof e&&(e=JSON.parse(e)),"string"==typeof t&&(t=JSON.parse(t));var n=Array.isArray(e)&&0=this._config.preview;if(o)f.postMessage({results:n,workerId:b.WORKER_ID,finished:a});else if(q(this._config.chunk)&&!t){if(this._config.chunk(n,this._handle),this._handle.paused()||this._handle.aborted())return void(this._halted=!0);n=void 0,this._completeResults=void 0}return this._config.step||this._config.chunk||(this._completeResults.data=this._completeResults.data.concat(n.data),this._completeResults.errors=this._completeResults.errors.concat(n.errors),this._completeResults.meta=n.meta),this._completed||!a||!q(this._config.complete)||n&&n.meta.aborted||(this._config.complete(this._completeResults,this._input),this._completed=!0),a||n&&n.meta.paused||this._nextChunk(),n}this._halted=!0},this._sendError=function(e){q(this._config.error)?this._config.error(e):o&&this._config.error&&f.postMessage({workerId:b.WORKER_ID,error:e,finished:!1})}}function l(e){var i;(e=e||{}).chunkSize||(e.chunkSize=b.RemoteChunkSize),u.call(this,e),this._nextChunk=n?function(){this._readChunk(),this._chunkLoaded()}:function(){this._readChunk()},this.stream=function(e){this._input=e,this._nextChunk()},this._readChunk=function(){if(this._finished)this._chunkLoaded();else{if(i=new XMLHttpRequest,this._config.withCredentials&&(i.withCredentials=this._config.withCredentials),n||(i.onload=y(this._chunkLoaded,this),i.onerror=y(this._chunkError,this)),i.open("GET",this._input,!n),this._config.downloadRequestHeaders){var e=this._config.downloadRequestHeaders;for(var t in e)i.setRequestHeader(t,e[t])}if(this._config.chunkSize){var r=this._start+this._config.chunkSize-1;i.setRequestHeader("Range","bytes="+this._start+"-"+r)}try{i.send()}catch(e){this._chunkError(e.message)}n&&0===i.status?this._chunkError():this._start+=this._config.chunkSize}},this._chunkLoaded=function(){4===i.readyState&&(i.status<200||400<=i.status?this._chunkError():(this._finished=!this._config.chunkSize||this._start>function(e){var t=e.getResponseHeader("Content-Range");if(null===t)return-1;return parseInt(t.substr(t.lastIndexOf("/")+1))}(i),this.parseChunk(i.responseText)))},this._chunkError=function(e){var t=i.statusText||e;this._sendError(new Error(t))}}function c(e){var i,n;(e=e||{}).chunkSize||(e.chunkSize=b.LocalChunkSize),u.call(this,e);var s="undefined"!=typeof FileReader;this.stream=function(e){this._input=e,n=e.slice||e.webkitSlice||e.mozSlice,s?((i=new FileReader).onload=y(this._chunkLoaded,this),i.onerror=y(this._chunkError,this)):i=new FileReaderSync,this._nextChunk()},this._nextChunk=function(){this._finished||this._config.preview&&!(this._rowCount=this._input.size,this.parseChunk(e.target.result)},this._chunkError=function(){this._sendError(i.error)}}function p(e){var r;u.call(this,e=e||{}),this.stream=function(e){return r=e,this._nextChunk()},this._nextChunk=function(){if(!this._finished){var e=this._config.chunkSize,t=e?r.substr(0,e):r;return r=e?r.substr(e):"",this._finished=!r,this.parseChunk(t)}}}function m(e){u.call(this,e=e||{});var t=[],r=!0,i=!1;this.pause=function(){u.prototype.pause.apply(this,arguments),this._input.pause()},this.resume=function(){u.prototype.resume.apply(this,arguments),this._input.resume()},this.stream=function(e){this._input=e,this._input.on("data",this._streamData),this._input.on("end",this._streamEnd),this._input.on("error",this._streamError)},this._checkIsFinished=function(){i&&1===t.length&&(this._finished=!0)},this._nextChunk=function(){this._checkIsFinished(),t.length?this.parseChunk(t.shift()):r=!0},this._streamData=y(function(e){try{t.push("string"==typeof e?e:e.toString(this._config.encoding)),r&&(r=!1,this._checkIsFinished(),this.parseChunk(t.shift()))}catch(e){this._streamError(e)}},this),this._streamError=y(function(e){this._streamCleanUp(),this._sendError(e)},this),this._streamEnd=y(function(){this._streamCleanUp(),i=!0,this._streamData("")},this),this._streamCleanUp=y(function(){this._input.removeListener("data",this._streamData),this._input.removeListener("end",this._streamEnd),this._input.removeListener("error",this._streamError)},this)}function r(g){var a,o,h,i=Math.pow(2,53),n=-i,s=/^\s*-?(\d*\.?\d+|\d+\.?\d*)(e[-+]?\d+)?\s*$/i,u=/(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/,t=this,r=0,f=0,d=!1,e=!1,l=[],c={data:[],errors:[],meta:{}};if(q(g.step)){var p=g.step;g.step=function(e){if(c=e,_())m();else{if(m(),0===c.data.length)return;r+=e.data.length,g.preview&&r>g.preview?o.abort():p(c,t)}}}function v(e){return"greedy"===g.skipEmptyLines?""===e.join("").trim():1===e.length&&0===e[0].length}function m(){if(c&&h&&(k("Delimiter","UndetectableDelimiter","Unable to auto-detect delimiting character; defaulted to '"+b.DefaultDelimiter+"'"),h=!1),g.skipEmptyLines)for(var e=0;e=l.length?"__parsed_extra":l[r]),g.transform&&(s=g.transform(s,n)),s=y(n,s),"__parsed_extra"===n?(i[n]=i[n]||[],i[n].push(s)):i[n]=s}return g.header&&(r>l.length?k("FieldMismatch","TooManyFields","Too many fields: expected "+l.length+" fields but parsed "+r,f+t):r=i.length/2?"\r\n":"\r"}(e,i)),h=!1,g.delimiter)q(g.delimiter)&&(g.delimiter=g.delimiter(e),c.meta.delimiter=g.delimiter);else{var n=function(e,t,r,i,n){var s,a,o,h;n=n||[",","\t","|",";",b.RECORD_SEP,b.UNIT_SEP];for(var u=0;u=L)return R(!0)}else for(g=M,M++;;){if(-1===(g=a.indexOf(O,g+1)))return t||u.push({type:"Quotes",code:"MissingQuotes",message:"Quoted field unterminated",row:h.length,index:M}),w();if(g===i-1)return w(a.substring(M,g).replace(_,O));if(O!==z||a[g+1]!==z){if(O===z||0===g||a[g-1]!==z){var y=E(-1===m?p:Math.min(p,m));if(a[g+1+y]===D){f.push(a.substring(M,g).replace(_,O)),a[M=g+1+y+e]!==O&&(g=a.indexOf(O,M)),p=a.indexOf(D,M),m=a.indexOf(I,M);break}var k=E(m);if(a.substr(g+1+k,n)===I){if(f.push(a.substring(M,g).replace(_,O)),C(g+1+k+n),p=a.indexOf(D,M),g=a.indexOf(O,M),o&&(S(),j))return R();if(L&&h.length>=L)return R(!0);break}u.push({type:"Quotes",code:"InvalidQuotes",message:"Trailing quote on quoted field is malformed",row:h.length,index:M}),g++}}else g++}return w();function b(e){h.push(e),d=M}function E(e){var t=0;if(-1!==e){var r=a.substring(g+1,e);r&&""===r.trim()&&(t=r.length)}return t}function w(e){return t||(void 0===e&&(e=a.substr(M)),f.push(e),M=i,b(f),o&&S()),R()}function C(e){M=e,b(f),f=[],m=a.indexOf(I,M)}function R(e,t){return{data:t||!1?h[0]:h,errors:u,meta:{delimiter:D,linebreak:I,aborted:j,truncated:!!e,cursor:d+(r||0)}}}function S(){A(R(void 0,!0)),h=[],u=[]}function x(e,t,r){var i={nextDelim:void 0,quoteSearch:void 0},n=a.indexOf(O,t+1);if(t. 19 | */ 20 | 21 | 22 | let endpoint = "https://query.wikidata.org/sparql?query="; 23 | let loadinginfo = d3.select("#loadinginfo") 24 | let loadinginfotext = ""; 25 | let graph, graphstore ; // neccessary globals 26 | 27 | // Make a list of countries 28 | let sparql = ` 29 | SELECT DISTINCT ?country ?countryLabel 30 | WHERE { 31 | ?item wdt:P1142 ?linkTo . 32 | ?linkTo wdt:P31 wd:Q12909644 . # keep only targets that are political ideologies 33 | VALUES ?type { wd:Q7278 wd:Q24649 } # filter by these types of political actors 34 | ?item wdt:P31 ?type . 35 | ?item wdt:P17 ?country . 36 | MINUS { ?item wdt:P576 ?abolitionDate } # exclude abolished parties 37 | MINUS { ?country wdt:P576 ?countryAbolitionDate }. # exclude abolished countries 38 | SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en" . } 39 | } 40 | `; 41 | let req = endpoint + encodeURIComponent(sparql); 42 | getCountryList(req); 43 | 44 | let initialCountries = [ // Europe 45 | "wd:Q1246", // Kosovo 46 | "wd:Q142", 47 | "wd:Q145", // UK 48 | "wd:Q183", 49 | "wd:Q189", // Iceland 50 | "wd:Q191", // Estonia 51 | "wd:Q20", // Norway 52 | "wd:Q25", // Wales 53 | "wd:Q211", // Latvia 54 | "wd:Q213", 55 | "wd:Q214", // Sloveni 56 | "wd:Q215", // Slovakia 57 | "wd:Q217", // Moldova 58 | "wd:Q218", // Romania 59 | "wd:Q219", 60 | "wd:Q222", // Albania 61 | "wd:Q223", // Greenland 62 | "wd:Q224", 63 | "wd:Q225", 64 | "wd:Q228", // Andora 65 | "wd:Q229", 66 | "wd:Q233", // Malta 67 | "wd:Q235", // Monaco 68 | "wd:Q236", // Montenegro 69 | "wd:Q238", // San Marino 70 | "wd:Q27", // Ireland 71 | "wd:Q28", // Hungary 72 | "wd:Q29", // Spain 73 | "wd:Q31", 74 | "wd:Q32", // Luxembourg 75 | "wd:Q33", // Finlannd 76 | "wd:Q34", // Sweden 77 | "wd:Q347", // Lichtenstein 78 | "wd:Q35", 79 | "wd:Q36", // Poland 80 | "wd:Q37", // Lituania 81 | "wd:Q38", 82 | "wd:Q39", 83 | "wd:Q40", 84 | "wd:Q403", // Serbia 85 | "wd:Q41" ,// Greece 86 | "wd:Q45", // Portugal 87 | "wd:Q4628", // Faroe Islands 88 | "wd:Q55", // Netherlands 89 | "wd:Q9676" // Isle of Man 90 | ]; 91 | 92 | makeCountriesGraph(initialCountries); 93 | 94 | function makeCountriesGraph(countries) { 95 | sparql = ` 96 | SELECT DISTINCT ?item ?itemLabel ?country ?countryLabel ?linkTo ?linkToLabel 97 | WHERE { 98 | ?item wdt:P1142 | wdt:P1142*/wdt:P279* ?linkTo . # alternative path makes link to ideology superclass 99 | VALUES ?ideatype { wd:Q7257 wd:Q5333510 wd:Q12909644 wd:Q179805} # ideologie ou philosophie politique ou économique 100 | ?linkTo wdt:P31 ?ideatype . 101 | VALUES ?type { wd:Q7278 wd:Q24649 } # filter by these types of political actors 102 | ?item wdt:P31 ?type . 103 | ?item wdt:P17 ?country . 104 | MINUS { ?item wdt:P576 ?abolitionDate } # exclude abolished parties 105 | MINUS { ?country wdt:P576 ?countryAbolitionDate }. # exclude abolished countries 106 | SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en" . } 107 | } 108 | `; 109 | let req = endpoint + encodeURIComponent(sparql); 110 | getGraphData(req); 111 | } 112 | 113 | 114 | // Constructs a list of countnries to choose from. 115 | // On first run, launch graph construction. 116 | async function getCountryList(req) { 117 | loadinginfotext += "Loading list of countries... "; 118 | loadinginfo.style('display', 'block').text(loadinginfotext); 119 | let response = await fetch(req, { 120 | headers: { 121 | "Accept": "text/csv" 122 | } 123 | }); 124 | let text = await response.text(); 125 | let countries = csvToArray(text); 126 | console.log(countries); 127 | countries.sort((a,b) => (a["countryLabel\r"] > b["countryLabel\r"]) ? 1 : ((b["countryLabel\r"] > a["countryLabel\r"]) ? -1 : 0)) 128 | let countriesdiv = d3.select("#countryselector"); 129 | countries.forEach(c=>{ 130 | let newdiv = countriesdiv.append("div") 131 | let cval = c.country.replace("http://www.wikidata.org/entity/","wd:"); 132 | let cid = cval.replace("wd:","c") 133 | newdiv 134 | .append("input") 135 | .attr("type","checkbox") 136 | .attr("name", c["countryLabel\r"]) 137 | .attr("id",cid) 138 | .attr("value", cval) 139 | .attr("onclick","updateGraph()") 140 | ; 141 | newdiv 142 | .append("label") 143 | .append("a") 144 | .attr("href",c.country) 145 | .attr("target","_blank") 146 | .text(c["countryLabel\r"]) 147 | ; 148 | }); 149 | initialCountries.forEach(cval => document.getElementById(cval.replace("wd:","c")).checked = true); 150 | } 151 | 152 | 153 | async function getGraphData(req) { 154 | loadinginfotext += "Fetching graph data...\n"; 155 | loadinginfo.style('display', 'block').text(loadinginfotext); 156 | let response = await fetch(req, { 157 | headers: { 158 | "Accept": "text/csv" 159 | } 160 | }); 161 | let text = await response.text(); 162 | let data = csvToArray(text); 163 | console.log(data); 164 | let nodes = []; 165 | let links = []; 166 | data.forEach((line)=>{ 167 | if (typeof line.item !== "undefined" & typeof line.linkTo !== "undefined") { 168 | nodes.push({ 169 | id: line.item.replace("http://www.wikidata.org/entity/","wd:"), 170 | label : line.itemLabel, 171 | group : 1 172 | }) ; 173 | nodes.push({ 174 | id: line.linkTo.replace("http://www.wikidata.org/entity/","wd:"), 175 | label : line["linkToLabel\r"], // might need to remove \r from the csv in csvToArray, 176 | group : 2 177 | }) ; 178 | links.push({ 179 | source: line.item.replace("http://www.wikidata.org/entity/","wd:"), 180 | target: line.linkTo.replace("http://www.wikidata.org/entity/","wd:"), 181 | value: 0.5 182 | }); 183 | } 184 | }); 185 | nodes = nodes.filter((e, i) => nodes.findIndex(a => a.id === e.id) === i); // get only unique nodes. 186 | graph = {links:links,nodes:nodes}; 187 | // store the full graph for later use 188 | graphstore = Object.assign({}, graph); 189 | drawGraph(graph); 190 | } 191 | 192 | function csvToArray(str, delimiter = ",") { 193 | const headers = str.slice(0, str.indexOf("\n")).split(delimiter); 194 | const rows = str.slice(str.indexOf("\n") + 1).split("\n"); 195 | const arr = rows.map(function (row) { 196 | const values = row.split(delimiter); 197 | const el = headers.reduce(function (object, header, index) { 198 | object[header] = values[index]; 199 | return object; 200 | }, {}); 201 | return el; 202 | }); 203 | return arr; 204 | } 205 | 206 | 207 | let width = screen.availWidth, height = screen.availHeight; 208 | 209 | 210 | function colour(num){ 211 | if (num > 1) return 0xFF0000 212 | return 11454440 ; 213 | } 214 | 215 | /* 216 | let colour = (function() { 217 | let scale = d3.scaleOrdinal(d3.schemeCategory20); 218 | return (num) => parseInt(scale(num).slice(1), 16); 219 | })(); 220 | */ 221 | 222 | let simulation = d3.forceSimulation() 223 | .force('link', d3.forceLink().id((d) => d.id)) 224 | .force('charge', d3.forceManyBody().strength(d => d.group == 2 ? -500 : -5)) 225 | // .force('center', d3.forceCenter(width / 2, height / 2) ) 226 | .force("x", d3.forceX(width / 2).strength(0.5)) 227 | .force("y", d3.forceY(height / 2).strength(0.5)) 228 | .force("collide",d3.forceCollide().radius(4.5)) // d => d.radius is slow 229 | .alphaDecay(0.002) 230 | ; 231 | 232 | 233 | let stage = new PIXI.Container(); 234 | let renderer = PIXI.autoDetectRenderer( 235 | width, height, 236 | {antialias: !0, transparent: !0, resolution: 1} 237 | ); 238 | document.body.appendChild(renderer.view); 239 | 240 | function drawGraph(graph) { 241 | loadinginfotext += "Drawing graph... "; 242 | loadinginfo.style('display', 'block').text(loadinginfotext); 243 | console.log(graph); 244 | 245 | // TRANSFORM THE DATA INTO A D3 GRAPH 246 | simulation 247 | .nodes(graph.nodes) 248 | .on('tick', ticked) // this d3 ticker can be replaced by PIXI's "requestAnimationFrame" but the system is then too excited 249 | .force('link') 250 | .links(graph.links); 251 | 252 | // count incoming links to set node sizes 253 | graph.links.forEach(function(link){ 254 | if (!link.target["linkCount"]) link.target["linkCount"] = 0; 255 | link.target["linkCount"]++; 256 | }); 257 | 258 | 259 | // Render with PIXI ------ 260 | 261 | // let layerLinks = new PIXI.display.Layer(); // does not work 262 | // see more here: https://github.com/pixijs/layers/wiki 263 | 264 | 265 | // the LINKS are just one object that actually gets drawn in the ticks: 266 | let containerLinks = new PIXI.Container(); 267 | let links = new PIXI.Graphics(); 268 | containerLinks.addChild(links); 269 | 270 | // render NODES 271 | 272 | let containerParties = new PIXI.Container(); 273 | let containerIdeologies = new PIXI.Container(); 274 | graph.nodes.forEach((node) => { 275 | node.gfx = new PIXI.Graphics(); 276 | node.gfx.lineStyle(0.5, 0xFFFFFF); 277 | node.gfx.beginFill(colour(node.group)); 278 | node.radius = node.group < 2 ? 3 : 3 + Math.sqrt(node.linkCount); 279 | node.gfx.drawCircle(0, 0, node.radius ); 280 | node.gfx.interactive = true; 281 | node.gfx.hitArea = new PIXI.Circle(0, 0, node.radius); 282 | node.gfx.mouseover = function(ev) { 283 | // TODO for some reason, the attached event does not work whenn the graph is updated 284 | console.log(node); 285 | let nodex = node.x + 10; 286 | let nodey = node.y - 10; 287 | d3.select("#label") 288 | .attr("style", "left:"+nodex+"px;top:"+nodey+"px;") 289 | .select("a") 290 | .attr("href",node.id.replace("wd:","http://www.wikidata.org/entity/")) 291 | .attr("target","_blank") 292 | .text(node.label) 293 | } 294 | node.gfx.click = function(ev) { 295 | focus(node); 296 | } 297 | if (node.group==1) containerParties.addChild(node.gfx); 298 | if (node.group==2) containerIdeologies.addChild(node.gfx); 299 | // stage.addChild(node.gfx); 300 | 301 | if (node.group == 2) { 302 | node.lgfx = new PIXI.Text( 303 | node.label, { 304 | fontFamily : 'Maven Pro', 305 | fontSize: 9 + node.radius / 2, 306 | fill : 0xFF0000, 307 | align : 'center' 308 | } 309 | ); 310 | containerIdeologies.addChild(node.lgfx); 311 | } 312 | }); 313 | 314 | containerLinks.zIndex = 0; 315 | containerIdeologies.zIndex = 2; 316 | containerParties.zIndex = 1; 317 | stage.addChild(containerLinks); 318 | stage.addChild(containerParties); 319 | stage.addChild(containerIdeologies); 320 | stage.children.sort((itemA, itemB) => itemA.zIndex - itemB.zIndex); 321 | 322 | /* 323 | stage.children.sort(function(a,b) { 324 | if (a.fillColor == 11454440 ) return -1; 325 | return 1; 326 | }); 327 | */ 328 | 329 | d3.select(renderer.view) 330 | .call( 331 | d3.drag() 332 | .container(renderer.view) 333 | .subject(() => simulation.find(d3.event.x, d3.event.y)) 334 | .on('start', dragstarted) 335 | .on('drag', dragged) 336 | .on('end', dragended) 337 | ); 338 | 339 | // ticked() 340 | function ticked() { 341 | // requestAnimationFrame(ticked); //this d3 on.ticker can be replaced by PIXI's "requestAnimationFrame" but the system is then too excited. See above 342 | graph.nodes.forEach((node) => { 343 | let { x, y, gfx, lgfx, radius } = node; 344 | gfx.position = new PIXI.Point(x, y); 345 | if (node.group == 2) lgfx.position = new PIXI.Point(x + radius / 2, y + radius /2); 346 | }); 347 | links.clear(); 348 | links.alpha = 0.6; 349 | graph.links.forEach((link) => { 350 | let { source, target } = link; 351 | links.lineStyle(Math.sqrt(link.value), 0x999999,link.alpha); 352 | links.moveTo(source.x, source.y); 353 | links.lineTo(target.x, target.y); 354 | }); 355 | links.endFill(); 356 | renderer.render(stage); 357 | // when this point is reached, the notification about loading can be removed 358 | loadinginfotext = ""; 359 | loadinginfo.style('display', 'none').text(loadinginfotext); 360 | } 361 | 362 | simulation.alphaTarget(0.3).restart(); // give it an initial push 363 | } 364 | 365 | function dragstarted() { 366 | if (!d3.event.active) simulation.alphaTarget(0.3).restart(); 367 | d3.event.subject.fx = d3.event.subject.x; 368 | d3.event.subject.fy = d3.event.subject.y; 369 | } 370 | 371 | function dragged() { 372 | d3.event.subject.fx = d3.event.x; 373 | d3.event.subject.fy = d3.event.y; 374 | } 375 | 376 | function dragended() { 377 | if (!d3.event.active) simulation.alphaTarget(0); 378 | d3.event.subject.fx = null; 379 | d3.event.subject.fy = null; 380 | } 381 | 382 | // Graph highlight ------- 383 | 384 | let rootSelectedNode = {}; 385 | 386 | function focus(d) { 387 | console.log(d); 388 | if (rootSelectedNode == d) { 389 | unfocus(); 390 | } else { 391 | rootSelectedNode = d; 392 | markSelected(d); 393 | hideLabels(); 394 | revealLabels(); 395 | } 396 | updateColor(); 397 | } 398 | 399 | function unfocus() { 400 | graph.nodes.forEach(n => {n.marked = true}); 401 | graph.links.forEach(l => {l.marked = true}); 402 | hideLabels(); 403 | } 404 | 405 | function markSelected(d){ 406 | graph.nodes.forEach(n => {n.marked = false}) 407 | graph.links.forEach(l => {l.marked = false}) 408 | d.marked = true; 409 | let linked = []; 410 | graph.links.filter(l => 411 | l.source == d | l.target == d 412 | ).forEach(l => { 413 | l.marked = true; 414 | linked.push(l.source.id); 415 | linked.push(l.target.id) 416 | }); 417 | graph.nodes.forEach(n => n.marked = linked.includes(n.id) ? true : false) 418 | } 419 | 420 | function updateColor() { 421 | graph.nodes.filter(n => !n.marked).forEach(n => { 422 | n.gfx.alpha = 0.2; 423 | if (n.group == 2) n.lgfx.alpha=0.2 424 | }); 425 | graph.links.filter(l => !l.marked).forEach(l => l.alpha = 0.1 ); 426 | graph.nodes.filter(n => n.marked).forEach(n => { 427 | n.gfx.alpha = 1; 428 | if (n.group == 2) n.lgfx.alpha =1 429 | }); 430 | graph.links.filter(l => l.marked).forEach(l => l.alpha = 1); 431 | } 432 | 433 | function revealLabels(){ 434 | graph.nodes.filter(n => n.marked & n.group == 1 ).forEach(n => { 435 | n.lgfx = new PIXI.Text( 436 | n.label, { 437 | fontFamily : 'Maven Pro', 438 | fontSize: 9 , 439 | fill : n.gfx.fill, 440 | align : 'center' 441 | } 442 | ); 443 | stage.addChild(n.lgfx); 444 | }); 445 | } 446 | 447 | function hideLabels(){ 448 | graph.nodes.filter(n => n.marked & n.group == 1 ).forEach(n => { 449 | stage.removeChild(n.lgfx); 450 | n.lgfx = null; 451 | }); 452 | } 453 | 454 | 455 | // Graph updates ------------ 456 | 457 | 458 | function updateGraph(){ 459 | simulation.stop(); 460 | graph = graphstore = null; 461 | loadinginfotext += "Updating Graph...\n"; 462 | loadinginfo.style('display', 'block').text(loadinginfotext); 463 | let checked = []; 464 | let boxes = d3.selectAll("input[type='checkbox']:checked") 465 | boxes._groups[0].forEach(b=>{ 466 | checked.push(b.value) 467 | }); 468 | console.log(checked); 469 | stage.removeChildren(); 470 | // d3.select("#label").text(""); 471 | makeCountriesGraph(checked); 472 | } 473 | 474 | // TODO add element without destroying everything 475 | function restoreGraph(){ 476 | // add all elements to graph removed by previous filter 477 | graphstore.nodes.forEach(sn => { 478 | if (graph.nodes.filter(n=> n.id == sn.id).length==0) graph.nodes.push(Object.assign({}, sn)); 479 | }) 480 | // TODO : something's wrong with attaching those links 481 | graphstore.links.forEach(sl => { 482 | if (graph.links.filter(l=> l.id == sl.id).length==0) graph.links.push(Object.assign({}, sl)); 483 | }) 484 | // relink nodes correcly 485 | graph.links.forEach(l => { 486 | l.source = graph.nodes.filter(n=> n.id == l.source.id)[0]; 487 | l.target = graph.nodes.filter(n=> n.id == l.target.id)[0]; 488 | }); 489 | } 490 | 491 | -------------------------------------------------------------------------------- /old/index02.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | IDEOGRAPH - explore ideologies of political parties with SPAQRL requests to WikiData, D3 and PixiJS. 4 | 5 | Copyright (C) 2021 André Ourednik 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | */ 20 | 21 | 22 | let endpoint = "https://query.wikidata.org/sparql?query="; 23 | let loadinginfo = d3.select("#loadinginfo"); 24 | let loadingCountries = d3.select("#loadingCountries"); 25 | let countriesLoaded = d3.select("#countriesLoaded"); 26 | let loadingGraph = d3.select("#loadingGraph"); 27 | let constructingGraph = d3.select("#constructingGraph"); 28 | let loadinginfotext = ""; 29 | let graph, graphstore ; // neccessary globals 30 | 31 | // Make a list of countries 32 | let sparql = ` 33 | SELECT DISTINCT ?country ?countryLabel 34 | WHERE { 35 | ?item wdt:P1142 ?linkTo . 36 | ?linkTo wdt:P31 wd:Q12909644 . # keep only targets that are political ideologies 37 | VALUES ?type { wd:Q7278 wd:Q24649 } # filter by these types of political actors 38 | ?item wdt:P31 ?type . 39 | ?item wdt:P17 ?country . 40 | MINUS { ?item wdt:P576 ?abolitionDate } # exclude abolished parties 41 | MINUS { ?country wdt:P576 ?countryAbolitionDate }. # exclude abolished countries 42 | SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en" . } 43 | } 44 | `; 45 | let req = endpoint + encodeURIComponent(sparql); 46 | getCountryList(req); 47 | 48 | let initialCountries = [ // Europe 49 | "wd:Q1246", // Kosovo 50 | "wd:Q142", 51 | "wd:Q145", // UK 52 | "wd:Q183", 53 | "wd:Q189", // Iceland 54 | "wd:Q191", // Estonia 55 | "wd:Q20", // Norway 56 | "wd:Q25", // Wales 57 | "wd:Q211", // Latvia 58 | "wd:Q213", 59 | "wd:Q214", // Sloveni 60 | "wd:Q215", // Slovakia 61 | "wd:Q217", // Moldova 62 | "wd:Q218", // Romania 63 | "wd:Q219", 64 | "wd:Q222", // Albania 65 | "wd:Q223", // Greenland 66 | "wd:Q224", 67 | "wd:Q225", 68 | "wd:Q228", // Andora 69 | "wd:Q229", 70 | "wd:Q233", // Malta 71 | "wd:Q235", // Monaco 72 | "wd:Q236", // Montenegro 73 | "wd:Q238", // San Marino 74 | "wd:Q27", // Ireland 75 | "wd:Q28", // Hungary 76 | "wd:Q29", // Spain 77 | "wd:Q31", 78 | "wd:Q32", // Luxembourg 79 | "wd:Q33", // Finlannd 80 | "wd:Q34", // Sweden 81 | "wd:Q347", // Lichtenstein 82 | "wd:Q35", 83 | "wd:Q36", // Poland 84 | "wd:Q37", // Lituania 85 | "wd:Q38", 86 | "wd:Q39", 87 | "wd:Q40", 88 | "wd:Q403", // Serbia 89 | "wd:Q41" ,// Greece 90 | "wd:Q45", // Portugal 91 | "wd:Q4628", // Faroe Islands 92 | "wd:Q55", // Netherlands 93 | "wd:Q9676" // Isle of Man 94 | ]; 95 | 96 | makeCountriesGraph(initialCountries); 97 | 98 | function makeCountriesGraph(countries) { 99 | // VALUES ?ideatype { wd:Q12909644 wd:Q179805 wd:Q7257 wd:Q5333510} # ideologie ou philosophie politique ou économique 100 | // ?linkTo wdt:P31 ?ideatype . 101 | sparql = ` 102 | SELECT DISTINCT ?item ?itemLabel ?country ?countryLabel ?linkTo ?linkToLabel 103 | WHERE { 104 | ?item wdt:P1142 | wdt:P1142/wdt:P279* ?linkTo . # alternative path makes link to ideology superclass 105 | ?linkTo wdt:P31 wd:Q12909644 . 106 | VALUES ?type { wd:Q7278 wd:Q24649 } # filter by these types of political actors 107 | ?item wdt:P31 ?type . 108 | VALUES ?country { ${countries.join(" ")} } #filter by selected countries 109 | ?item wdt:P17 ?country . 110 | MINUS { ?item wdt:P576 ?abolitionDate } # exclude abolished parties 111 | MINUS { ?country wdt:P576 ?countryAbolitionDate }. # exclude abolished countries 112 | SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en" . } 113 | } 114 | `;//.replaceAll("\n"," ").replaceAll(" "," "); 115 | let req = endpoint + encodeURIComponent(sparql); 116 | getGraphData(req); 117 | } 118 | 119 | 120 | // Constructs a list of countnries to choose from. 121 | // On first run, launch graph construction. 122 | async function getCountryList(req) { 123 | loadinginfo.style('display', 'block'); 124 | loadingCountries.style('display', 'block'); 125 | let response = await fetch(req, { 126 | headers: { 127 | "Accept": "text/csv" 128 | } 129 | }); 130 | let text = await response.text(); 131 | // let countries = csvToArray(text); 132 | let countries = Papa.parse(text,{header:true}); 133 | countries = countries.data; 134 | // console.log(countries); 135 | countries.sort((a,b) => (a["countryLabel"] > b["countryLabel"]) ? 1 : ((b["countryLabel"] > a["countryLabel"]) ? -1 : 0)) 136 | let countriesdiv = d3.select("#countryselector"); 137 | countries.forEach(c=>{ 138 | let newdiv = countriesdiv.append("div") 139 | let cval = c.country.replace("http://www.wikidata.org/entity/","wd:"); 140 | let cid = cval.replace("wd:","c") 141 | newdiv 142 | .append("input") 143 | .attr("type","checkbox") 144 | .attr("name", c["countryLabel"]) 145 | .attr("id",cid) 146 | .attr("value", cval) 147 | .attr("onclick","updateGraph()") 148 | ; 149 | newdiv 150 | .append("label") 151 | .append("a") 152 | .attr("href",c.country) 153 | .attr("target","_blank") 154 | .text(c["countryLabel"]) 155 | ; 156 | }); 157 | initialCountries.forEach(cval => document.getElementById(cval.replace("wd:","c")).checked = true); 158 | loadingCountries.style('display', 'none'); 159 | countriesLoaded.style('display', 'block'); 160 | } 161 | 162 | 163 | async function getGraphData(req) { 164 | loadinginfo.style('display', 'block'); 165 | loadingGraph.style('display', 'block'); 166 | let response = await fetch(req, { 167 | headers: { 168 | "Accept": "text/csv" 169 | } 170 | }); 171 | let text = await response.text(); 172 | // let data = csvToArray(text); 173 | let data = Papa.parse(text,{header:true}); 174 | data = data.data; 175 | console.log(data); 176 | let nodes = []; 177 | let links = []; 178 | data.forEach((line)=>{ 179 | if (typeof line.item !== "undefined" & typeof line.linkTo !== "undefined") { 180 | nodes.push({ 181 | id: line.item.replace("http://www.wikidata.org/entity/","wd:"), 182 | label : line.itemLabel, 183 | group : 1 184 | }) ; 185 | nodes.push({ 186 | id: line.linkTo.replace("http://www.wikidata.org/entity/","wd:"), 187 | label : line["linkToLabel"], // might need to remove \r from the csv in csvToArray, 188 | group : 2 189 | }) ; 190 | links.push({ 191 | source: line.item.replace("http://www.wikidata.org/entity/","wd:"), 192 | target: line.linkTo.replace("http://www.wikidata.org/entity/","wd:"), 193 | value: 0.5 194 | }); 195 | } 196 | }); 197 | nodes = nodes.filter((e, i) => nodes.findIndex(a => a.id === e.id) === i); // get only unique nodes. 198 | graph = {links:links,nodes:nodes}; 199 | // store the full graph for later use 200 | graphstore = Object.assign({}, graph); 201 | drawGraph(graph); 202 | } 203 | 204 | /* 205 | function csvToArray(str, delimiter = ",") { 206 | const headers = str.slice(0, str.indexOf("\n")).split(delimiter); 207 | const rows = str.slice(str.indexOf("\n") + 1).split("\n"); 208 | const arr = rows.map(function (row) { 209 | const values = row.split(delimiter); 210 | const el = headers.reduce(function (object, header, index) { 211 | object[header] = values[index]; 212 | return object; 213 | }, {}); 214 | return el; 215 | }); 216 | return arr; 217 | } 218 | */ 219 | 220 | 221 | let width = screen.availWidth, height = screen.availHeight; 222 | 223 | 224 | function colour(num){ 225 | if (num > 1) return 0xFF0000 226 | return 11454440 ; 227 | } 228 | 229 | /* 230 | let colour = (function() { 231 | let scale = d3.scaleOrdinal(d3.schemeCategory20); 232 | return (num) => parseInt(scale(num).slice(1), 16); 233 | })(); 234 | */ 235 | 236 | let simulation = d3.forceSimulation() 237 | .force('link', d3.forceLink().id((d) => d.id)) 238 | .force('charge', d3.forceManyBody().strength(d => d.group == 2 ? -500 : -5)) 239 | // .force('center', d3.forceCenter(width / 2, height / 2) ) 240 | .force("x", d3.forceX(width / 2).strength(0.5)) 241 | .force("y", d3.forceY(height / 2).strength(0.5)) 242 | .force("collide",d3.forceCollide().radius(4.5)) // d => d.radius is slow 243 | .alphaDecay(0.005) 244 | ; 245 | 246 | 247 | let stage = new PIXI.Container(); 248 | let renderer = PIXI.autoDetectRenderer( 249 | width, height, 250 | {antialias: !0, transparent: !0, resolution: 1} 251 | ); 252 | document.body.appendChild(renderer.view); 253 | 254 | function drawGraph(graph) { 255 | constructingGraph.style('display', 'block'); 256 | console.log(graph); 257 | 258 | // TRANSFORM THE DATA INTO A D3 GRAPH 259 | simulation 260 | .nodes(graph.nodes) 261 | .on('tick', ticked) // this d3 ticker can be replaced by PIXI's "requestAnimationFrame" but the system is then too excited 262 | .force('link') 263 | .links(graph.links); 264 | 265 | // count incoming links to set node sizes 266 | graph.links.forEach(function(link){ 267 | if (!link.target["linkCount"]) link.target["linkCount"] = 0; 268 | link.target["linkCount"]++; 269 | }); 270 | 271 | 272 | // Render with PIXI ------ 273 | 274 | // let layerLinks = new PIXI.display.Layer(); // does not work 275 | // see more here: https://github.com/pixijs/layers/wiki 276 | 277 | 278 | // the LINKS are just one object that actually gets drawn in the ticks: 279 | let containerLinks = new PIXI.Container(); 280 | let links = new PIXI.Graphics(); 281 | containerLinks.addChild(links); 282 | 283 | // render NODES 284 | 285 | let containerParties = new PIXI.Container(); 286 | let containerIdeologies = new PIXI.Container(); 287 | graph.nodes.forEach((node) => { 288 | node.gfx = new PIXI.Graphics(); 289 | node.gfx.lineStyle(0.5, 0xFFFFFF); 290 | node.gfx.beginFill(colour(node.group)); 291 | node.radius = node.group < 2 ? 3 : 3 + Math.sqrt(node.linkCount); 292 | node.gfx.drawCircle(0, 0, node.radius ); 293 | node.gfx.interactive = true; 294 | node.gfx.hitArea = new PIXI.Circle(0, 0, node.radius); 295 | node.gfx.mouseover = function(ev) { 296 | // TODO for some reason, the attached event does not work whenn the graph is updated 297 | console.log(node); 298 | let nodex = node.x + 10; 299 | let nodey = node.y - 10; 300 | d3.select("#label") 301 | .attr("style", "left:"+nodex+"px;top:"+nodey+"px;") 302 | .select("a") 303 | .attr("href",node.id.replace("wd:","http://www.wikidata.org/entity/")) 304 | .attr("target","_blank") 305 | .text(node.label) 306 | } 307 | node.gfx.click = function(ev) { 308 | focus(node); 309 | } 310 | if (node.group==1) containerParties.addChild(node.gfx); 311 | if (node.group==2) containerIdeologies.addChild(node.gfx); 312 | // stage.addChild(node.gfx); 313 | 314 | if (node.group == 2) { 315 | node.lgfx = new PIXI.Text( 316 | node.label, { 317 | fontFamily : 'Maven Pro', 318 | fontSize: 9 + node.radius / 2, 319 | fill : 0xFF0000, 320 | align : 'center' 321 | } 322 | ); 323 | containerIdeologies.addChild(node.lgfx); 324 | } 325 | }); 326 | 327 | containerLinks.zIndex = 0; 328 | containerIdeologies.zIndex = 2; 329 | containerParties.zIndex = 1; 330 | stage.addChild(containerLinks); 331 | stage.addChild(containerParties); 332 | stage.addChild(containerIdeologies); 333 | stage.children.sort((itemA, itemB) => itemA.zIndex - itemB.zIndex); 334 | 335 | /* 336 | stage.children.sort(function(a,b) { 337 | if (a.fillColor == 11454440 ) return -1; 338 | return 1; 339 | }); 340 | */ 341 | 342 | d3.select(renderer.view) 343 | .call( 344 | d3.drag() 345 | .container(renderer.view) 346 | .subject(() => simulation.find(d3.event.x, d3.event.y)) 347 | .on('start', dragstarted) 348 | .on('drag', dragged) 349 | .on('end', dragended) 350 | ); 351 | 352 | // ticked() 353 | function ticked() { 354 | // requestAnimationFrame(ticked); //this d3 on.ticker can be replaced by PIXI's "requestAnimationFrame" but the system is then too excited. See above 355 | graph.nodes.forEach((node) => { 356 | let { x, y, gfx, lgfx, radius } = node; 357 | gfx.position = new PIXI.Point(x, y); 358 | if (node.group == 2) lgfx.position = new PIXI.Point(x + radius / 2, y + radius /2); 359 | }); 360 | links.clear(); 361 | links.alpha = 0.6; 362 | graph.links.forEach((link) => { 363 | let { source, target } = link; 364 | links.lineStyle(Math.sqrt(link.value), 0x999999,link.alpha); 365 | links.moveTo(source.x, source.y); 366 | links.lineTo(target.x, target.y); 367 | }); 368 | links.endFill(); 369 | renderer.render(stage); 370 | // when this point is reached, the notification about loading can be removed 371 | loadinginfotext = ""; 372 | loadinginfo.style('display', 'none'); 373 | constructingGraph.style('display', 'none'); 374 | } 375 | 376 | simulation.alphaTarget(0.3).restart(); // give it an initial push 377 | } 378 | 379 | function dragstarted() { 380 | if (!d3.event.active) simulation.alphaTarget(0.3).restart(); 381 | d3.event.subject.fx = d3.event.subject.x; 382 | d3.event.subject.fy = d3.event.subject.y; 383 | } 384 | 385 | function dragged() { 386 | d3.event.subject.fx = d3.event.x; 387 | d3.event.subject.fy = d3.event.y; 388 | } 389 | 390 | function dragended() { 391 | if (!d3.event.active) simulation.alphaTarget(0); 392 | d3.event.subject.fx = null; 393 | d3.event.subject.fy = null; 394 | } 395 | 396 | // Graph highlight ------- 397 | 398 | let rootSelectedNode = {}; 399 | 400 | function focus(d) { 401 | console.log(d); 402 | if (rootSelectedNode == d) { 403 | unfocus(); 404 | } else { 405 | rootSelectedNode = d; 406 | markSelected(d); 407 | hideLabels(); 408 | revealLabels(); 409 | } 410 | updateColor(); 411 | } 412 | 413 | function unfocus() { 414 | graph.nodes.forEach(n => {n.marked = true}); 415 | graph.links.forEach(l => {l.marked = true}); 416 | hideLabels(); 417 | } 418 | 419 | function markSelected(d){ 420 | graph.nodes.forEach(n => {n.marked = false}) 421 | graph.links.forEach(l => {l.marked = false}) 422 | d.marked = true; 423 | let linked = []; 424 | graph.links.filter(l => 425 | l.source == d | l.target == d 426 | ).forEach(l => { 427 | l.marked = true; 428 | linked.push(l.source.id); 429 | linked.push(l.target.id) 430 | }); 431 | graph.nodes.forEach(n => n.marked = linked.includes(n.id) ? true : false) 432 | } 433 | 434 | function updateColor() { 435 | graph.nodes.filter(n => !n.marked).forEach(n => { 436 | n.gfx.alpha = 0.2; 437 | if (n.group == 2) n.lgfx.alpha=0.2 438 | }); 439 | graph.links.filter(l => !l.marked).forEach(l => l.alpha = 0.1 ); 440 | graph.nodes.filter(n => n.marked).forEach(n => { 441 | n.gfx.alpha = 1; 442 | if (n.group == 2) n.lgfx.alpha =1 443 | }); 444 | graph.links.filter(l => l.marked).forEach(l => l.alpha = 1); 445 | } 446 | 447 | function revealLabels(){ 448 | graph.nodes.filter(n => n.marked & n.group == 1 ).forEach(n => { 449 | n.lgfx = new PIXI.Text( 450 | n.label, { 451 | fontFamily : 'Maven Pro', 452 | fontSize: 9 , 453 | fill : n.gfx.fill, 454 | align : 'center' 455 | } 456 | ); 457 | stage.addChild(n.lgfx); 458 | }); 459 | } 460 | 461 | function hideLabels(){ 462 | graph.nodes.filter(n => n.marked & n.group == 1 ).forEach(n => { 463 | stage.removeChild(n.lgfx); 464 | n.lgfx = null; 465 | }); 466 | } 467 | 468 | 469 | // Graph updates ------------ 470 | 471 | 472 | function updateGraph(){ 473 | simulation.stop(); 474 | graph = graphstore = null; 475 | loadinginfotext += "Updating Graph...\n"; 476 | loadinginfo.style('display', 'block').text(loadinginfotext); 477 | let checked = []; 478 | let boxes = d3.selectAll("input[type='checkbox']:checked") 479 | boxes._groups[0].forEach(b=>{ 480 | checked.push(b.value) 481 | }); 482 | console.log(checked); 483 | stage.removeChildren(); 484 | // d3.select("#label").text(""); 485 | makeCountriesGraph(checked); 486 | } 487 | 488 | // TODO add element without destroying everything 489 | function restoreGraph(){ 490 | // add all elements to graph removed by previous filter 491 | graphstore.nodes.forEach(sn => { 492 | if (graph.nodes.filter(n=> n.id == sn.id).length==0) graph.nodes.push(Object.assign({}, sn)); 493 | }) 494 | // TODO : something's wrong with attaching those links 495 | graphstore.links.forEach(sl => { 496 | if (graph.links.filter(l=> l.id == sl.id).length==0) graph.links.push(Object.assign({}, sl)); 497 | }) 498 | // relink nodes correcly 499 | graph.links.forEach(l => { 500 | l.source = graph.nodes.filter(n=> n.id == l.source.id)[0]; 501 | l.target = graph.nodes.filter(n=> n.id == l.target.id)[0]; 502 | }); 503 | } 504 | 505 | -------------------------------------------------------------------------------- /old/index03.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | IDEOGRAPH - explore ideologies of political parties with SPAQRL requests to WikiData, D3 and PixiJS. 4 | 5 | Copyright (C) 2021 André Ourednik 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | */ 21 | 22 | let endpoint = "https://query.wikidata.org/sparql?query="; 23 | // messages: 24 | let loadinginfo = d3.select("#loadinginfo"); 25 | let loadingCountries = d3.select("#loadingCountries"); 26 | let countriesLoaded = d3.select("#countriesLoaded"); 27 | let loadingGraph = d3.select("#loadingGraph"); 28 | let constructingGraph = d3.select("#constructingGraph"); 29 | let updatingGraph = d3.select("#updatingGraph"); 30 | let loadinginfotext = ""; 31 | // neccessary globals 32 | let graph, graphstore ; 33 | 34 | // Make a list of countries 35 | let sparql = ` 36 | SELECT DISTINCT ?country ?countryLabel 37 | WHERE { 38 | ?item wdt:P1142 ?linkTo . 39 | ?linkTo wdt:P31 wd:Q12909644 . # keep only targets that are political ideologies 40 | VALUES ?type { wd:Q7278 wd:Q24649 } # filter by these types of political actors 41 | ?item wdt:P31 ?type . 42 | ?item wdt:P17 ?country . 43 | MINUS { ?item wdt:P576 ?abolitionDate } # exclude abolished parties 44 | MINUS { ?country wdt:P576 ?countryAbolitionDate }. # exclude abolished countries 45 | SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en" . } 46 | } 47 | `; 48 | let req = endpoint + encodeURIComponent(sparql); 49 | getCountryList(req); 50 | 51 | let Europe = [ // Europe 52 | "wd:Q1246", // Kosovo 53 | "wd:Q142", 54 | "wd:Q145", // UK 55 | "wd:Q183", 56 | "wd:Q189", // Iceland 57 | "wd:Q191", // Estonia 58 | "wd:Q20", // Norway 59 | "wd:Q25", // Wales 60 | "wd:Q211", // Latvia 61 | "wd:Q212", // Ukraine 62 | "wd:Q213", 63 | "wd:Q214", // Sloveni 64 | "wd:Q215", // Slovakia 65 | "wd:Q217", // Moldova 66 | "wd:Q218", // Romania 67 | "wd:Q219", 68 | "wd:Q221", // Northern Macedonia 69 | "wd:Q222", // Albania 70 | "wd:Q223", // Greenland 71 | "wd:Q224", 72 | "wd:Q225", 73 | "wd:Q228", // Andora 74 | "wd:Q229", 75 | "wd:Q233", // Malta 76 | "wd:Q235", // Monaco 77 | "wd:Q236", // Montenegro 78 | "wd:Q238", // San Marino 79 | "wd:Q27", // Ireland 80 | "wd:Q28", // Hungary 81 | "wd:Q29", // Spain 82 | "wd:Q31", 83 | "wd:Q32", // Luxembourg 84 | "wd:Q33", // Finlannd 85 | "wd:Q34", // Sweden 86 | "wd:Q347", // Lichtenstein 87 | "wd:Q35", 88 | "wd:Q36", // Poland 89 | "wd:Q37", // Lituania 90 | "wd:Q38", 91 | "wd:Q39", 92 | "wd:Q40", 93 | "wd:Q403", // Serbia 94 | "wd:Q41" ,// Greece 95 | "wd:Q45", // Portugal 96 | "wd:Q4628", // Faroe Islands 97 | "wd:Q55", // Netherlands 98 | "wd:Q9676" // Isle of Man 99 | ]; 100 | 101 | let subsaharanAfrica = ["wd:Q916","wd:Q962","wd:Q963","wd:Q965","wd:Q967","wd:Q1009","wd:Q929","wd:Q657","wd:Q974","wd:Q977","wd:Q983","wd:Q986","wd:Q1050","wd:Q115","wd:Q1000","wd:Q117","wd:Q1006","wd:Q1007","wd:Q1008","wd:Q114","wd:Q1013","wd:Q1014","wd:Q1019","wd:Q1020","wd:Q912","wd:Q1025","wd:Q1029","wd:Q1030","wd:Q1032","wd:Q1033","wd:Q971","wd:Q1041","wd:Q1045","wd:Q34754","wd:Q258","wd:Q1049","wd:Q924","wd:Q1005","wd:Q945","wd:Q1036","wd:Q953","wd:Q954"] 102 | let Asia = ["wd:Q851","wd:Q40362", "wd:Q244165", "wd:Q1027", "wd:Q826", "wd:Q801", "wd:Q574","wd:Q889", "wd:Q399", "wd:Q619829", "wd:Q227", "wd:Q398", "wd:Q902", "wd:Q917", "wd:Q424", "wd:Q326343", "wd:Q230", "wd:Q8646", "wd:Q668", "wd:Q252", "wd:Q17", "wd:Q810", "wd:Q232", "wd:Q41470", "wd:Q205047", "wd:Q817", "wd:Q813", "wd:Q819", "wd:Q822", "wd:Q14773", "wd:Q833", "wd:Q711", "wd:Q836", "wd:Q837", "wd:Q423", "wd:Q843", "wd:Q148", "wd:Q928", "wd:Q334", "wd:Q884", "wd:Q23427", "wd:Q854", "wd:Q219060", "wd:Q858", "wd:Q865", "wd:Q863", "wd:Q869", "wd:Q43", "wd:Q23681", "wd:Q874", "wd:Q1498", "wd:Q265", "wd:Q881"] 103 | 104 | // INITIALISATION 105 | document.getElementById("upgradeGraphButton").disabled = true; 106 | let reqGraph = makeGraphReq(Europe); 107 | let reqGraphExtra = makeGraphExtraReq(Europe); 108 | getGraphData(reqGraph,reqGraphExtra); 109 | 110 | function makeGraphReq(countries) { 111 | // VALUES ?ideatype { wd:Q12909644 wd:Q179805 wd:Q7257 wd:Q5333510 wd:Q780687} # ideologie ou philosophie politique ou économique 112 | // ?linkTo wdt:P31 ?ideatype . 113 | sparql = ` 114 | SELECT DISTINCT ?item ?itemLabel ?country ?countryLabel ?linkTo ?linkToLabel 115 | WHERE { 116 | ?item wdt:P1142 ?linkTo . # alternative path makes link to ideology superclass 117 | ?linkTo wdt:P31 wd:Q12909644 . # take "political ideology" only 118 | VALUES ?type { wd:Q7278 wd:Q24649 } # filter by these types of political actors 119 | ?item wdt:P31 ?type . 120 | VALUES ?country { ${countries.join(" ")} } #filter by selected countries 121 | ?item wdt:P17 ?country . 122 | MINUS { ?item wdt:P576 ?abolitionDate } # exclude abolished parties 123 | SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en,fr,ca,ko,zh" . } 124 | } 125 | `;//.replaceAll("\n"," ").replaceAll(" "," "); 126 | let req = endpoint + encodeURIComponent(sparql); 127 | return req ; 128 | } 129 | 130 | function makeGraphExtraReq(countries) { 131 | sparql = ` 132 | SELECT DISTINCT ?linkTo ?linkToLabel ?superLinkTo ?superLinkToLabel 133 | WHERE { 134 | ?item wdt:P1142 ?linkTo . # alternative path makes link to ideology superclass 135 | ?linkTo wdt:P279+ ?superLinkTo . 136 | ?superLinkTo wdt:P31|wdt:P279 wd:Q12909644 . # take political ideology only 137 | VALUES ?type { wd:Q7278 wd:Q24649 } # filter by these types of political actors 138 | ?item wdt:P31|wdt:P279 ?type . 139 | ?item wdt:P17 ?country . 140 | MINUS { ?item wdt:P576 ?abolitionDate } # exclude abolished parties 141 | SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en,fr,es,ca,ko,zh" . } 142 | FILTER (?country IN (${countries.join(", ")}) ) # here, this is faster than using VALUES. Why? 143 | } 144 | `; 145 | let req = endpoint + encodeURIComponent(sparql); 146 | return req ; 147 | } 148 | 149 | async function fetchWikiData(req) { 150 | let response = await fetch(req, {headers: { "Accept": "text/csv"}}); 151 | let text = await response.text(); 152 | let data = Papa.parse(text,{header:true}); 153 | data = data.data; 154 | return data ; 155 | } 156 | 157 | // Constructs a list of countnries to choose from. 158 | // On first run, launch graph construction. 159 | async function getCountryList(req) { 160 | loadinginfo.style('display', 'block'); 161 | loadingCountries.style('display', 'block'); 162 | let countries = await fetchWikiData(req); 163 | // console.log(countries); 164 | countries.sort((a,b) => (a["countryLabel"] > b["countryLabel"]) ? 1 : ((b["countryLabel"] > a["countryLabel"]) ? -1 : 0)) 165 | let countriesdiv = d3.select("#countryselector"); 166 | countries.forEach(c=>{ 167 | let newdiv = countriesdiv.append("div") 168 | let cval = c.country.replace("http://www.wikidata.org/entity/","wd:"); 169 | let cid = cval.replace("wd:","c") 170 | newdiv 171 | .append("input") 172 | .attr("type","checkbox") 173 | .attr("name", c["countryLabel"]) 174 | .attr("id",cid) 175 | .attr("value", cval) 176 | //.attr("onclick","updateGraph()") 177 | ; 178 | newdiv 179 | .append("label") 180 | .append("a") 181 | .attr("href",c.country) 182 | .attr("target","_blank") 183 | .text(c["countryLabel"]) 184 | ; 185 | }); 186 | Europe.forEach(cval => document.getElementById(cval.replace("wd:","c")).checked = true); 187 | loadingCountries.style('display', 'none'); 188 | countriesLoaded.style('display', 'block'); 189 | } 190 | 191 | let dataExtra; 192 | let parties = [] 193 | async function getGraphData(req, reqExtra) { 194 | loadinginfo.style('display', 'block'); 195 | loadingGraph.style('display', 'block'); 196 | let data = await fetchWikiData(req); 197 | loadingGraph.text("Fetching extra graph links from WikiData..."); 198 | // console.log("Fetching Extra Links"); 199 | dataExtra = await fetchWikiData(reqExtra); 200 | // console.log(dataExtra); 201 | // let parties = []; // for later filtering out ideology nodes with no incoming parties 202 | let nodes = []; 203 | let links = []; 204 | data.forEach((line)=>{ 205 | if (typeof line.item !== "undefined" & typeof line.linkTo !== "undefined") { 206 | parties.push(line.item.replace("http://www.wikidata.org/entity/","wd:")); 207 | nodes.push({ 208 | id: line.item.replace("http://www.wikidata.org/entity/","wd:"), 209 | label : line.itemLabel + " (" + line.countryLabel + ")" , 210 | group : 1 211 | }) ; 212 | nodes.push({ 213 | id: line.linkTo.replace("http://www.wikidata.org/entity/","wd:"), 214 | label : line["linkToLabel"], 215 | group : 2 216 | }) ; 217 | links.push({ 218 | source: line.item.replace("http://www.wikidata.org/entity/","wd:"), 219 | target: line.linkTo.replace("http://www.wikidata.org/entity/","wd:"), 220 | value: 0.5 221 | }); 222 | } 223 | }); 224 | dataExtra.forEach((line)=>{ 225 | if (typeof line.linkTo !== "undefined" & typeof line.superLinkTo !== "undefined") { 226 | nodes.push({ 227 | id: line.linkTo.replace("http://www.wikidata.org/entity/","wd:"), 228 | label : line.linkToLabel, 229 | group : 2 230 | }) ; 231 | nodes.push({ 232 | id: line.superLinkTo.replace("http://www.wikidata.org/entity/","wd:"), 233 | label : line.superLinkToLabel, // might need to remove \r from the csv in csvToArray, 234 | group : 2 235 | }) ; 236 | links.push({ 237 | source: line.linkTo.replace("http://www.wikidata.org/entity/","wd:"), 238 | target: line.superLinkTo.replace("http://www.wikidata.org/entity/","wd:"), 239 | value: 0.8 240 | }); 241 | } 242 | }); 243 | nodes = nodes.filter((e, i) => nodes.findIndex(a => a.id === e.id) === i); // get only unique nodes. 244 | // graph.links.filter(l => parties.includes(l.source.id)) 245 | // we'll filter the links later with isNaN(radisu) 246 | graph = {links:links,nodes:nodes}; 247 | // store the full graph for later use 248 | graphstore = Object.assign({}, graph); 249 | drawGraph(graph); 250 | } 251 | 252 | 253 | let width = screen.availWidth, height = screen.availHeight; 254 | function colour(num){ 255 | if (num > 1) return 0xFF0000 256 | return 11454440 ; 257 | } 258 | 259 | /* 260 | let colour = (function() { 261 | let scale = d3.scaleOrdinal(d3.schemeCategory20); 262 | return (num) => parseInt(scale(num).slice(1), 16); 263 | })(); 264 | */ 265 | 266 | let simulation = d3.forceSimulation() 267 | .force('link', d3.forceLink().id((d) => d.id)) 268 | .force('charge', d3.forceManyBody().strength(d => d.group == 2 ? -500 : -5)) 269 | // .force('center', d3.forceCenter(width / 2, height / 2) ) 270 | .force("x", d3.forceX(width / 2).strength(0.5)) 271 | .force("y", d3.forceY(height / 2).strength(0.5)) 272 | .force("collide",d3.forceCollide().radius(4.5)) // d => d.radius is slow 273 | .alphaDecay(0.005) 274 | ; 275 | 276 | 277 | let stage = new PIXI.Container(); 278 | let renderer = PIXI.autoDetectRenderer( 279 | width, height, 280 | {antialias: !0, transparent: !0, resolution: 1} 281 | ); 282 | document.body.appendChild(renderer.view); 283 | 284 | function drawGraph(graph) { 285 | constructingGraph.style('display', 'block'); 286 | console.log(graph); 287 | 288 | // TRANSFORM THE DATA INTO A D3 GRAPH 289 | simulation 290 | .nodes(graph.nodes) 291 | .on('tick', ticked) // this d3 ticker can be replaced by PIXI's "requestAnimationFrame" but the system is then too excited 292 | .force('link') 293 | .links(graph.links); 294 | 295 | // count incoming links to set node sizes, and remove nodes with no radius, stemming from super-ideologies 296 | graph.links.forEach(function(link){ 297 | if (!link.target["linkCount"]) link.target["linkCount"] = 0; 298 | link.target["linkCount"]++; 299 | }); 300 | graph.nodes.forEach((node) => { 301 | node.radius = node.group < 2 ? 3 : 3 + Math.sqrt(node.linkCount); 302 | }); 303 | graph.links = graph.links.filter(l => ! isNaN(l.source.radius)); 304 | // remove freely floating nodes 305 | graph.nodes = graph.nodes.filter(n => graph.links.filter(l => 306 | l.source == n | l.target == n 307 | ).length > 0 ); 308 | 309 | // Render with PIXI ------ 310 | 311 | // let layerLinks = new PIXI.display.Layer(); // does not work 312 | // see more here: https://github.com/pixijs/layers/wiki 313 | 314 | 315 | // the LINKS are just one object that actually gets drawn in the ticks: 316 | let containerLinks = new PIXI.Container(); 317 | let links = new PIXI.Graphics(); 318 | containerLinks.addChild(links); 319 | 320 | // render NODES 321 | 322 | let containerParties = new PIXI.Container(); 323 | let containerIdeologies = new PIXI.Container(); 324 | graph.nodes.forEach((node) => { 325 | node.gfx = new PIXI.Graphics(); 326 | node.gfx.lineStyle(0.5, 0xFFFFFF); 327 | node.gfx.beginFill(colour(node.group)); 328 | node.gfx.drawCircle(0, 0, node.radius ); 329 | node.gfx.interactive = true; 330 | node.gfx.hitArea = new PIXI.Circle(0, 0, node.radius); 331 | node.gfx.mouseover = function(ev) { 332 | let nodex = node.x + 15; 333 | let nodey = node.y - 15; 334 | d3.select("#label") 335 | .attr("style", "left:"+nodex+"px;top:"+nodey+"px;") 336 | .select("a") 337 | .attr("href",node.id.replace("wd:","http://www.wikidata.org/entity/")) 338 | .attr("target","_blank") 339 | .text(node.label) 340 | }; 341 | node.gfx.on("pointerdown", function(ev) { // should work also on touchscreen but does not 342 | focus(node); 343 | }); 344 | /* 345 | node.gfx.on("tap", function(ev) { // touchscreen specific. Does not work. 346 | focus(node); 347 | }); 348 | */ 349 | 350 | 351 | if (node.group==1) containerParties.addChild(node.gfx); 352 | if (node.group==2) containerIdeologies.addChild(node.gfx); 353 | // stage.addChild(node.gfx); 354 | 355 | if (node.group == 2) { 356 | node.lgfx = new PIXI.Text( 357 | node.label, { 358 | fontFamily : 'Maven Pro', 359 | fontSize: 9 + node.radius / 2, 360 | fill : 0xFF0000, 361 | align : 'center' 362 | } 363 | ); 364 | containerIdeologies.addChild(node.lgfx); 365 | } 366 | }); 367 | 368 | 369 | containerLinks.zIndex = 0; 370 | containerIdeologies.zIndex = 2; 371 | containerParties.zIndex = 1; 372 | stage.addChild(containerLinks); 373 | stage.addChild(containerParties); 374 | stage.addChild(containerIdeologies); 375 | stage.children.sort((itemA, itemB) => itemA.zIndex - itemB.zIndex); 376 | 377 | /* 378 | stage.children.sort(function(a,b) { 379 | if (a.fillColor == 11454440 ) return -1; 380 | return 1; 381 | }); 382 | */ 383 | 384 | d3.select(renderer.view) 385 | .call( 386 | d3.drag() 387 | .container(renderer.view) 388 | .subject(() => simulation.find(d3.event.x, d3.event.y)) 389 | .on('start', dragstarted) 390 | .on('drag', dragged) 391 | .on('end', dragended) 392 | ); 393 | 394 | // ticked() 395 | function ticked() { 396 | // requestAnimationFrame(ticked); //this d3 on.ticker can be replaced by PIXI's "requestAnimationFrame" but the system is then too excited. See above 397 | graph.nodes.forEach((node) => { 398 | let { x, y, gfx, lgfx, radius } = node; 399 | gfx.position = new PIXI.Point(x, y); 400 | if (node.group == 2) lgfx.position = new PIXI.Point(x + radius / 2, y + radius /2); 401 | }); 402 | links.clear(); 403 | links.alpha = 0.6; 404 | graph.links.forEach((link) => { 405 | let { source, target } = link; 406 | links.lineStyle(Math.sqrt(link.value), 0x999999,link.alpha); 407 | links.moveTo(source.x, source.y); 408 | links.lineTo(target.x, target.y); 409 | }); 410 | links.endFill(); 411 | renderer.render(stage); 412 | // when this point is reached, the notification about loading can be removed 413 | loadinginfo.style('display', 'none'); 414 | constructingGraph.style('display', 'none'); 415 | document.getElementById("upgradeGraphButton").disabled = false; 416 | } 417 | 418 | simulation.alphaTarget(0.3).restart(); // give it an initial push 419 | } 420 | 421 | function dragstarted() { 422 | if (!d3.event.active) simulation.alphaTarget(0.3).restart(); 423 | d3.event.subject.fx = d3.event.subject.x; 424 | d3.event.subject.fy = d3.event.subject.y; 425 | } 426 | 427 | function dragged() { 428 | d3.event.subject.fx = d3.event.x; 429 | d3.event.subject.fy = d3.event.y; 430 | } 431 | 432 | function dragended() { 433 | if (!d3.event.active) simulation.alphaTarget(0); 434 | d3.event.subject.fx = null; 435 | d3.event.subject.fy = null; 436 | } 437 | 438 | function unSelectAllCountries(){ 439 | let allBoxes = d3.selectAll("input[type='checkbox']"); 440 | allBoxes._groups[0].forEach(b=>{b.checked = false}); 441 | } 442 | 443 | function selectGroupAndUpdate(group){ 444 | console.log(group); 445 | unSelectAllCountries(); 446 | let allBoxes = d3.selectAll("input[type='checkbox']"); 447 | allBoxes._groups[0].forEach(b=>{ 448 | console.log(b.id); 449 | if (group.includes(b.value)) b.checked = true 450 | }); 451 | updateGraph(); 452 | } 453 | 454 | // Graph highlight ------- 455 | 456 | let rootSelectedNode = {}; 457 | 458 | function focus(d) { 459 | console.log(d); 460 | if (rootSelectedNode == d) { 461 | unfocus(); 462 | } else { 463 | rootSelectedNode = d; 464 | markSelected(d); 465 | hideLabels(); 466 | revealLabels(); 467 | } 468 | updateColor(); 469 | } 470 | 471 | function unfocus() { 472 | graph.nodes.forEach(n => {n.marked = true}); 473 | graph.links.forEach(l => {l.marked = true}); 474 | hideLabels(); 475 | } 476 | 477 | function markSelected(d){ 478 | graph.nodes.forEach(n => {n.marked = false}) 479 | graph.links.forEach(l => {l.marked = false}) 480 | d.marked = true; 481 | let linked = []; 482 | graph.links.filter(l => 483 | l.source == d | l.target == d 484 | ).forEach(l => { 485 | l.marked = true; 486 | linked.push(l.source.id); 487 | linked.push(l.target.id) 488 | }); 489 | graph.nodes.forEach(n => n.marked = linked.includes(n.id) ? true : false) 490 | } 491 | 492 | function updateColor() { 493 | graph.nodes.filter(n => !n.marked).forEach(n => { 494 | n.gfx.alpha = 0.2; 495 | if (n.group == 2) n.lgfx.alpha=0.2 496 | }); 497 | graph.links.filter(l => !l.marked).forEach(l => l.alpha = 0.1 ); 498 | graph.nodes.filter(n => n.marked).forEach(n => { 499 | n.gfx.alpha = 1; 500 | if (n.group == 2) n.lgfx.alpha =1 501 | }); 502 | graph.links.filter(l => l.marked).forEach(l => l.alpha = 1); 503 | } 504 | 505 | function revealLabels(){ 506 | graph.nodes.filter(n => n.marked & n.group == 1 ).forEach(n => { 507 | n.lgfx = new PIXI.Text( 508 | n.label, { 509 | fontFamily : 'Maven Pro', 510 | fontSize: 9 , 511 | fill : n.gfx.fill, 512 | align : 'center' 513 | } 514 | ); 515 | stage.addChild(n.lgfx); 516 | }); 517 | } 518 | 519 | function hideLabels(){ 520 | graph.nodes.filter(n => n.marked & n.group == 1 ).forEach(n => { 521 | stage.removeChild(n.lgfx); 522 | n.lgfx = null; 523 | }); 524 | } 525 | 526 | 527 | // Graph updates ------------ 528 | 529 | 530 | function updateGraph(){ 531 | document.getElementById("upgradeGraphButton").disabled = true; 532 | simulation.stop(); 533 | graph = graphstore = null; 534 | loadinginfo.style('display', 'block'); 535 | updatingGraph.style('display', 'block'); 536 | let checked = []; 537 | let boxes = d3.selectAll("input[type='checkbox']:checked") 538 | boxes._groups[0].forEach(b=>{ 539 | checked.push(b.value) 540 | }); 541 | console.log(checked); 542 | stage.removeChildren(); 543 | let reqGraph = makeGraphReq(checked); 544 | let reqGraphExtra = makeGraphExtraReq(checked); 545 | // wait before launching 546 | getGraphData(reqGraph,reqGraphExtra); 547 | } 548 | 549 | // TODO add element without destroying everything 550 | function restoreGraph(){ 551 | // add all elements to graph removed by previous filter 552 | graphstore.nodes.forEach(sn => { 553 | if (graph.nodes.filter(n=> n.id == sn.id).length==0) graph.nodes.push(Object.assign({}, sn)); 554 | }) 555 | // TODO : something's wrong with attaching those links 556 | graphstore.links.forEach(sl => { 557 | if (graph.links.filter(l=> l.id == sl.id).length==0) graph.links.push(Object.assign({}, sl)); 558 | }) 559 | // relink nodes correcly 560 | graph.links.forEach(l => { 561 | l.source = graph.nodes.filter(n=> n.id == l.source.id)[0]; 562 | l.target = graph.nodes.filter(n=> n.id == l.target.id)[0]; 563 | }); 564 | } 565 | 566 | -------------------------------------------------------------------------------- /sparql/CountryList.rq: -------------------------------------------------------------------------------- 1 | SELECT DISTINCT ?country ?countryLabel 2 | WHERE { 3 | ?item wdt:P1142 ?linkTo . 4 | ?linkTo wdt:P31 wd:Q12909644 . # keep only targets that are political ideologies 5 | VALUES ?type { wd:Q7278 wd:Q24649 } # filter by these types of political actors 6 | ?item wdt:P31 ?type . 7 | ?item wdt:P17 ?country . 8 | MINUS { ?item wdt:P576 ?abolitionDate } # exclude abolished parties 9 | MINUS { ?country wdt:P576 ?countryAbolitionDate }. # exclude abolished countries 10 | SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en" . } 11 | } -------------------------------------------------------------------------------- /sparql/GraphExtraReq.rq: -------------------------------------------------------------------------------- 1 | SELECT DISTINCT ?linkTo ?linkToLabel ?superLinkTo ?superLinkToLabel 2 | WHERE { 3 | ?item wdt:P1142 ?linkTo . # alternative path makes link to ideology superclass 4 | ?linkTo wdt:P279+ ?superLinkTo . 5 | ?superLinkTo wdt:P31|wdt:P279 wd:Q12909644 . # take political ideology only 6 | VALUES ?type { wd:Q7278 wd:Q24649 } # filter by these types of political actors 7 | ?item wdt:P31|wdt:P279 ?type . 8 | ?item wdt:P17 ?country . 9 | MINUS { ?item wdt:P576 ?abolitionDate } # exclude abolished parties 10 | SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en,fr,es,ca,ko,zh" . } 11 | FILTER (?country IN ( JSVAR:COUNTRIES ) ) # here, this is faster than using VALUES. Why? 12 | } -------------------------------------------------------------------------------- /sparql/GraphReq.rq: -------------------------------------------------------------------------------- 1 | SELECT DISTINCT ?item ?itemLabel ?country ?countryLabel ?linkTo ?linkToLabel 2 | WHERE { 3 | ?item wdt:P1142 ?linkTo . # alternative path makes link to ideology superclass 4 | ?linkTo wdt:P31 wd:Q12909644 . # take "political ideology" only 5 | VALUES ?type { wd:Q7278 wd:Q24649 } # filter by these types of political actors 6 | ?item wdt:P31 ?type . 7 | VALUES ?country { JSVAR:COUNTRIES } #filter by selected countries 8 | ?item wdt:P17 ?country . 9 | MINUS { ?item wdt:P576 ?abolitionDate } # exclude abolished parties 10 | SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en,fr,es,ca,ko,zh" . } 11 | } -------------------------------------------------------------------------------- /yarn-error.log: -------------------------------------------------------------------------------- 1 | Arguments: 2 | /usr/local/Cellar/node/16.6.2/bin/node /usr/local/Cellar/yarn/1.22.11/libexec/bin/yarn.js add pixijs@5.3 3 | 4 | PATH: 5 | /Users/ourednik/opt/miniconda3/bin:/Users/ourednik/opt/miniconda3/condabin:/usr/local/sbin:/usr/local/sbin:/Applications/VisualSFM_OS_X_Installer-master/vsfm/bin:/Users/ourednik/.rbenv/shims:/Users/ourednik/.rbenv/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/X11/bin 6 | 7 | Yarn version: 8 | 1.22.11 9 | 10 | Node version: 11 | 16.6.2 12 | 13 | Platform: 14 | darwin x64 15 | 16 | Trace: 17 | Error: https://registry.yarnpkg.com/pixijs: Not found 18 | at Request.params.callback [as _callback] (/usr/local/Cellar/yarn/1.22.11/libexec/lib/cli.js:66992:18) 19 | at Request.self.callback (/usr/local/Cellar/yarn/1.22.11/libexec/lib/cli.js:140763:22) 20 | at Request.emit (node:events:394:28) 21 | at Request. (/usr/local/Cellar/yarn/1.22.11/libexec/lib/cli.js:141735:10) 22 | at Request.emit (node:events:394:28) 23 | at IncomingMessage. (/usr/local/Cellar/yarn/1.22.11/libexec/lib/cli.js:141657:12) 24 | at Object.onceWrapper (node:events:513:28) 25 | at IncomingMessage.emit (node:events:406:35) 26 | at endReadableNT (node:internal/streams/readable:1331:12) 27 | at processTicksAndRejections (node:internal/process/task_queues:83:21) 28 | 29 | npm manifest: 30 | { 31 | "name": "ideograph", 32 | "version": "0.1", 33 | "description": "Explorer of ideologies and plotical parties registered on WikiData", 34 | "main": "index.js", 35 | "repository": "https://ourednik.info", 36 | "author": "André Ourednik", 37 | "license": "GNU GPL 3.0", 38 | "files": [ 39 | "index.js", 40 | "index.html" 41 | ], 42 | "dependencies": { 43 | "d3.js": "5", 44 | "pixi": "^0.3.1", 45 | "pixi.js": "4.8" 46 | } 47 | } 48 | 49 | yarn manifest: 50 | No manifest 51 | 52 | Lockfile: 53 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 54 | # yarn lockfile v1 55 | 56 | 57 | bit-twiddle@^1.0.2: 58 | version "1.0.2" 59 | resolved "https://registry.yarnpkg.com/bit-twiddle/-/bit-twiddle-1.0.2.tgz#0c6c1fabe2b23d17173d9a61b7b7093eb9e1769e" 60 | integrity sha1-DGwfq+KyPRcXPZpht7cJPrnhdp4= 61 | 62 | d3.js@5: 63 | version "1.0.2" 64 | resolved "https://registry.yarnpkg.com/d3.js/-/d3.js-1.0.2.tgz#2fa3ec70750243e8f946bed356c8a487bd3b9df3" 65 | integrity sha512-k88E4MfCyHiioewoVnEaLkIMiS1bQsPUOnHouq0nICoDffV+TFFuqTtEAO9vXe5fI7+C5wUzpgFvmag1w4dkfg== 66 | 67 | earcut@^2.1.4: 68 | version "2.2.3" 69 | resolved "https://registry.yarnpkg.com/earcut/-/earcut-2.2.3.tgz#d44ced2ff5a18859568e327dd9c7d46b16f55cf4" 70 | integrity sha512-iRDI1QeCQIhMCZk48DRDMVgQSSBDmbzzNhnxIo+pwx3swkfjMh6vh0nWLq1NdvGHLKH6wIrAM3vQWeTj6qeoug== 71 | 72 | eventemitter3@^2.0.0: 73 | version "2.0.3" 74 | resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba" 75 | integrity sha1-teEHm1n7XhuidxwKmTvgYKWMmbo= 76 | 77 | ismobilejs@^0.5.1: 78 | version "0.5.2" 79 | resolved "https://registry.yarnpkg.com/ismobilejs/-/ismobilejs-0.5.2.tgz#e81bacf6187c532ad8348355f4fecd6e6adfdce1" 80 | integrity sha512-ta9UdV60xVZk/ZafFtSFslQaE76SvNkcs1r73d2PVR21zVzx9xuYv9tNe4MxA1NN7WoeCc2RjGot3Bz1eHDx3Q== 81 | 82 | mini-signals@^1.1.1: 83 | version "1.2.0" 84 | resolved "https://registry.yarnpkg.com/mini-signals/-/mini-signals-1.2.0.tgz#45b08013c5fae51a24aa1a935cd317c9ed721d74" 85 | integrity sha1-RbCAE8X65RokqhqTXNMXye1yHXQ= 86 | 87 | object-assign@^4.0.1: 88 | version "4.1.1" 89 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 90 | integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= 91 | 92 | parse-uri@^1.0.0: 93 | version "1.0.3" 94 | resolved "https://registry.yarnpkg.com/parse-uri/-/parse-uri-1.0.3.tgz#f3c24a74907a4e357c1741e96ca9faadecfd6db5" 95 | integrity sha512-upMnGxNcm+45So85HoguwZTVZI9u11i36DdxJfGF2HYWS2eh3TIx7+/tTi7qrEq15qzGkVhsKjesau+kCk48pA== 96 | 97 | pixi-gl-core@^1.1.4: 98 | version "1.1.4" 99 | resolved "https://registry.yarnpkg.com/pixi-gl-core/-/pixi-gl-core-1.1.4.tgz#8b4b5c433b31e419bc379dc565ce1b835a91b372" 100 | integrity sha1-i0tcQzsx5Bm8N53FZc4bg1qRs3I= 101 | 102 | pixi.js@4.8: 103 | version "4.8.9" 104 | resolved "https://registry.yarnpkg.com/pixi.js/-/pixi.js-4.8.9.tgz#36dc0de8907d9e64336436e237c6f7f7c0a362e1" 105 | integrity sha512-YcepG5/bXLAVTSTXaMIU9NeSzwyPq/oMu2oQi6L6iE5giwng02ixVCKgc6/eMv3zl2Ho+teSOLC8R5Wp3jBvLA== 106 | dependencies: 107 | bit-twiddle "^1.0.2" 108 | earcut "^2.1.4" 109 | eventemitter3 "^2.0.0" 110 | ismobilejs "^0.5.1" 111 | object-assign "^4.0.1" 112 | pixi-gl-core "^1.1.4" 113 | remove-array-items "^1.0.0" 114 | resource-loader "^2.2.3" 115 | 116 | pixi@^0.3.1: 117 | version "0.3.1" 118 | resolved "https://registry.yarnpkg.com/pixi/-/pixi-0.3.1.tgz#858cd6164a32a1e4b41192cdf5c6d98209300ba8" 119 | integrity sha1-hYzWFkoyoeS0EZLN9cbZggkwC6g= 120 | 121 | remove-array-items@^1.0.0: 122 | version "1.1.1" 123 | resolved "https://registry.yarnpkg.com/remove-array-items/-/remove-array-items-1.1.1.tgz#fd745ff73d0822e561ea910bf1b401fc7843e693" 124 | integrity sha512-MXW/jtHyl5F1PZI7NbpS8SOtympdLuF20aoWJT5lELR1p/HJDd5nqW8Eu9uLh/hCRY3FgvrIT5AwDCgBODklcA== 125 | 126 | resource-loader@^2.2.3: 127 | version "2.2.4" 128 | resolved "https://registry.yarnpkg.com/resource-loader/-/resource-loader-2.2.4.tgz#9bf43dba59475d56be29c796399211ce0e96fd2d" 129 | integrity sha512-MrY0bEJN26us3h4bzJUSP0n4tFEb79lCpYBavtLjSezWCcXZMgxhSgvC9LxueuqpcxG+qPjhwFu5SQAcUNacdA== 130 | dependencies: 131 | mini-signals "^1.1.1" 132 | parse-uri "^1.0.0" 133 | --------------------------------------------------------------------------------