├── .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 | 
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 |
35 |
Source : WikiData (live requests to SPARQL endpoint)
36 |
37 |
38 |
39 |
59 |
60 |
61 |
Loading list of countries...
62 |
Countries loaded.
63 |
Loading graph from WikiData...
64 |
Loading list of countries...
65 |
Updatig graph...
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 |
--------------------------------------------------------------------------------