app.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671
  1. const parties_list = [
  2. {
  3. "Name": "CDU",
  4. "comment": null,
  5. "highres":
  6. "https://de.wikipedia.org/wiki/Datei:CDU_flagge_logo_alternativ.svg",
  7. "preview":
  8. "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0e/CDU_Logo_2023.svg/330px-CDU_Logo_2023.svg.png",
  9. "color": "Black",
  10. "hex": "#000000",
  11. },
  12. {
  13. "Name": "AFD",
  14. "comment": " Duplicate of Alternative für Deutschland (Afd)",
  15. "highres":
  16. "https://upload.wikimedia.org/wikipedia/commons/b/b1/AfD_Logo_2021.svg?download",
  17. "preview":
  18. "https://upload.wikimedia.org/wikipedia/commons/thumb/4/48/AfD-Logo-2017.svg/240px-AfD-Logo-2017.svg.png",
  19. "color": "Dark Blue",
  20. "hex": "#003A70",
  21. },
  22. {
  23. "Name": "SPD",
  24. "comment": null,
  25. "highres": "https://de.wikipedia.org/wiki/Datei:SPD_Hessen.svg",
  26. "preview":
  27. "https://upload.wikimedia.org/wikipedia/de/thumb/c/ca/SPD_Hessen.svg/240px-SPD_Hessen.svg.png",
  28. "color": "Red",
  29. "hex": "#E3000F",
  30. },
  31. {
  32. "Name": "Bündnis 90/Die Grünen",
  33. "comment": null,
  34. "highres":
  35. "https://de.wikipedia.org/wiki/Datei:B%C3%BCndnis_90_Die_Gr%C3%BCnen.svg",
  36. "preview":
  37. "https://upload.wikimedia.org/wikipedia/de/thumb/1/16/B%C3%BCndnis_90_Die_Gr%C3%BCnen.svg/240px-B%C3%BCndnis_90_Die_Gr%C3%BCnen.svg.png",
  38. "color": "Green",
  39. "hex": "#00A950",
  40. },
  41. {
  42. "Name": "LINKE",
  43. "comment": null,
  44. "highres":
  45. "https://www.energie-klimaschutz.de/wp-content/uploads/2017/05/Die-Linke_DEZ_20170503.jpg",
  46. "preview":
  47. "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d6/Logo_Die_Linke_%282023%29.svg/450px-Logo_Die_Linke_%282023%29.svg.png",
  48. "color": "Maroon",
  49. "hex": "#A30000",
  50. },
  51. {
  52. "Name": "CSU",
  53. "comment": null,
  54. "highres": "https://de.wikipedia.org/wiki/Datei:CSU_Logo_since_2016.svg",
  55. "preview":
  56. "https://upload.wikimedia.org/wikipedia/commons/thumb/e/ee/CSU_Logo_since_2016.svg/330px-CSU_Logo_since_2016.svg.png",
  57. "color": "Blue",
  58. "hex": "#007AC2",
  59. },
  60. {
  61. "Name": "FDP",
  62. "comment": null,
  63. "highres": "https://de.wikipedia.org/wiki/Datei:FDP_Sachsen-Anhalt.svg",
  64. "preview":
  65. "https://upload.wikimedia.org/wikipedia/de/thumb/e/e8/FDP_Sachsen-Anhalt.svg/240px-FDP_Sachsen-Anhalt.svg.png",
  66. "color": "Yellow",
  67. "hex": "#FFED00",
  68. },
  69. {
  70. "Name": "SSW",
  71. "comment": null,
  72. "highres":
  73. "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/S%C3%BCdschleswigscher_W%C3%A4hlerverband%2C_Logo.svg/500px-S%C3%BCdschleswigscher_W%C3%A4hlerverband%2C_Logo.svg.png",
  74. "preview":
  75. "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/S%C3%BCdschleswigscher_W%C3%A4hlerverband%2C_Logo.svg/640px-S%C3%BCdschleswigscher_W%C3%A4hlerverband%2C_Logo.svg.png?download",
  76. "color": "Yellow",
  77. "hex": "#FFD700",
  78. },
  79. {
  80. "Name": "BSW - Bündnis Sarah Wagenknecht",
  81. "comment": null,
  82. "highres":
  83. "https://de.wikipedia.org/wiki/Datei:B%C3%BCndnis_Sahra_Wagenknecht_logo_2.svg",
  84. "preview":
  85. "https://upload.wikimedia.org/wikipedia/commons/thumb/7/7f/B%C3%BCndnis_Sahra_Wagenknecht_logo_2.svg/450px-B%C3%BCndnis_Sahra_Wagenknecht_logo_2.svg.png",
  86. "color": "Deep Purple",
  87. "hex": "#660099",
  88. },
  89. {
  90. "Name": "Volt",
  91. "comment": null,
  92. "highres": "https://de.wikipedia.org/wiki/Datei:Logo_of_Volt.svg",
  93. "preview":
  94. "https://upload.wikimedia.org/wikipedia/commons/thumb/6/68/Logo_of_Volt.svg/600px-Logo_of_Volt.svg.png",
  95. "color": "Purple",
  96. "hex": "#7F3FBF",
  97. },
  98. {
  99. "Name": "MLPD",
  100. "comment": null,
  101. "highres": "https://de.wikipedia.org/wiki/Datei:MLPD_Logo_2011_(2).svg",
  102. "preview":
  103. "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b1/MLPD_Logo_2011_%282%29.svg/300px-MLPD_Logo_2011_%282%29.svg.png",
  104. "color": "Red",
  105. "hex": "#FF0000",
  106. },
  107. {
  108. "Name": "DKP",
  109. "comment": null,
  110. "highres":
  111. "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c5/Deutsche_Kommunistische_Partei_Logo.svg/500px-Deutsche_Kommunistische_Partei_Logo.svg.png",
  112. "preview":
  113. "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c5/Deutsche_Kommunistische_Partei_Logo.svg/240px-Deutsche_Kommunistische_Partei_Logo.svg.png",
  114. "color": "Red",
  115. "hex": "#FF0000",
  116. },
  117. {
  118. "Name": "Team Todenhöfer",
  119. "comment": null,
  120. "highres":
  121. "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d4/Gerechtigkeitsparteilogo.png/500px-Gerechtigkeitsparteilogo.png",
  122. "preview":
  123. "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d4/Gerechtigkeitsparteilogo.png/240px-Gerechtigkeitsparteilogo.png",
  124. "color": "Teal",
  125. "hex": "#008080",
  126. },
  127. {
  128. "Name": "Piraten",
  129. "comment": null,
  130. "highres": "https://de.wikipedia.org/wiki/Datei:Pidataratpartiet.svg",
  131. "preview":
  132. "https://upload.wikimedia.org/wikipedia/commons/thumb/8/8c/Piratenpartei_deutschland_logo.svg/640px-Piratenpartei_deutschland_logo.svg.png?download",
  133. "color": "Purple",
  134. "hex": "#6600CC",
  135. },
  136. {
  137. "Name": "ÖDP",
  138. "comment": null,
  139. "highres":
  140. "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3e/OEDP_Logo_CMYK.svg/300px-OEDP_Logo_CMYK.svg.png",
  141. "preview":
  142. "https://upload.wikimedia.org/wikipedia/commons/3/3e/OEDP_Logo_CMYK.svg",
  143. "color": "Green",
  144. "hex": "#008000",
  145. },
  146. {
  147. "Name": "PARTEI",
  148. "comment": null,
  149. "highres":
  150. "https://de.wikipedia.org/wiki/Datei:Die_PARTEI_Logo_Blur_Schatten_Vektor_V2.svg",
  151. "preview":
  152. "https://upload.wikimedia.org/wikipedia/commons/thumb/8/85/Die_PARTEI_Logo_Blur_Schatten_Vektor_V2.svg/640px-Die_PARTEI_Logo_Blur_Schatten_Vektor_V2.svg.png?download",
  153. "color": "Pink",
  154. "hex": "#FF69B4",
  155. },
  156. {
  157. "Name": "FW",
  158. "comment": null,
  159. "highres": "https://de.wikipedia.org/wiki/Datei:Freie_Waehler_Logo.svg",
  160. "preview":
  161. "https://upload.wikimedia.org/wikipedia/de/thumb/a/a6/Freie_Waehler_Logo.svg/240px-Freie_Waehler_Logo.svg.png",
  162. "color": "Orange",
  163. "hex": "#FF7F00",
  164. },
  165. {
  166. "Name": "Bayernpartei",
  167. "comment": null,
  168. "highres": null,
  169. "preview":
  170. "https://upload.wikimedia.org/wikipedia/commons/thumb/4/48/Bayernpartei_Logo.svg/270px-Bayernpartei_Logo.svg.png",
  171. "color": "Blue",
  172. "hex": "#0000FF",
  173. },
  174. {
  175. "Name": "Freie Wähler",
  176. "comment": null,
  177. "highres": "https://de.wikipedia.org/wiki/Datei:Freie_Waehler_Logo.svg",
  178. "preview":
  179. "https://upload.wikimedia.org/wikipedia/de/thumb/a/a6/Freie_Waehler_Logo.svg/240px-Freie_Waehler_Logo.svg.png",
  180. "color": "Orange",
  181. "hex": "#FF7F00",
  182. },
  183. {
  184. "Name": "NPD",
  185. "comment": "Umbenannt zu Heimat",
  186. "highres":
  187. "https://upload.wikimedia.org/wikipedia/commons/a/a2/Heimat-Logo.png",
  188. "preview":
  189. "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a2/Heimat-Logo.png/240px-Heimat-Logo.png",
  190. "color": "Dark Blue",
  191. "hex": "#00008B",
  192. },
  193. {
  194. "Name": "BVB/Freie Wähler",
  195. "comment": "Duplicate of “Freie Wähler”",
  196. "highres": null,
  197. "preview":
  198. "https://upload.wikimedia.org/wikipedia/de/thumb/a/a6/Freie_Waehler_Logo.svg/240px-Freie_Waehler_Logo.svg.png",
  199. "color": "Orange",
  200. "hex": "#FF7F00",
  201. },
  202. {
  203. "Name": "WerteUnion",
  204. "comment": null,
  205. "highres": "https://de.wikipedia.org/wiki/Datei:WerteUnion_Logo.svg",
  206. "preview":
  207. "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e2/WerteUnion_Logo.svg/640px-WerteUnion_Logo.svg.png",
  208. "color": "Dark Blue",
  209. "hex": "#00008B",
  210. },
  211. ];
  212. const URI = "localhost";
  213. const PORT = 7687;
  214. const DEFAULT_YEAR_RANGE = [2021, 2025];
  215. const DEFAULT_LIMIT = 250;
  216. const NEO4J_CONFIG = {
  217. serverUrl: `bolt://${URI}:${PORT}`,
  218. serverUser: "neo4j",
  219. serverPassword: "lkjk3jlksfjlk34lkjrs432",
  220. };
  221. const BASE_CONFIG = {
  222. containerId: "viz",
  223. neo4j: NEO4J_CONFIG,
  224. labels: {
  225. visConfig: {
  226. nodes: {},
  227. edges: {
  228. length: 100,
  229. arrows: {
  230. to: { enabled: true },
  231. },
  232. },
  233. physics: {
  234. hierarchicalRepulsion: {
  235. avoidOverlap: 1,
  236. },
  237. solver: "repulsion",
  238. repulsion: {
  239. nodeDistance: 1500,
  240. },
  241. },
  242. layout: {
  243. improvedLayout: true,
  244. randomSeed: 420,
  245. hierarchical: {
  246. enabled: true,
  247. direction: "DU",
  248. sortMethod: "directed",
  249. nodeSpacing: 1000,
  250. treeSpacing: 20,
  251. levelSeparation: 250,
  252. },
  253. },
  254. },
  255. "Party": {
  256. label: "name",
  257. value: "pagerank",
  258. group: "community",
  259. [NeoVis.NEOVIS_ADVANCED_CONFIG]: {
  260. cypher: {
  261. value:
  262. "MATCH (n) WHERE id(n) = $id MATCH ()-[r]-(n) RETURN sum(r.amount) AS c",
  263. },
  264. function: {
  265. // color: (node) => ({
  266. // background: node.properties.hex || "#CCCCCC",
  267. // border: node.properties.hex || "#CCCCCC",
  268. // }),
  269. title: (props) =>
  270. NeoVis.objectToTitleHtml(props, ["name", "pagerank", "community"]),
  271. image: (node) => node.properties.preview,
  272. },
  273. static: {
  274. font: {
  275. size: 15,
  276. color: "#000000", // Font color
  277. },
  278. shape: "image", // Optional: specify shape
  279. size: 30, // Node size
  280. },
  281. },
  282. },
  283. "Person": {
  284. label: "name",
  285. value: "pagerank",
  286. group: "community",
  287. [NeoVis.NEOVIS_ADVANCED_CONFIG]: {
  288. cypher: {
  289. value:
  290. "MATCH (n) WHERE id(n) = $id MATCH (n)-[r]-() RETURN sum(r.amount) AS c",
  291. },
  292. function: {
  293. title: (props) =>
  294. NeoVis.objectToTitleHtml(props, ["name", "pagerank", "community"]),
  295. },
  296. static: {
  297. font: {
  298. size: 20,
  299. color: "#000000",
  300. },
  301. shape: "dot",
  302. // color: {
  303. // background: "#33FF57",
  304. // border: "#39C700",
  305. // },
  306. size: 25,
  307. },
  308. },
  309. },
  310. "Entity": {
  311. value: "pagerank",
  312. label: "name",
  313. group: "community",
  314. [NeoVis.NEOVIS_ADVANCED_CONFIG]: {
  315. cypher: {
  316. value:
  317. "MATCH (n) WHERE id(n) = $id MATCH (n)-[r]-() RETURN sum(r.amount) AS c",
  318. },
  319. function: {
  320. title: (props) =>
  321. NeoVis.objectToTitleHtml(props, ["name", "pagerank", "community"]),
  322. },
  323. static: {
  324. font: {
  325. size: 18,
  326. // color: "#000000",
  327. },
  328. shape: "dot",
  329. // color: {
  330. // background: "#3357FF",
  331. // border: "#0039C7",
  332. // },
  333. size: 20,
  334. },
  335. },
  336. },
  337. },
  338. relationships: {
  339. "DONATION": {
  340. value: "amount",
  341. group: "community",
  342. [NeoVis.NEOVIS_ADVANCED_CONFIG]: {
  343. function: {
  344. title: NeoVis.objectToTitleHtml,
  345. },
  346. },
  347. },
  348. },
  349. };
  350. // Alpine.js data store
  351. document.addEventListener("alpine:init", () => {
  352. Alpine.store("filter", {
  353. parties: parties_list,
  354. donors: [""],
  355. yearRange: [DEFAULT_YEAR_RANGE[0], DEFAULT_YEAR_RANGE[1]],
  356. donorType: "all",
  357. strict: true,
  358. limit: DEFAULT_LIMIT,
  359. addDonorField() {
  360. this.donors.push("");
  361. },
  362. removeDonorField(index) {
  363. this.donors.splice(index, 1);
  364. },
  365. setDonorType(type) {
  366. this.donorType = type;
  367. },
  368. updateYearRange(start, end) {
  369. this.yearRange = [
  370. Math.min(parseInt(start), parseInt(end)),
  371. Math.max(parseInt(start), parseInt(end)),
  372. ];
  373. },
  374. });
  375. });
  376. class QueryBuilder {
  377. buildQuery(params = {}) {
  378. const {
  379. parties = [],
  380. donors = [],
  381. yearRange = DEFAULT_YEAR_RANGE,
  382. strict = true,
  383. donorType = "all",
  384. limit = DEFAULT_LIMIT,
  385. } = params;
  386. let donorMatch = "(n)";
  387. if (donorType === "person") {
  388. donorMatch = "(n:Person)";
  389. } else if (donorType === "entity") {
  390. donorMatch = "(n:Entity)";
  391. }
  392. const whereConditions = [];
  393. // Add party conditions only if not all parties are selected
  394. if (parties.length > 0 && parties.length < parties_list.length) {
  395. const partyCondition = parties.map((p) => `m.name = '${p}'`).join(" OR ");
  396. whereConditions.push(`(${partyCondition})`);
  397. }
  398. // Add donor name conditions
  399. if (donors.length > 0) {
  400. const validDonors = donors.filter(Boolean);
  401. if (validDonors.length > 0) {
  402. const donorCondition = validDonors.map((d) => `n.name CONTAINS '${d}'`)
  403. .join(" OR ");
  404. whereConditions.push(`(${donorCondition})`);
  405. }
  406. }
  407. // Add year range condition - always included and connected with AND
  408. const yearCondition = `(r.year >= ${yearRange[0]} AND r.year <= ${
  409. yearRange[1]
  410. })`;
  411. // Build WHERE clause
  412. let whereClause = "";
  413. if (whereConditions.length > 0) {
  414. const otherConditions = whereConditions.join(strict ? " AND " : " OR ");
  415. whereClause = `WHERE (${otherConditions}) AND ${yearCondition}`;
  416. } else {
  417. whereClause = `WHERE ${yearCondition}`;
  418. }
  419. return `
  420. MATCH ${donorMatch}-[r:DONATION]->(m:Party)
  421. ${whereClause}
  422. RETURN n, r, m
  423. ORDER BY r.amount DESC
  424. LIMIT ${limit}
  425. `.trim();
  426. }
  427. }
  428. // UI State Management
  429. let viz;
  430. const queryBuilder = new QueryBuilder();
  431. function initializeVisualization() {
  432. viz = new NeoVis.default({
  433. ...BASE_CONFIG,
  434. initialCypher: queryBuilder.buildQuery(),
  435. });
  436. viz.render();
  437. }
  438. // UI State Management
  439. function updateVisualization(reset = true) {
  440. const filterState = Alpine.store("filter");
  441. const query = queryBuilder.buildQuery({
  442. parties: filterState.parties,
  443. donors: filterState.donors.filter(Boolean),
  444. yearRange: filterState.yearRange,
  445. strict: filterState.strict,
  446. donorType: filterState.donorType,
  447. limit: filterState.limit,
  448. });
  449. if (reset) {
  450. viz.clearNetwork();
  451. }
  452. viz.updateWithCypher(query);
  453. console.log("Query", query, reset);
  454. }
  455. // Initialize application
  456. document.addEventListener("DOMContentLoaded", async () => {
  457. // Observe explainer visibility
  458. const explainer = document.getElementById("explainer");
  459. const toggleButton = document.getElementById("toggleSidebar");
  460. const sidebar = document.getElementById("sidebar");
  461. initializeVisualization();
  462. initializePartySelect();
  463. // Event listeners for filter changes
  464. Alpine.effect(() => {
  465. // Check for saved state
  466. const savedState = localStorage.getItem("visualizationState");
  467. if (savedState) {
  468. // Show explainer when there is no state
  469. document.getElementById("explainer").classList.add("translate-x-full");
  470. }
  471. // Save State
  472. const state = Alpine.store("filter");
  473. localStorage.setItem("visualizationState", JSON.stringify(state));
  474. // Track previous state for comparison
  475. const prevState = JSON.parse(localStorage.getItem("previousState") || "{}");
  476. // Determine if we need to reset based on certain changes
  477. const shouldReset = !prevState.parties ||
  478. // state.parties != prevState.parties || // Part Changes
  479. !prevState.parties.every((party) => state.parties.includes(party)) || // Do not reset if a party was added
  480. state.parties.length < prevState.parties.length || // Party unselected
  481. state.donors.length < prevState.donors.length || // Donor removed
  482. state.donorType !== prevState.donorType; // Donor type changed
  483. updateVisualization(shouldReset);
  484. // Save current state as previous state
  485. localStorage.setItem("previousState", JSON.stringify(state));
  486. });
  487. // Toggle sidebar functionality
  488. toggleButton.addEventListener("click", () => {
  489. sidebar.classList.toggle("hidden");
  490. });
  491. // Event listeners for non-Alpine elements
  492. document.getElementById("toggleSidebar").addEventListener("click", () => {
  493. document.getElementById("sidebar").classList.toggle("hidden");
  494. });
  495. const observer = new MutationObserver((mutations) => {
  496. mutations.forEach((mutation) => {
  497. if (mutation.target.classList.contains("translate-x-full")) {
  498. toggleButton.style.display = "block";
  499. } else {
  500. toggleButton.style.display = "none";
  501. }
  502. });
  503. });
  504. observer.observe(explainer, { attributes: true, attributeFilter: ["class"] });
  505. });
  506. // Initialize event listeners
  507. document.getElementById("toggleSidebar").addEventListener("click", () => {
  508. document.getElementById("sidebar").classList.toggle("hidden");
  509. });
  510. // TODO add tutorial
  511. // document.getElementById('startTutorial').addEventListener('click', startTutorial);
  512. // Fix hamburger visibility when explainer is open
  513. const explainer = document.getElementById("explainer");
  514. const toggleButton = document.getElementById("toggleSidebar");
  515. const observer = new MutationObserver((mutations) => {
  516. mutations.forEach((mutation) => {
  517. if (mutation.target.classList.contains("translate-x-full")) {
  518. toggleButton.style.display = "block";
  519. } else {
  520. toggleButton.style.display = "none";
  521. }
  522. });
  523. });
  524. observer.observe(explainer, { attributes: true, attributeFilter: ["class"] });
  525. // Fix filter positioning
  526. const sidebar = document.getElementById("sidebar");
  527. sidebar.style.top = "30px"; // Add space for the hamburger
  528. // Make explainer scrollable
  529. explainer.style.overflowY = "auto";
  530. // Initialize party select dropdown
  531. function initializePartySelect() {
  532. const select = document.getElementById("partySelect");
  533. parties_list.forEach((party) => {
  534. // console.log(party);
  535. const option = document.createElement("option");
  536. option.value = party.Name;
  537. // Check if the party has a logo
  538. if (party.preview) {
  539. option.innerHTML = `
  540. <div class="party-option flex items-center">
  541. <img src="${party.preview}" alt="${party.Name}" width="20" height="20" class="mr-2">
  542. <span>${party.Name}</span>
  543. </div>
  544. `;
  545. } else {
  546. option.innerHTML = `
  547. <div class="party-option">
  548. <span>${party.Name}</span>
  549. </div>
  550. `;
  551. }
  552. // option.style.backgroundColor = party.hex;
  553. select.appendChild(option);
  554. });
  555. }
  556. // FIXME add Tutorial
  557. // // Tutorial steps
  558. // const tutorialSteps = [
  559. // {
  560. // title: "Einzelne Partei",
  561. // config: {
  562. // parties: ["Bündnis 90/Die Grünen"],
  563. // donors: [],
  564. // strict: false
  565. // },
  566. // description: "Hier sehen Sie alle Spenden an die Grünen."
  567. // },
  568. // {
  569. // title: "Ampel Parteien",
  570. // config: {
  571. // parties: ["Bündnis 90/Die Grünen", "FDP", "SPD"],
  572. // donors: [],
  573. // strict: false
  574. // },
  575. // description: "Spenden an die Ampel-Koalition im Vergleich."
  576. // },
  577. // {
  578. // title: "Verundete Suche",
  579. // config: {
  580. // parties: ["CDU", "AFD"],
  581. // donors: [],
  582. // strict: true
  583. // },
  584. // description: "Spender, die sowohl an CDU als auch AFD gespendet haben."
  585. // }
  586. // ];
  587. //
  588. // let currentTutorialStep = 0;
  589. //
  590. // function startTutorial() {
  591. // showTutorialStep(0);
  592. // }
  593. //
  594. // function showTutorialStep(step) {
  595. // if (step >= tutorialSteps.length) {
  596. // endTutorial();
  597. // return;
  598. // }
  599. //
  600. // const tutorialConfig = tutorialSteps[step];
  601. // // TODO Update UI with tutorial configuration
  602. // // ... implementation details ...
  603. // updateVisualization();
  604. // }
  605. // Function below are used in the html part
  606. // Show/Hide Explainer
  607. function toggleExplainer() {
  608. const explainer = document.getElementById("explainer");
  609. explainer.classList.toggle("translate-x-full");
  610. }
  611. function closeExplainer() {
  612. document.getElementById("explainer").classList.add("translate-x-full");
  613. localStorage.setItem("explainerSeen", "true");
  614. }
  615. // Show/Hide Legend
  616. function toggleLegend() {
  617. const legend = document.getElementById("legend");
  618. legend.classList.toggle("hidden");
  619. }
  620. function toggleZeitraumInfo() {
  621. const zeitraumInfo = document.getElementById("zeitraum-info");
  622. zeitraumInfo.classList.toggle("hidden");
  623. }