123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671 |
- const parties_list = [
- {
- "Name": "CDU",
- "comment": null,
- "highres":
- "https://de.wikipedia.org/wiki/Datei:CDU_flagge_logo_alternativ.svg",
- "preview":
- "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0e/CDU_Logo_2023.svg/330px-CDU_Logo_2023.svg.png",
- "color": "Black",
- "hex": "#000000",
- },
- {
- "Name": "AFD",
- "comment": " Duplicate of Alternative für Deutschland (Afd)",
- "highres":
- "https://upload.wikimedia.org/wikipedia/commons/b/b1/AfD_Logo_2021.svg?download",
- "preview":
- "https://upload.wikimedia.org/wikipedia/commons/thumb/4/48/AfD-Logo-2017.svg/240px-AfD-Logo-2017.svg.png",
- "color": "Dark Blue",
- "hex": "#003A70",
- },
- {
- "Name": "SPD",
- "comment": null,
- "highres": "https://de.wikipedia.org/wiki/Datei:SPD_Hessen.svg",
- "preview":
- "https://upload.wikimedia.org/wikipedia/de/thumb/c/ca/SPD_Hessen.svg/240px-SPD_Hessen.svg.png",
- "color": "Red",
- "hex": "#E3000F",
- },
- {
- "Name": "Bündnis 90/Die Grünen",
- "comment": null,
- "highres":
- "https://de.wikipedia.org/wiki/Datei:B%C3%BCndnis_90_Die_Gr%C3%BCnen.svg",
- "preview":
- "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",
- "color": "Green",
- "hex": "#00A950",
- },
- {
- "Name": "LINKE",
- "comment": null,
- "highres":
- "https://www.energie-klimaschutz.de/wp-content/uploads/2017/05/Die-Linke_DEZ_20170503.jpg",
- "preview":
- "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d6/Logo_Die_Linke_%282023%29.svg/450px-Logo_Die_Linke_%282023%29.svg.png",
- "color": "Maroon",
- "hex": "#A30000",
- },
- {
- "Name": "CSU",
- "comment": null,
- "highres": "https://de.wikipedia.org/wiki/Datei:CSU_Logo_since_2016.svg",
- "preview":
- "https://upload.wikimedia.org/wikipedia/commons/thumb/e/ee/CSU_Logo_since_2016.svg/330px-CSU_Logo_since_2016.svg.png",
- "color": "Blue",
- "hex": "#007AC2",
- },
- {
- "Name": "FDP",
- "comment": null,
- "highres": "https://de.wikipedia.org/wiki/Datei:FDP_Sachsen-Anhalt.svg",
- "preview":
- "https://upload.wikimedia.org/wikipedia/de/thumb/e/e8/FDP_Sachsen-Anhalt.svg/240px-FDP_Sachsen-Anhalt.svg.png",
- "color": "Yellow",
- "hex": "#FFED00",
- },
- {
- "Name": "SSW",
- "comment": null,
- "highres":
- "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",
- "preview":
- "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",
- "color": "Yellow",
- "hex": "#FFD700",
- },
- {
- "Name": "BSW - Bündnis Sarah Wagenknecht",
- "comment": null,
- "highres":
- "https://de.wikipedia.org/wiki/Datei:B%C3%BCndnis_Sahra_Wagenknecht_logo_2.svg",
- "preview":
- "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",
- "color": "Deep Purple",
- "hex": "#660099",
- },
- {
- "Name": "Volt",
- "comment": null,
- "highres": "https://de.wikipedia.org/wiki/Datei:Logo_of_Volt.svg",
- "preview":
- "https://upload.wikimedia.org/wikipedia/commons/thumb/6/68/Logo_of_Volt.svg/600px-Logo_of_Volt.svg.png",
- "color": "Purple",
- "hex": "#7F3FBF",
- },
- {
- "Name": "MLPD",
- "comment": null,
- "highres": "https://de.wikipedia.org/wiki/Datei:MLPD_Logo_2011_(2).svg",
- "preview":
- "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b1/MLPD_Logo_2011_%282%29.svg/300px-MLPD_Logo_2011_%282%29.svg.png",
- "color": "Red",
- "hex": "#FF0000",
- },
- {
- "Name": "DKP",
- "comment": null,
- "highres":
- "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c5/Deutsche_Kommunistische_Partei_Logo.svg/500px-Deutsche_Kommunistische_Partei_Logo.svg.png",
- "preview":
- "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c5/Deutsche_Kommunistische_Partei_Logo.svg/240px-Deutsche_Kommunistische_Partei_Logo.svg.png",
- "color": "Red",
- "hex": "#FF0000",
- },
- {
- "Name": "Team Todenhöfer",
- "comment": null,
- "highres":
- "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d4/Gerechtigkeitsparteilogo.png/500px-Gerechtigkeitsparteilogo.png",
- "preview":
- "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d4/Gerechtigkeitsparteilogo.png/240px-Gerechtigkeitsparteilogo.png",
- "color": "Teal",
- "hex": "#008080",
- },
- {
- "Name": "Piraten",
- "comment": null,
- "highres": "https://de.wikipedia.org/wiki/Datei:Pidataratpartiet.svg",
- "preview":
- "https://upload.wikimedia.org/wikipedia/commons/thumb/8/8c/Piratenpartei_deutschland_logo.svg/640px-Piratenpartei_deutschland_logo.svg.png?download",
- "color": "Purple",
- "hex": "#6600CC",
- },
- {
- "Name": "ÖDP",
- "comment": null,
- "highres":
- "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3e/OEDP_Logo_CMYK.svg/300px-OEDP_Logo_CMYK.svg.png",
- "preview":
- "https://upload.wikimedia.org/wikipedia/commons/3/3e/OEDP_Logo_CMYK.svg",
- "color": "Green",
- "hex": "#008000",
- },
- {
- "Name": "PARTEI",
- "comment": null,
- "highres":
- "https://de.wikipedia.org/wiki/Datei:Die_PARTEI_Logo_Blur_Schatten_Vektor_V2.svg",
- "preview":
- "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",
- "color": "Pink",
- "hex": "#FF69B4",
- },
- {
- "Name": "FW",
- "comment": null,
- "highres": "https://de.wikipedia.org/wiki/Datei:Freie_Waehler_Logo.svg",
- "preview":
- "https://upload.wikimedia.org/wikipedia/de/thumb/a/a6/Freie_Waehler_Logo.svg/240px-Freie_Waehler_Logo.svg.png",
- "color": "Orange",
- "hex": "#FF7F00",
- },
- {
- "Name": "Bayernpartei",
- "comment": null,
- "highres": null,
- "preview":
- "https://upload.wikimedia.org/wikipedia/commons/thumb/4/48/Bayernpartei_Logo.svg/270px-Bayernpartei_Logo.svg.png",
- "color": "Blue",
- "hex": "#0000FF",
- },
- {
- "Name": "Freie Wähler",
- "comment": null,
- "highres": "https://de.wikipedia.org/wiki/Datei:Freie_Waehler_Logo.svg",
- "preview":
- "https://upload.wikimedia.org/wikipedia/de/thumb/a/a6/Freie_Waehler_Logo.svg/240px-Freie_Waehler_Logo.svg.png",
- "color": "Orange",
- "hex": "#FF7F00",
- },
- {
- "Name": "NPD",
- "comment": "Umbenannt zu Heimat",
- "highres":
- "https://upload.wikimedia.org/wikipedia/commons/a/a2/Heimat-Logo.png",
- "preview":
- "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a2/Heimat-Logo.png/240px-Heimat-Logo.png",
- "color": "Dark Blue",
- "hex": "#00008B",
- },
- {
- "Name": "BVB/Freie Wähler",
- "comment": "Duplicate of “Freie Wähler”",
- "highres": null,
- "preview":
- "https://upload.wikimedia.org/wikipedia/de/thumb/a/a6/Freie_Waehler_Logo.svg/240px-Freie_Waehler_Logo.svg.png",
- "color": "Orange",
- "hex": "#FF7F00",
- },
- {
- "Name": "WerteUnion",
- "comment": null,
- "highres": "https://de.wikipedia.org/wiki/Datei:WerteUnion_Logo.svg",
- "preview":
- "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e2/WerteUnion_Logo.svg/640px-WerteUnion_Logo.svg.png",
- "color": "Dark Blue",
- "hex": "#00008B",
- },
- ];
- const URI = "localhost";
- const PORT = 7687;
- const DEFAULT_YEAR_RANGE = [2021, 2025];
- const DEFAULT_LIMIT = 250;
- const NEO4J_CONFIG = {
- serverUrl: `bolt://${URI}:${PORT}`,
- serverUser: "neo4j",
- serverPassword: "lkjk3jlksfjlk34lkjrs432",
- };
- const BASE_CONFIG = {
- containerId: "viz",
- neo4j: NEO4J_CONFIG,
- labels: {
- visConfig: {
- nodes: {},
- edges: {
- length: 100,
- arrows: {
- to: { enabled: true },
- },
- },
- physics: {
- hierarchicalRepulsion: {
- avoidOverlap: 1,
- },
- solver: "repulsion",
- repulsion: {
- nodeDistance: 1500,
- },
- },
- layout: {
- improvedLayout: true,
- randomSeed: 420,
- hierarchical: {
- enabled: true,
- direction: "DU",
- sortMethod: "directed",
- nodeSpacing: 1000,
- treeSpacing: 20,
- levelSeparation: 250,
- },
- },
- },
- "Party": {
- label: "name",
- value: "pagerank",
- group: "community",
- [NeoVis.NEOVIS_ADVANCED_CONFIG]: {
- cypher: {
- value:
- "MATCH (n) WHERE id(n) = $id MATCH ()-[r]-(n) RETURN sum(r.amount) AS c",
- },
- function: {
- // color: (node) => ({
- // background: node.properties.hex || "#CCCCCC",
- // border: node.properties.hex || "#CCCCCC",
- // }),
- title: (props) =>
- NeoVis.objectToTitleHtml(props, ["name", "pagerank", "community"]),
- image: (node) => node.properties.preview,
- },
- static: {
- font: {
- size: 15,
- color: "#000000", // Font color
- },
- shape: "image", // Optional: specify shape
- size: 30, // Node size
- },
- },
- },
- "Person": {
- label: "name",
- value: "pagerank",
- group: "community",
- [NeoVis.NEOVIS_ADVANCED_CONFIG]: {
- cypher: {
- value:
- "MATCH (n) WHERE id(n) = $id MATCH (n)-[r]-() RETURN sum(r.amount) AS c",
- },
- function: {
- title: (props) =>
- NeoVis.objectToTitleHtml(props, ["name", "pagerank", "community"]),
- },
- static: {
- font: {
- size: 20,
- color: "#000000",
- },
- shape: "dot",
- // color: {
- // background: "#33FF57",
- // border: "#39C700",
- // },
- size: 25,
- },
- },
- },
- "Entity": {
- value: "pagerank",
- label: "name",
- group: "community",
- [NeoVis.NEOVIS_ADVANCED_CONFIG]: {
- cypher: {
- value:
- "MATCH (n) WHERE id(n) = $id MATCH (n)-[r]-() RETURN sum(r.amount) AS c",
- },
- function: {
- title: (props) =>
- NeoVis.objectToTitleHtml(props, ["name", "pagerank", "community"]),
- },
- static: {
- font: {
- size: 18,
- // color: "#000000",
- },
- shape: "dot",
- // color: {
- // background: "#3357FF",
- // border: "#0039C7",
- // },
- size: 20,
- },
- },
- },
- },
- relationships: {
- "DONATION": {
- value: "amount",
- group: "community",
- [NeoVis.NEOVIS_ADVANCED_CONFIG]: {
- function: {
- title: NeoVis.objectToTitleHtml,
- },
- },
- },
- },
- };
- // Alpine.js data store
- document.addEventListener("alpine:init", () => {
- Alpine.store("filter", {
- parties: parties_list,
- donors: [""],
- yearRange: [DEFAULT_YEAR_RANGE[0], DEFAULT_YEAR_RANGE[1]],
- donorType: "all",
- strict: true,
- limit: DEFAULT_LIMIT,
- addDonorField() {
- this.donors.push("");
- },
- removeDonorField(index) {
- this.donors.splice(index, 1);
- },
- setDonorType(type) {
- this.donorType = type;
- },
- updateYearRange(start, end) {
- this.yearRange = [
- Math.min(parseInt(start), parseInt(end)),
- Math.max(parseInt(start), parseInt(end)),
- ];
- },
- });
- });
- class QueryBuilder {
- buildQuery(params = {}) {
- const {
- parties = [],
- donors = [],
- yearRange = DEFAULT_YEAR_RANGE,
- strict = true,
- donorType = "all",
- limit = DEFAULT_LIMIT,
- } = params;
- let donorMatch = "(n)";
- if (donorType === "person") {
- donorMatch = "(n:Person)";
- } else if (donorType === "entity") {
- donorMatch = "(n:Entity)";
- }
- const whereConditions = [];
- // Add party conditions only if not all parties are selected
- if (parties.length > 0 && parties.length < parties_list.length) {
- const partyCondition = parties.map((p) => `m.name = '${p}'`).join(" OR ");
- whereConditions.push(`(${partyCondition})`);
- }
- // Add donor name conditions
- if (donors.length > 0) {
- const validDonors = donors.filter(Boolean);
- if (validDonors.length > 0) {
- const donorCondition = validDonors.map((d) => `n.name CONTAINS '${d}'`)
- .join(" OR ");
- whereConditions.push(`(${donorCondition})`);
- }
- }
- // Add year range condition - always included and connected with AND
- const yearCondition = `(r.year >= ${yearRange[0]} AND r.year <= ${
- yearRange[1]
- })`;
- // Build WHERE clause
- let whereClause = "";
- if (whereConditions.length > 0) {
- const otherConditions = whereConditions.join(strict ? " AND " : " OR ");
- whereClause = `WHERE (${otherConditions}) AND ${yearCondition}`;
- } else {
- whereClause = `WHERE ${yearCondition}`;
- }
- return `
- MATCH ${donorMatch}-[r:DONATION]->(m:Party)
- ${whereClause}
- RETURN n, r, m
- ORDER BY r.amount DESC
- LIMIT ${limit}
- `.trim();
- }
- }
- // UI State Management
- let viz;
- const queryBuilder = new QueryBuilder();
- function initializeVisualization() {
- viz = new NeoVis.default({
- ...BASE_CONFIG,
- initialCypher: queryBuilder.buildQuery(),
- });
- viz.render();
- }
- // UI State Management
- function updateVisualization(reset = true) {
- const filterState = Alpine.store("filter");
- const query = queryBuilder.buildQuery({
- parties: filterState.parties,
- donors: filterState.donors.filter(Boolean),
- yearRange: filterState.yearRange,
- strict: filterState.strict,
- donorType: filterState.donorType,
- limit: filterState.limit,
- });
- if (reset) {
- viz.clearNetwork();
- }
- viz.updateWithCypher(query);
- console.log("Query", query, reset);
- }
- // Initialize application
- document.addEventListener("DOMContentLoaded", async () => {
- // Observe explainer visibility
- const explainer = document.getElementById("explainer");
- const toggleButton = document.getElementById("toggleSidebar");
- const sidebar = document.getElementById("sidebar");
- initializeVisualization();
- initializePartySelect();
- // Event listeners for filter changes
- Alpine.effect(() => {
- // Check for saved state
- const savedState = localStorage.getItem("visualizationState");
- if (savedState) {
- // Show explainer when there is no state
- document.getElementById("explainer").classList.add("translate-x-full");
- }
- // Save State
- const state = Alpine.store("filter");
- localStorage.setItem("visualizationState", JSON.stringify(state));
- // Track previous state for comparison
- const prevState = JSON.parse(localStorage.getItem("previousState") || "{}");
- // Determine if we need to reset based on certain changes
- const shouldReset = !prevState.parties ||
- // state.parties != prevState.parties || // Part Changes
- !prevState.parties.every((party) => state.parties.includes(party)) || // Do not reset if a party was added
- state.parties.length < prevState.parties.length || // Party unselected
- state.donors.length < prevState.donors.length || // Donor removed
- state.donorType !== prevState.donorType; // Donor type changed
- updateVisualization(shouldReset);
- // Save current state as previous state
- localStorage.setItem("previousState", JSON.stringify(state));
- });
- // Toggle sidebar functionality
- toggleButton.addEventListener("click", () => {
- sidebar.classList.toggle("hidden");
- });
- // Event listeners for non-Alpine elements
- document.getElementById("toggleSidebar").addEventListener("click", () => {
- document.getElementById("sidebar").classList.toggle("hidden");
- });
- const observer = new MutationObserver((mutations) => {
- mutations.forEach((mutation) => {
- if (mutation.target.classList.contains("translate-x-full")) {
- toggleButton.style.display = "block";
- } else {
- toggleButton.style.display = "none";
- }
- });
- });
- observer.observe(explainer, { attributes: true, attributeFilter: ["class"] });
- });
- // Initialize event listeners
- document.getElementById("toggleSidebar").addEventListener("click", () => {
- document.getElementById("sidebar").classList.toggle("hidden");
- });
- // TODO add tutorial
- // document.getElementById('startTutorial').addEventListener('click', startTutorial);
- // Fix hamburger visibility when explainer is open
- const explainer = document.getElementById("explainer");
- const toggleButton = document.getElementById("toggleSidebar");
- const observer = new MutationObserver((mutations) => {
- mutations.forEach((mutation) => {
- if (mutation.target.classList.contains("translate-x-full")) {
- toggleButton.style.display = "block";
- } else {
- toggleButton.style.display = "none";
- }
- });
- });
- observer.observe(explainer, { attributes: true, attributeFilter: ["class"] });
- // Fix filter positioning
- const sidebar = document.getElementById("sidebar");
- sidebar.style.top = "30px"; // Add space for the hamburger
- // Make explainer scrollable
- explainer.style.overflowY = "auto";
- // Initialize party select dropdown
- function initializePartySelect() {
- const select = document.getElementById("partySelect");
- parties_list.forEach((party) => {
- // console.log(party);
- const option = document.createElement("option");
- option.value = party.Name;
- // Check if the party has a logo
- if (party.preview) {
- option.innerHTML = `
- <div class="party-option flex items-center">
- <img src="${party.preview}" alt="${party.Name}" width="20" height="20" class="mr-2">
- <span>${party.Name}</span>
- </div>
- `;
- } else {
- option.innerHTML = `
- <div class="party-option">
- <span>${party.Name}</span>
- </div>
- `;
- }
- // option.style.backgroundColor = party.hex;
- select.appendChild(option);
- });
- }
- // FIXME add Tutorial
- // // Tutorial steps
- // const tutorialSteps = [
- // {
- // title: "Einzelne Partei",
- // config: {
- // parties: ["Bündnis 90/Die Grünen"],
- // donors: [],
- // strict: false
- // },
- // description: "Hier sehen Sie alle Spenden an die Grünen."
- // },
- // {
- // title: "Ampel Parteien",
- // config: {
- // parties: ["Bündnis 90/Die Grünen", "FDP", "SPD"],
- // donors: [],
- // strict: false
- // },
- // description: "Spenden an die Ampel-Koalition im Vergleich."
- // },
- // {
- // title: "Verundete Suche",
- // config: {
- // parties: ["CDU", "AFD"],
- // donors: [],
- // strict: true
- // },
- // description: "Spender, die sowohl an CDU als auch AFD gespendet haben."
- // }
- // ];
- //
- // let currentTutorialStep = 0;
- //
- // function startTutorial() {
- // showTutorialStep(0);
- // }
- //
- // function showTutorialStep(step) {
- // if (step >= tutorialSteps.length) {
- // endTutorial();
- // return;
- // }
- //
- // const tutorialConfig = tutorialSteps[step];
- // // TODO Update UI with tutorial configuration
- // // ... implementation details ...
- // updateVisualization();
- // }
- // Function below are used in the html part
- // Show/Hide Explainer
- function toggleExplainer() {
- const explainer = document.getElementById("explainer");
- explainer.classList.toggle("translate-x-full");
- }
- function closeExplainer() {
- document.getElementById("explainer").classList.add("translate-x-full");
- localStorage.setItem("explainerSeen", "true");
- }
- // Show/Hide Legend
- function toggleLegend() {
- const legend = document.getElementById("legend");
- legend.classList.toggle("hidden");
- }
- function toggleZeitraumInfo() {
- const zeitraumInfo = document.getElementById("zeitraum-info");
- zeitraumInfo.classList.toggle("hidden");
- }
|