🏠 Home 

Wikipedia History and History Visualizer

Visualize Wikipedia Articles that you've visited in an interactive graph


Install this script?
  1. // ==UserScript==
  2. // @name Wikipedia History and History Visualizer
  3. // @namespace https://en.wikipedia.org/wiki/*
  4. // @description Visualize Wikipedia Articles that you've visited in an interactive graph
  5. // @author Sidem
  6. // @version 0.1
  7. // @match https://en.wikipedia.org/wiki/*
  8. // @grant none
  9. // @license GPL-3.0-only
  10. // ==/UserScript==
  11. const dbName = "WikipediaHistoryDB";
  12. const objectStoreName = "wikipediaHistory";
  13. function openDatabase() {
  14. return new Promise((resolve, reject) => {
  15. const request = indexedDB.open(dbName, 1);
  16. request.onupgradeneeded = (event) => {
  17. const db = event.target.r###lt;
  18. db.createObjectStore(objectStoreName, { keyPath: "id", autoIncrement: true });
  19. };
  20. request.onsuccess = (event) => resolve(event.target.r###lt);
  21. request.onerror = (event) => reject(event.target.error);
  22. });
  23. }
  24. async function getWikipediaHistoryData() {
  25. const db = await openDatabase();
  26. const transaction = db.transaction(objectStoreName, "readonly");
  27. const objectStore = transaction.objectStore(objectStoreName);
  28. const request = objectStore.getAll();
  29. return new Promise((resolve, reject) => {
  30. request.onsuccess = (event) => resolve(event.target.r###lt);
  31. request.onerror = (event) => reject(event.target.error);
  32. });
  33. }
  34. async function setWikipediaHistoryData(data) {
  35. const db = await openDatabase();
  36. const transaction = db.transaction(objectStoreName, "readwrite");
  37. const objectStore = transaction.objectStore(objectStoreName);
  38. const request = objectStore.clear();
  39. request.onsuccess = () => {
  40. if (data && data.length) { // Add this condition
  41. for (const item of data) {
  42. objectStore.add(item);
  43. }
  44. }
  45. };
  46. return new Promise((resolve, reject) => {
  47. transaction.oncomplete = () => resolve();
  48. transaction.onerror = (event) => reject(event.target.error);
  49. });
  50. }
  51. const linkToTitle = (link) => { return link.split('#')[0].split('/wiki/')[1] };
  52. const createHistoryButton = () => {
  53. const button = document.createElement('button');
  54. button.innerText = 'View History Graph';
  55. button.style.position = 'fixed';
  56. button.style.top = '10px';
  57. button.style.right = '10px';
  58. button.style.zIndex = 1000;
  59. button.onclick = () => {
  60. window.location = '/wiki/History_visualization_script';
  61. };
  62. document.body.appendChild(button);
  63. };
  64. let tooltipTimer;
  65. const addStyles = () => {
  66. const style = document.createElement("style");
  67. style.innerHTML = `
  68. svg {
  69. font-family: sans-serif;
  70. overflow: visible;
  71. }
  72. circle {
  73. stroke: #fff;
  74. stroke-width: 1.5px;
  75. }
  76. line {
  77. stroke: #999;
  78. stroke-opacity: 0.6;
  79. stroke-width: 2;
  80. }
  81. text {
  82. /*make unselectable*/
  83. -webkit-user-select: none;
  84. -moz-user-select: none;
  85. -ms-user-select: none;
  86. user-select: none;
  87. pointer-events: none;
  88. }
  89. body {
  90. overflow-y: hidden !important;
  91. overflow-x: hidden !important;
  92. }
  93. `;
  94. document.head.appendChild(style);
  95. };
  96. const forceCluster = () => {
  97. const strength = 0.3;
  98. const clusters = new Map();
  99. let nodes;
  100. function force(alpha) {
  101. for (const node of nodes) {
  102. const cluster = clusters.get(node.clusterId);
  103. if (!cluster) continue;
  104. let { x: cx, y: cy } = cluster;
  105. node.vx -= (node.x - cx) * strength * alpha;
  106. node.vy -= (node.y - cy) * strength * alpha;
  107. }
  108. }
  109. force.initialize = (_) => (nodes = _);
  110. force.clusters = (_) => {
  111. clusters.clear();
  112. for (const node of _) {
  113. clusters.set(node.clusterId, node);
  114. }
  115. return force;
  116. };
  117. return force;
  118. };
  119. function drag(simulation) {
  120. function dragstarted(event, d) {
  121. if (!event.active) simulation.alphaTarget(0.3).restart();
  122. d.fx = d.x;
  123. d.fy = d.y;
  124. }
  125. function dragged(event, d) {
  126. d.fx = event.x;
  127. d.fy = event.y;
  128. }
  129. function dragended(event, d) {
  130. if (!event.active) simulation.alphaTarget(0);
  131. d.fx = null;
  132. d.fy = null;
  133. }
  134. return d3
  135. .drag()
  136. .on("start", dragstarted)
  137. .on("drag", dragged)
  138. .on("end", dragended);
  139. }
  140. const clearGraph = () => {
  141. d3.select("#graphContainer").remove();
  142. };
  143. let isMouseOverNode = false;
  144. const createGraph = (wikipediaHistoryData) => {
  145. const deleteEntry = (id) => {
  146. const updatedHistoryData = wikipediaHistoryData.filter((_, index) => index !== id);
  147. localStorage.setItem('wikipediaHistory', JSON.stringify(updatedHistoryData));
  148. clearGraph();
  149. createGraph(updatedHistoryData);
  150. };
  151. const links = wikipediaHistoryData.flatMap((entry, index) =>
  152. entry.links.map((link) => {
  153. const targetIndex = wikipediaHistoryData.findIndex((e) => e.link === link.link);
  154. return targetIndex !== -1 ? { source: index, target: targetIndex } : null;
  155. })
  156. ).filter((link) => link !== null);
  157. const nodes = wikipediaHistoryData.map((entry, index) => {
  158. const connectedNodes = links.filter(link => link.source === index || link.target === index);
  159. const clusterId = connectedNodes.length > 0 ? index : null;
  160. return { id: index, label: entry.title, clusterId };
  161. });
  162. const svg = d3.select("#graph");
  163. const width = +svg.attr("width");
  164. const height = +svg.attr("height");
  165. const g = svg.append("g").attr("id", "graphContainer");
  166. svg.append("defs")
  167. .append("marker")
  168. .attr("id", "arrowhead")
  169. .attr("viewBox", "-0 -5 10 10")
  170. .attr("refX", 13)
  171. .attr("refY", 0)
  172. .attr("orient", "auto")
  173. .attr("markerWidth", 10)
  174. .attr("markerHeight", 10)
  175. .attr("xoverflow", "visible")
  176. .append("svg:path")
  177. .attr("d", "M 0,-5 L 10 ,0 L 0,5")
  178. .attr("fill", "#999")
  179. .style("stroke", "none");
  180. const simulation = d3
  181. .forceSimulation(nodes)
  182. .force("link", d3.forceLink(links).id((d) => d.id).distance(50))
  183. .force("charge", d3.forceManyBody().strength(-200))
  184. .force("center", d3.forceCenter(width / 2, height / 2))
  185. .force("collide", d3.forceCollide(60))
  186. .force("cluster", forceCluster().clusters(nodes)); // Add cluster force
  187. const link = g
  188. .selectAll("line")
  189. .data(links)
  190. .join("line")
  191. .attr("stroke", "#999")
  192. .attr("stroke-opacity", 0.6)
  193. .attr("stroke-width", 2)
  194. .attr("marker-end", (d) => {
  195. const sourceLinks = wikipediaHistoryData[d.source.index].links;
  196. const targetLinks = wikipediaHistoryData[d.target.index].links;
  197. const sourceToTarget = sourceLinks.some((link) => link.link === wikipediaHistoryData[d.target.index].link);
  198. const targetToSource = targetLinks.some((link) => link.link === wikipediaHistoryData[d.source.index].link);
  199. return sourceToTarget && !targetToSource ? "url(#arrowhead)" : "";
  200. });
  201. const node = g
  202. .selectAll("circle")
  203. .data(nodes)
  204. .join("circle")
  205. .attr("r", 5)
  206. .attr("fill", "#69b3a2");
  207. node.on("dblclick", (event, d) => {
  208. window.open(`https://en.wikipedia.org/wiki/${wikipediaHistoryData[d.id].link}`, "_blank");
  209. });
  210. const tooltip = d3.select("body").append("div")
  211. .attr("class", "tooltip")
  212. .style("opacity", 0)
  213. .style("background-color", "white")
  214. .style("border", "solid")
  215. .style("border-width", "1px")
  216. .style("border-radius", "5px")
  217. .style("padding", "10px")
  218. .style("position", "absolute")
  219. .style("pointer-events", "auto");
  220. node.on("mouseover", (event, d) => {
  221. isMouseOverNode = true;
  222. tooltip.transition()
  223. .duration(200)
  224. .style("opacity", 1);
  225. tooltip.html(`Title: ${wikipediaHistoryData[d.id].title}<br/>URL: https://en.wikipedia.org/wiki/${wikipediaHistoryData[d.id].link}<br/>Dates Accessed: ${wikipediaHistoryData[d.id].dates_accessed.join(', ')}<br/><button id="deleteButton">Delete</button>`)
  226. .style("left", `${event.pageX + 10}px`)
  227. .style("top", `${event.pageY + 10}px`);
  228. tooltip.select("#deleteButton").on("click", () => deleteEntry(d.id));
  229. })
  230. // ...
  231. .on("mousemove", (event) => {
  232. tooltip.style("left", `${event.pageX + 10}px`).style("top", `${event.pageY + 10}px`);
  233. });
  234. tooltip.on("mouseover", () => {
  235. clearTimeout(tooltipTimer);
  236. if (isMouseOverNode) {
  237. tooltip.style("opacity", 1);
  238. // move tooltip back to mouse position
  239. tooltip.style("left", `${d3.event.pageX + 10}px`).style("top", `${d3.event.pageY + 10}px`);
  240. }
  241. })
  242. .on("mouseout", () => {
  243. clearTimeout(tooltipTimer);
  244. tooltipTimer = setTimeout(() => {
  245. tooltip.transition()
  246. .duration(200)
  247. .style("opacity", 0);
  248. //also move invisible tooltip out of screen so it doesn't block mouse events
  249. tooltip.style("left", "-1000px").style("top", "-1000px");
  250. }, 150);
  251. });
  252. node.on("mouseout", () => {
  253. isMouseOverNode = false;
  254. clearTimeout(tooltipTimer);
  255. tooltipTimer = setTimeout(() => {
  256. tooltip.transition()
  257. .duration(200)
  258. .style("opacity", 0);
  259. //also move invisible tooltip out of screen so it doesn't block mouse events
  260. tooltip.style("left", "-1000px").style("top", "-1000px");
  261. }, 150);
  262. });
  263. const label = g
  264. .selectAll("text")
  265. .data(nodes)
  266. .join("text")
  267. .text((d) => d.label)
  268. .attr("font-size", "10px")
  269. .attr("dx", 8)
  270. .attr("dy", "0.35em");
  271. const zoomBehavior = d3.zoom()
  272. .scaleExtent([0.1, 5])
  273. .on('zoom', (event) => {
  274. g.attr('transform', event.transform);
  275. });
  276. svg.call(zoomBehavior);
  277. simulation.on("tick", () => {
  278. link
  279. .attr("x1", (d) => d.source.x)
  280. .attr("y1", (d) => d.source.y)
  281. .attr("x2", (d) => d.target.x)
  282. .attr("y2", (d) => d.target.y);
  283. node
  284. .attr("cx", (d) => d.x)
  285. .attr("cy", (d) => d.y);
  286. label
  287. .attr("x", (d) => d.x)
  288. .attr("y", (d) => d.y);
  289. });
  290. };
  291. window.addEventListener('load', () => {
  292. let links = document.querySelector('#bodyContent').querySelectorAll('a[href*="/wiki/"]');
  293. let title = document.querySelector('#firstHeading').innerText;
  294. let link = linkToTitle(window.location.pathname);
  295. let linkEntries = [];
  296. for (let i = 0; i < links.length; i++) {
  297. if (links[i].href.includes('/wiki/')) {
  298. let linkEntry = {
  299. title: links[i].title,
  300. link: linkToTitle(links[i].href)
  301. };
  302. linkEntries.push(linkEntry);
  303. }
  304. }
  305. let entry = {
  306. title: title,
  307. link: link,
  308. links: linkEntries
  309. };
  310. let date = new Date();
  311. let dateString = `${date.getDate()}.${date.getMonth() + 1}.${date.getFullYear()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
  312. let database;
  313. getWikipediaHistoryData().then(data => {
  314. database = data;
  315. if (database) {
  316. let entryIndex = -1;
  317. for (let i = 0; i < database.length; i++) {
  318. if (database[i].title === entry.title) {
  319. entryIndex = i;
  320. break;
  321. }
  322. }
  323. if (entryIndex < 0) {
  324. entry.dates_accessed = [dateString];
  325. database.push(entry);
  326. } else {
  327. let lastDate = database[entryIndex].dates_accessed[database[entryIndex].dates_accessed.length - 1];
  328. if (lastDate.split(' ')[0] !== dateString.split(' ')[0]) {
  329. database[entryIndex].dates_accessed.push(dateString);
  330. }
  331. database[entryIndex].links = entry.links;
  332. }
  333. } else {
  334. entry.dates_accessed = [dateString];
  335. database = [entry];
  336. }
  337. if (link === 'History_visualization_script') {
  338. addStyles();
  339. const bodyContent = document.querySelector('#bodyContent');
  340. bodyContent.innerHTML = '';
  341. document.body.innerHTML = '';
  342. document.body.appendChild(bodyContent);
  343. let script = document.createElement('script');
  344. script.src = 'https://d3js.org/d3.v7.min.js';
  345. document.querySelector('#bodyContent').appendChild(script);
  346. let script2 = document.createElement('script');
  347. script2.src = 'https://unpkg.com/d3-force-cluster@latest';
  348. document.querySelector('#bodyContent').appendChild(script2);
  349. script.onload = () => {
  350. let svgElement = document.createElementNS("http://www.w3.org/2000/svg", 'svg');
  351. svgElement.setAttribute('width', window.innerWidth);
  352. svgElement.setAttribute('height', window.innerHeight);
  353. svgElement.setAttribute('id', 'graph');
  354. document.querySelector('#bodyContent').appendChild(svgElement);
  355. getWikipediaHistoryData().then(wikipediaHistoryData => {
  356. createGraph(wikipediaHistoryData);
  357. });
  358. }
  359. } else {
  360. setWikipediaHistoryData(database);
  361. createHistoryButton();
  362. }
  363. });
  364. }, false);