diff --git a/index.html b/index.html index 02b6afc98221a1b036b47fa08bb2a54f93a62678..9e8e298ee6d1a0d002e36c582d47a04cc3f919be 100644 --- a/index.html +++ b/index.html @@ -15,15 +15,7 @@ <div id="time-series-container" class="visualization-container"> <div class="dropdown-container button"> - <select id="visa-type-selector" class="button-inner"> - <option value="Total uniform visas issued (including MEV)">Total Issued</option> - <option value="ATVs issued (including multiple)">ATV Issued</option> - <option value="Multiple entry uniform visas (MEVs) issued">MEV Issued</option> - <option value="Total LTVs issued">LTV Issued</option> - <option value="Uniform visas applied for">Total Applied for</option> - <option value="Airport transit visas (ATVs) applied for">ATV Applied for</option> - <option value="Multiple ATVs issued">MEV Applied for</option> - <option value="Total LTVs issued">LTV Applied for</option> + <select id="visa-type-selector-time" class="button-inner visa-type-autofill"> </select> </div> </div> @@ -37,48 +29,35 @@ </div> </div> -<div id="bubble-container" class="visualization-container"></div> +<div id="bubble-container" class="visualization-container"> + <div class="dropdown-container"> + <div class="button flex"> + <div class="button-text">Bubble Opacity: </div> + <select id="visa-type-selector-bubble-opacity" class="button-inner visa-type-autofill"> + </select> + </div> + <div class="button flex"> + <div class="button-text">Bubble Size: </div> + <select id="visa-type-selector-bubble-size" class="button-inner visa-type-autofill"> + </select> + </div> + </div> + +</div> <div id="bottom-spacer"></div> <div class="bottom-nav"> - <div id="tree-view" class="button-nav active">Tree View</div> + <div id="tree-view" class="button-nav active">Tree</div> <div id="time-series-view" class="button-nav">Time Series</div> <div id="parallel-view" class="button-nav">Parallel Coordinates</div> - <div id="bubble-view" class="button-nav">Bubble Chart</div> + <div id="bubble-view" class="button-nav">Bubbles</div> </div> <script src="modules/tree.js" type="module"></script> <script src="modules/time-series.js" type="module"></script> <script src="modules/parallel.js" type="module"></script> <script src="modules/bubble.js" type="module"></script> -<script> - const views = { - tree: document.getElementById('tree-container'), - timeSeries: document.getElementById('time-series-container'), - parallel: document.getElementById('parallel-coordinates-container'), - bubble: document.getElementById('bubble-container') - }; - - const buttons = { - tree: document.getElementById('tree-view'), - timeSeries: document.getElementById('time-series-view'), - parallel: document.getElementById('parallel-view'), - bubble: document.getElementById('bubble-view') - }; - - function showView(viewName) { - Object.values(views).forEach(view => view.classList.remove('visible')); - Object.values(buttons).forEach(button => button.classList.remove('active')); - - views[viewName].classList.add('visible'); - buttons[viewName].classList.add('active'); - } - - buttons.tree.addEventListener('click', () => showView('tree')); - buttons.timeSeries.addEventListener('click', () => showView('timeSeries')); - buttons.parallel.addEventListener('click', () => showView('parallel')); - buttons.bubble.addEventListener('click', () => showView('bubble')); -</script> +<script src="modules/main.js" type="module"></script> </body> </html> diff --git a/modules/bubble.js b/modules/bubble.js index e239cab929364ba24799eeb6f09bec4a6a52c77c..c3e508a22e37e774630c1d31c88006903b291053 100644 --- a/modules/bubble.js +++ b/modules/bubble.js @@ -1,12 +1,8 @@ -import {width, height, margin, colors} from "./constants.js"; -import {addLegend} from "./main.js"; +import {addLegend, addSvg, width, height as _height, margin, colors, loadData, getDimensions} from "./lib.js"; +const height = _height-105 -// Load the combined dataset -Promise.all([ - d3.csv("schengen_data.csv"), // Schengen data - d3.csv("world-data-2023.csv") // Country data -]).then(([schengenData1, countryData]) => { +loadData().then(([schengenData1, countryData]) => { // Map country data by country name for quick lookup const countryMap = {}; countryData.forEach(d => { @@ -23,108 +19,128 @@ Promise.all([ d.Population = countryInfo ? countryInfo.population : 0; }); - const schengenData = Array.from(d3.rollup( - schengenData1, - v => ({ - GDP: v[0].GDP, - Population: v[0].Population, - "Visas Issued": d3.sum(v, d => d["Total uniform visas issued (including MEV)"]), - "Visas Applied for": d3.sum(v, d => d["Total ATVs and uniform visas applied for"]) - }), - d => d["Schengen State"] - )).map(([key, value]) => ({ - "Schengen State": key, - GDP: value.GDP, - Population: value.Population, - "Visas Issued": value["Visas Issued"], - "Visas Applied for": value["Visas Applied for"] - })); - - console.log(schengenData); - - - const xScale = d3.scaleLog() - .domain([d3.min(schengenData, d => d.GDP), d3.max(schengenData, d => d.GDP)]) - .range([0, width-margin.left-margin.right-130]); - - const yScale = d3.scaleLog() - .domain([d3.min(schengenData, d => d.Population), d3.max(schengenData, d => d.Population)]) - .range([height-margin.top-margin.bottom-110, 0]); - - const sizeScale = d3.scaleSqrt() - .domain([0, d3.max(schengenData, d => d["Visas Issued"] || 1)]) - .range([8, 60]); - - const opacityScale = d3.scaleSqrt() - .domain([0, d3.max(schengenData, d => d["Visas Applied for"] || 1)]) - .range([0.1, 1]); - - //const colorScale = d3.scaleOrdinal(d3.schemeCategory10); - const colorScale = d3 - .scaleOrdinal() - .domain(schengenData) - .range(colors) - + let sizeDim; + let opacityDim; // Append SVG container - const svg = d3.select("#bubble-container") - .append("svg") - .attr("width", width) - .attr("height", height) - .append("g") - .attr("transform", `translate(${margin.left+80},${margin.top+50})`); - - // Add X axis - svg.append("g") - .attr("transform", `translate(0,${height-margin.top-margin.bottom-110})`) - .call(d3.axisBottom(xScale).ticks(10, ".1s")) - .append("text") - .attr("class", "axis-label") - .attr("transform", `translate(${(width-margin.left-margin.right-150)/2},50)`) - .text("GDP") - .style("fill", "black"); - - // Add Y axis - svg.append("g") - .call(d3.axisLeft(yScale)) - .append("text") - .attr("class", "axis-label") - //.attr("x", -height / 2) - .attr("y", -50) - .attr("transform", `rotate(-90) translate(${-(height-margin.top-margin.bottom-120)/2},-10)`) - .text("Population") - .style("fill", "black"); - - // Draw bubbles - const tooltip = d3.select("body") - .append("div") - .attr("class", "tooltip") - .style("visibility", "hidden"); - - svg.selectAll("circle") - .data(schengenData) - .enter() - .append("circle") - .attr("cx", d => xScale(d.GDP || 1e10)) - .attr("cy", d => yScale(d.Population || 1)) - .attr("r", d => Math.round(sizeScale(d["Visas Issued"] || 1))) - .style("opacity", d => opacityScale(d["Visas Applied for"] || 1)) - .attr("fill", d => colorScale(d["Schengen State"])) - .on("mouseover", (event, d) => { - tooltip.style("visibility", "visible") - .html(` + const svg = addSvg("#bubble-container", 0, 115) + + function updateBubble() { + console.log("dims", sizeDim, opacityDim) + const schengenData = Array.from(d3.rollup( + schengenData1, + v => ({ + GDP: v[0].GDP, + Population: v[0].Population, + "size": d3.sum(v, d => d[sizeDim]), + "opacity": d3.sum(v, d => d[opacityDim]) + }), + d => d["Schengen State"] + )).map(([key, value]) => ({ + "Schengen State": key, + GDP: value.GDP, + Population: value.Population, + "size": value["size"], + "opacity": value["opacity"] + })); + + console.log(schengenData) + + const xScale = d3.scaleLog() + .domain([d3.min(schengenData, d => d.GDP), d3.max(schengenData, d => d.GDP)]) + .range([0, width-margin.left-margin.right]); + + const yScale = d3.scaleLog() + .domain([d3.min(schengenData, d => d.Population), d3.max(schengenData, d => d.Population)]) + .range([height-margin.top-margin.bottom, 0]); + + const sizeScale = d3.scaleSqrt() + .domain([0, d3.max(schengenData, d => d["size"] || 1)]) + .range([8, 60]); + + const opacityScale = d3.scaleSqrt() + .domain([0, d3.max(schengenData, d => d["opacity"] || 1)]) + .range([0.1, 1]); + + //const colorScale = d3.scaleOrdinal(d3.schemeCategory10); + const colorScale = d3 + .scaleOrdinal() + .domain(schengenData) + .range(colors) + + svg.selectAll("*").remove(); + + // Add X axis + svg.append("g") + .attr("transform", `translate(0,${height-margin.top-margin.bottom})`) + .call(d3.axisBottom(xScale).ticks(10, ".1s")) + .append("text") + .attr("class", "axis-label") + .attr("transform", `translate(${(width-margin.left-margin.right)/2},50)`) + .text("GDP") + .style("fill", "black"); + + // Add Y axis + svg.append("g") + .call(d3.axisLeft(yScale)) + .append("text") + .attr("class", "axis-label") + //.attr("x", -height / 2) + .attr("y", -50) + .attr("transform", `rotate(-90) translate(${-(height-margin.top-margin.bottom)/2},-10)`) + .text("Population") + .style("fill", "black"); + + // Draw bubbles + const tooltip = d3.select("body") + .append("div") + .attr("class", "tooltip") + .style("visibility", "hidden"); + + svg.selectAll("circle") + .data(schengenData) + .enter() + .append("circle") + .attr("class", "circle") + .attr("cx", d => xScale(d.GDP || 1e10)) + .attr("cy", d => yScale(d.Population || 1)) + .attr("r", d => Math.round(sizeScale(d["size"] || 1))) + .style("opacity", d => opacityScale(d["opacity"] || 1)) + .attr("fill", d => colorScale(d["Schengen State"])) + .on("mouseover", (event, d) => { + tooltip.style("visibility", "visible") + .html(` <strong>${d["Schengen State"]}</strong><br> GDP: $${d.GDP.toLocaleString()}<br> Population: ${d.Population.toLocaleString()}<br> - Total Visas Issued: ${d["Visas Issued"].toLocaleString()} + ${sizeDim}: ${d["size"].toLocaleString()}<br> + ${opacityDim}: ${d["opacity"].toLocaleString()} `) - .style("left", `${event.pageX + 10}px`) - .style("top", `${event.pageY - 20}px`); - }) - .on("mouseout", () => { - tooltip.style("visibility", "hidden"); - }); - - const allStates = new Set(schengenData.map(d => d["Schengen State"])); - addLegend(svg, allStates, colorScale) + .style("left", `${event.pageX + 10}px`) + .style("top", `${event.pageY - 20}px`); + }) + .on("mouseout", () => { + tooltip.style("visibility", "hidden"); + }); + + const allStates = new Set(schengenData.map(d => d["Schengen State"])); + addLegend(svg, allStates, colorScale) + } + + const opacitySelector = document.getElementById("visa-type-selector-bubble-opacity") + opacitySelector.addEventListener("change", (event) => { + opacityDim = event.target.value; + updateBubble(); + }); + + const sizeSelector = document.getElementById("visa-type-selector-bubble-size") + sizeSelector.addEventListener("change", (event) => { + sizeDim = event.target.value; + updateBubble(); + }); + + const dims = getDimensions(schengenData1) + opacityDim = dims[0] + sizeDim = dims[0] + updateBubble() }); diff --git a/modules/constants.js b/modules/constants.js deleted file mode 100644 index 7ca34a6e7a7db214d392abc85be465cf8a14d761..0000000000000000000000000000000000000000 --- a/modules/constants.js +++ /dev/null @@ -1,6 +0,0 @@ -//const container = Array.from(document.querySelector('.visualization-container'))[0]; - -export const margin = { top: 20, right: 30, bottom: 50, left: 50 }; -export const width = window.innerWidth - margin.left - margin.right; -export const height = window.innerHeight - margin.top - margin.bottom; -export const colors = ["#a6cee3","#1f78b4","#b2df8a","#33a02c","#fb9a99","#e31a1c","#fdbf6f","#ff7f00","#cab2d6","#6a3d9a","#ffff99","#b15928","#8dd3c7","#ffffb3","#bebada","#fb8072","#80b1d3","#fdb462","#b3de69","#fccde5","#d9d9d9","#bc80bd","#ccebc5","#ffed6f"] \ No newline at end of file diff --git a/modules/country-legend.js b/modules/country-legend.js deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/modules/lib.js b/modules/lib.js new file mode 100644 index 0000000000000000000000000000000000000000..3615e598539cd2d75d6e4826b9d57d6fdfe81ec4 --- /dev/null +++ b/modules/lib.js @@ -0,0 +1,85 @@ +export function addLegend(svg, labelsSet, color) { + const labels = Array.from(labelsSet) + const legend = svg + .selectAll(".legend-group") + .data([null]) + .enter() + .append("g") + .attr("class", "legend-group") + .attr("transform", `translate(${width - 200}, 0)`) + .selectAll(".legend-item") + .data(labels) + .enter() + .append("g") + .attr("class", "legend-item") + .attr("transform", (d, i) => `translate(0, ${i * 20})`) + .each(function(d, i) { + const group = d3.select(this); + + group.append("rect") + .attr("width", 10) + .attr("height", 10) + .attr("fill", color(d)) + .on("mouseover", () => { + svg.selectAll(".line").classed("highlighted", line => line === d); + }) + .on("mouseout", () => { + svg.selectAll(".line").classed("highlighted", false); + }); + + group.append("text") + .attr("x", 20) + .attr("y", 10) + .attr("text-anchor", "start") + .style("text-transform", "capitalize") + .text(d); + }); + + const legendBBox = svg.select(".legend-group").node().getBBox() + + svg.select(".legend-group") + .insert("rect", ":first-child") + .attr("width", 140) + .attr("height", labels.length*20+20) + .attr("transform", `translate(-10, -10)`) + .attr("fill", "white") + .attr("fill-opacity", 0.7) + .attr("stroke", "#c6c6c6") + .attr("stroke-width", 2) + .attr("rx", 5) + .attr("ry", 5) +} + +export function addSvg(containerSelector, addX=0, addY=0) { + return d3.select(containerSelector) + .append("svg") + .attr("width", "100%") + .attr("height", "100%") + .append("g") + .attr("transform", `translate(${margin.left + 30+addX},${margin.top - 10+addY})`) +} + +export function getDimensions(data) { + return Object + .entries(data[0]) + .filter(([key, value])=>typeof value === "number" && key !== "Year") + .map(([key, value]) => key); +} + +//load schengen csv and country csv +let visaCsv +let countryCsv +export async function loadData() { + if (visaCsv&&countryCsv) { + return [visaCsv, countryCsv]; + } else { + visaCsv = await d3.csv("schengen_data.csv"); + countryCsv = await d3.csv("world-data-2023.csv"); + return [visaCsv, countryCsv]; + } +} + +export const margin = { top: 20, right: 30, bottom: 50, left: 50 }; +export const width = window.innerWidth - margin.left - margin.right; +export const height = window.innerHeight - margin.top - margin.bottom; +export const colors = ["#a6cee3","#1f78b4","#b2df8a","#33a02c","#fb9a99","#e31a1c","#fdbf6f","#ff7f00","#cab2d6","#6a3d9a","#ffff99","#b15928","#8dd3c7","#ffffb3","#bebada","#fb8072","#80b1d3","#fdb462","#b3de69","#fccde5","#d9d9d9","#bc80bd","#ccebc5","#ffed6f"] \ No newline at end of file diff --git a/modules/main.js b/modules/main.js index 9fbc9bcd58f5daa7f5b54c3011f90a7befb97aa4..b828901f7461daea2b04d27f30ac68aa89a1ad51 100644 --- a/modules/main.js +++ b/modules/main.js @@ -1,63 +1,49 @@ -import {margin, height, width} from "./constants.js"; - - -export function addLegend(svg, labelsSet, color) { - const labels = Array.from(labelsSet) - const legend = svg - .selectAll(".legend-group") - .data([null]) - .enter() - .append("g") - .attr("class", "legend-group") - .attr("transform", `translate(${width - 200}, 0)`) - .selectAll(".legend-item") - .data(labels) - .enter() - .append("g") - .attr("class", "legend-item") - .attr("transform", (d, i) => `translate(0, ${i * 20})`) - .each(function(d, i) { - const group = d3.select(this); - - group.append("rect") - .attr("width", 10) - .attr("height", 10) - .attr("fill", color(d)) - .on("mouseover", () => { - svg.selectAll(".line").classed("highlighted", line => line === d); - }) - .on("mouseout", () => { - svg.selectAll(".line").classed("highlighted", false); - }); - - group.append("text") - .attr("x", 20) - .attr("y", 10) - .attr("text-anchor", "start") - .style("text-transform", "capitalize") - .text(d); +import {getDimensions, loadData} from "./lib.js"; + +window.addEventListener('load', () => { + // navigation + const views = { + tree: document.getElementById('tree-container'), + timeSeries: document.getElementById('time-series-container'), + parallel: document.getElementById('parallel-coordinates-container'), + bubble: document.getElementById('bubble-container') + }; + + const buttons = { + tree: document.getElementById('tree-view'), + timeSeries: document.getElementById('time-series-view'), + parallel: document.getElementById('parallel-view'), + bubble: document.getElementById('bubble-view') + }; + + function showView(viewName) { + Object.values(views).forEach(view => view.classList.remove('visible')); + Object.values(buttons).forEach(button => button.classList.remove('active')); + + views[viewName].classList.add('visible'); + buttons[viewName].classList.add('active'); + } + + buttons.tree.addEventListener('click', () => showView('tree')); + buttons.timeSeries.addEventListener('click', () => showView('timeSeries')); + buttons.parallel.addEventListener('click', () => showView('parallel')); + buttons.bubble.addEventListener('click', () => showView('bubble')); + + + //data filter buttons + const selectors = Array.from(document.querySelectorAll('.visa-type-autofill')); + console.log(selectors); + loadData().then(([schengenCsv, countryCsv])=>{ + const dimensions = getDimensions(schengenCsv) + console.log(dimensions) + selectors.map(selector => { + dimensions.forEach(dimension => { + const optionEl = document.createElement('option'); + optionEl.setAttribute("value", dimension); + optionEl.textContent = dimension; + selector.append(optionEl); + }) }); + }) - const legendBBox = svg.select(".legend-group").node().getBBox() - - svg.select(".legend-group") - .insert("rect", ":first-child") - .attr("width", 140) - .attr("height", labels.length*20+20) - .attr("transform", `translate(-10, -10)`) - .attr("fill", "white") - .attr("fill-opacity", 0.7) - .attr("stroke", "#c6c6c6") - .attr("stroke-width", 2) - .attr("rx", 5) - .attr("ry", 5) -} - -export function addSvg(containerSelector) { - return d3.select(containerSelector) - .append("svg") - .attr("width", "100%") - .attr("height", "100%") - .append("g") - .attr("transform", `translate(${margin.left + 30},${margin.top - 10})`) -} \ No newline at end of file +}); \ No newline at end of file diff --git a/modules/parallel.js b/modules/parallel.js index 8a8ebbf8f73447c175f678b06b0dc4cc8f606054..42e57104c35164be9a2d9d994987ed837511924b 100644 --- a/modules/parallel.js +++ b/modules/parallel.js @@ -1,7 +1,8 @@ -import {width, height, margin} from "./constants.js"; -import {addSvg} from "./main.js"; +import {addSvg, width, height as _height, margin, colors, getDimensions, loadData} from "./lib.js"; -const svg = addSvg("#parallel-coordinates-container") +const height = _height-120 + +const svg = addSvg("#parallel-coordinates-container", 0, 60) const loadingIndicator = d3.select("#loading"); @@ -17,7 +18,7 @@ function hideLoading() { // Load data showLoading(); -d3.csv("schengen_data.csv").then((data) => { +loadData().then(([data,_]) => { // Parse data data.forEach(d => { for (const key in d) { @@ -27,13 +28,7 @@ d3.csv("schengen_data.csv").then((data) => { } }); - // Get data keys with numeric value - const dimensions = Object - .entries(data[0]) - .filter(([key, value])=>typeof value === "number" && key !== "Year") - .map(([key, value]) => key); - console.log(data); - console.log(dimensions); + const dimensions = getDimensions(data) const yScales = {}; const epsilon = 1; // Small positive constant to avoid log(0) @@ -53,11 +48,7 @@ d3.csv("schengen_data.csv").then((data) => { const lineGenerator = d3.line() .defined(d => !isNaN(d[1])) .x(d => xScale(d[0])) - .y(d => yScales[d[0]](d[1] + epsilon)); // Apply epsilon adjustment here - - - - + .y(d => yScales[d[0]](d[1] + epsilon)); // epsilon to avoid log(0) // Group data by state const groupedData = d3.group(data, d => d["Schengen State"]); @@ -115,7 +106,7 @@ d3.csv("schengen_data.csv").then((data) => { .attr("x", xScale(dim) - 30) // Adjust x position .attr("y", height - margin.top - margin.bottom-20) // Adjust y position .attr("width", 60) // Set width for wrapping - .attr("height", 50) // Set height for the container + .attr("height", 140) // Set height for the container .append("xhtml:div") // Add a div for HTML content .style("font-size", "12px") .style("text-align", "center") diff --git a/modules/time-series.js b/modules/time-series.js index 4678c95416ec6e82251802d1ae9ef5c5b22622ca..9b9d3c6b7672f41941280faa4142f89ab4996d50 100644 --- a/modules/time-series.js +++ b/modules/time-series.js @@ -1,15 +1,14 @@ -import {width, height, margin, colors} from "./constants.js"; -import {addLegend, addSvg} from "./main.js"; +import {addLegend, addSvg, width, height as _height, margin, colors, loadData} from "./lib.js"; - -d3.csv("schengen_data.csv").then((data) => { +const height = _height-50 +loadData().then(([data,_]) => { const allStates = new Set(data.map(d => d["Schengen State"])); const color = d3 .scaleOrdinal() .domain(data) .range(colors) - const svg = addSvg("#time-series-container") + const svg = addSvg("#time-series-container",20,70) function updateGraph(type) { const processedData = d3.rollups( @@ -74,7 +73,7 @@ d3.csv("schengen_data.csv").then((data) => { addLegend(svg, allStates, color) } - document.getElementById("visa-type-selector").addEventListener("change", (event) => { + document.getElementById("visa-type-selector-time").addEventListener("change", (event) => { const type = event.target.value; updateGraph(type); }); diff --git a/modules/tree.js b/modules/tree.js index 1a5e532685da477b6872a78d3f409265bc5914b4..ec51f8622c201ca7877ddf79a6f525284965b900 100644 --- a/modules/tree.js +++ b/modules/tree.js @@ -1,12 +1,11 @@ -import {width, height, margin} from "./constants.js"; -import {addSvg} from "./main.js"; +import {addSvg, width, height, margin, loadData} from "./lib.js"; let currentData; let hierarchyStack = ["Schengen States"]; let treeHistory = []; -d3.csv("schengen_data.csv").then((data) => { +loadData().then(([data,_]) => { currentData = data; updateTree("Schengen States"); }); @@ -48,7 +47,6 @@ function updateTree(filterKey) { // Build hierarchy const rootData = buildHierarchy(filteredData, filterKey); const treeData = insertHistory(treeHistory, rootData); - console.log(treeData) const root = d3.hierarchy(treeData); // Funktion, um Skalierungen für finale Knoten pro Ebene zu definieren @@ -168,7 +166,6 @@ function buildHierarchy(nest, name) { return { name, children: Array.from(nest, ([key, value]) => { - console.log([key, value]) if (!key) return null; return value instanceof Map ? buildHierarchy(value, key) diff --git a/style.css b/style.css index b71d382013faa80ed93bea2a9dc631086abe2f1c..420f94df2a39824dd51585f578ab5a66efb3e640 100644 --- a/style.css +++ b/style.css @@ -151,4 +151,15 @@ html, body, #tree-container, #time-series-container, #bubble-container, #paralle } .bottom-nav .button-nav.active { border-top: #4698ff solid 4px; +} + +.flex { + display: flex; + flex-direction: row; +} + +.button-text { + flex-grow: 1; + align-self: center; + margin-right: 10px; } \ No newline at end of file