diff --git a/index.html b/index.html index 555b1394946de0c08c4f3a4a70d18fbdf08ae749..02b6afc98221a1b036b47fa08bb2a54f93a62678 100644 --- a/index.html +++ b/index.html @@ -3,18 +3,17 @@ <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>Schengen Visa Tree Visualization</title> + <title>Schengen Visa Visualizations</title> <script src="https://d3js.org/d3.v7.min.js"></script> <link rel="stylesheet" href="style.css"> </head> <body> -<div class="visualization-container"> - <button id="back-button" class="button button-inner" disabled>Back</button> - <div id="tree-container"></div> +<div id="tree-container" class="visualization-container visible"> + <button id="back-button" class="button button-inner" disabled>Back</button> </div> -<div class="visualization-container"> +<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> @@ -27,30 +26,59 @@ <option value="Total LTVs issued">LTV Applied for</option> </select> </div> - - <div id="time-series-container"></div> </div> -<div class="visualization-container"> +<div id="parallel-coordinates-container" class="visualization-container"> <div id="loading">Loading...</div> <div class="dropdown-container button"> <select id="state-dropdown" class="button-inner"> <option value="">All States</option> </select> </div> - <div id="parallel-coordinates-container"></div> </div> -<div class="visualization-container"> - <div id="bubble-container"></div> -</div> +<div id="bubble-container" class="visualization-container"></div> <div id="bottom-spacer"></div> +<div class="bottom-nav"> + <div id="tree-view" class="button-nav active">Tree View</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> <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> </body> -</html> \ No newline at end of file +</html> diff --git a/modules/bubble.js b/modules/bubble.js index ed711a803d72f6721b4d6976550b48bdae9729b2..e239cab929364ba24799eeb6f09bec4a6a52c77c 100644 --- a/modules/bubble.js +++ b/modules/bubble.js @@ -1,5 +1,5 @@ import {width, height, margin, colors} from "./constants.js"; -import {legend} from "./country-legend.js"; +import {addLegend} from "./main.js"; // Load the combined dataset @@ -126,5 +126,5 @@ Promise.all([ }); const allStates = new Set(schengenData.map(d => d["Schengen State"])); - legend(svg, allStates, colorScale) + addLegend(svg, allStates, colorScale) }); diff --git a/modules/country-legend.js b/modules/country-legend.js index df205421c844d023faa969b0227d58a052b2c3b2..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 --- a/modules/country-legend.js +++ b/modules/country-legend.js @@ -1,53 +0,0 @@ -import {width, height} from "./constants.js"; - - -export function legend(svg, allStates, color) { - 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(Array.from(allStates)) - .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", legendBBox.width+20) - .attr("height", legendBBox.height+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) -} \ No newline at end of file diff --git a/modules/main.js b/modules/main.js new file mode 100644 index 0000000000000000000000000000000000000000..9fbc9bcd58f5daa7f5b54c3011f90a7befb97aa4 --- /dev/null +++ b/modules/main.js @@ -0,0 +1,63 @@ +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); + }); + + 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 diff --git a/modules/parallel.js b/modules/parallel.js index 66a71919f0a2a0f1df29251c53d66c0477e2a655..8a8ebbf8f73447c175f678b06b0dc4cc8f606054 100644 --- a/modules/parallel.js +++ b/modules/parallel.js @@ -1,12 +1,7 @@ import {width, height, margin} from "./constants.js"; +import {addSvg} from "./main.js"; -// Append SVG -const svg = d3.select("#parallel-coordinates-container") - .append("svg") - .attr("width", width) - .attr("height", height) - .append("g") - .attr("transform", `translate(${margin.left+margin.right-10},${margin.top+margin.bottom})`); +const svg = addSvg("#parallel-coordinates-container") const loadingIndicator = d3.select("#loading"); @@ -32,46 +27,36 @@ d3.csv("schengen_data.csv").then((data) => { } }); - // Define dimensions and scales - const dimensions = [ - "Airport transit visas (ATVs) applied for", - "ATVs issued (including multiple)", - "Not issued rate for ATVs", - "Uniform visas applied for", - "Total uniform visas issued (including MEV)", - "Not issued rate for uniform visas" - ]; + // 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 yScales = {}; + const epsilon = 1; // Small positive constant to avoid log(0) dimensions.forEach(dim => { - yScales[dim] = d3.scaleLinear() - .domain(d3.extent(data, d => d[dim])) - .range([height-margin.top-margin.bottom-30, 0]); + const extent = d3.extent(data, d => d[dim]); + const adjustedExtent = [extent[0] + epsilon, extent[1] + epsilon]; + yScales[dim] = d3.scaleLog() + .domain(adjustedExtent) + .range([height - margin.top - margin.bottom - 30, 0]); }); + const xScale = d3.scalePoint() .domain(dimensions) .range([0, width-margin.left-margin.right]); - // Line generator const lineGenerator = d3.line() .defined(d => !isNaN(d[1])) .x(d => xScale(d[0])) - .y(d => yScales[d[0]](d[1])); + .y(d => yScales[d[0]](d[1] + epsilon)); // Apply epsilon adjustment here + - // Draw axes with labels - dimensions.forEach(dim => { - svg.append("g") - .attr("transform", `translate(${xScale(dim)},0)`) - .call(d3.axisLeft(yScales[dim])); - // Add axis label - svg.append("text") - .attr("transform", `translate(${xScale(dim)},${height-margin.left-margin.right})`) - .style("text-anchor", "middle") - .style("font-size", "12px") - .text(dim); - }); // Group data by state @@ -107,7 +92,8 @@ d3.csv("schengen_data.csv").then((data) => { .merge(paths) .attr("d", d => lineGenerator(dimensions.map(dim => [dim, d[dim]]))) .style("stroke", "steelblue") - .style("opacity", 0.1) + .style("stroke-width", 2) + .style("opacity", 0.05) .attr("fill", "none"); @@ -118,6 +104,26 @@ d3.csv("schengen_data.csv").then((data) => { // Initialize with full dataset updatePlot(null); + // Draw axes with labels + dimensions.forEach(dim => { + svg.append("g") + .attr("transform", `translate(${xScale(dim)},0)`) + .call(d3.axisLeft(yScales[dim])); + + // Use foreignObject for axis labels + svg.append("foreignObject") + .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 + .append("xhtml:div") // Add a div for HTML content + .style("font-size", "12px") + .style("text-align", "center") + .style("word-wrap", "break-word") + .text(dim); + }); + + // Dropdown change event dropdown.on("change", function() { const selectedState = d3.select(this).property("value"); diff --git a/modules/time-series.js b/modules/time-series.js index fc0106081ce51b3b2dfb32822b6435b9a54719bb..4678c95416ec6e82251802d1ae9ef5c5b22622ca 100644 --- a/modules/time-series.js +++ b/modules/time-series.js @@ -1,28 +1,15 @@ -import {legend} from "./country-legend.js"; import {width, height, margin, colors} from "./constants.js"; +import {addLegend, addSvg} from "./main.js"; -function resize() { - const container = document.getElementById('time-series-container'); - const width = container.clientWidth - margin.left - margin.right; - const height = container.clientHeight - margin.top - margin.bottom; - return { width, height }; -} - d3.csv("schengen_data.csv").then((data) => { - const { width, height } = resize(); const allStates = new Set(data.map(d => d["Schengen State"])); const color = d3 .scaleOrdinal() .domain(data) .range(colors) - const svg = d3.select("#time-series-container") - .append("svg") - .attr("width", width + margin.left + margin.right) - .attr("height", height + margin.top + margin.bottom) - .append("g") - .attr("transform", `translate(${margin.left+margin.right},${margin.top+margin.bottom})`); + const svg = addSvg("#time-series-container") function updateGraph(type) { const processedData = d3.rollups( @@ -84,7 +71,7 @@ d3.csv("schengen_data.csv").then((data) => { }) } - legend(svg, allStates, color) + addLegend(svg, allStates, color) } document.getElementById("visa-type-selector").addEventListener("change", (event) => { diff --git a/modules/tree.js b/modules/tree.js index 6fecec6922d98210e0c43aa00c423569800b4f01..11603570091389ff4db50f5502fe377da9cb2b1d 100644 --- a/modules/tree.js +++ b/modules/tree.js @@ -1,14 +1,23 @@ import {width, height, margin} from "./constants.js"; +import {addSvg} from "./main.js"; let currentData; let hierarchyStack = ["Schengen States"]; +let treeHistory = []; d3.csv("schengen_data.csv").then((data) => { currentData = data; updateTree("Schengen States"); }); +function insertHistory(history, tree) { + for (let i = history.length-1; i >= 0; i--) { + tree = {name: history[i], children: [tree]} + } + return tree +} + function updateTree(filterKey) { // Filter data based on the selected key let filteredData; @@ -38,16 +47,25 @@ function updateTree(filterKey) { // Build hierarchy const rootData = buildHierarchy(filteredData, filterKey); - const root = d3.hierarchy(rootData); + const treeData = insertHistory(treeHistory, rootData); + console.log(treeData) + const root = d3.hierarchy(treeData); + + function removeChildren(child) { + if (child.children) { + child._children = child.children; + child.children = null; + } + } //Remove children that are not direct successors of current node if (root.children) { - root.children.forEach(child => { - if (child.children) { - child._children = child.children; - child.children = null; - } - }); + switch (treeHistory.length) { + case 0:root.children.forEach(child=>removeChildren(child));break; + case 1:root.children[0].children.forEach(child=>removeChildren(child));break; + case 2:root.children[0].children[0].children.forEach(child=>removeChildren(child));break; + case 3:root.children[0].children[0].children[0].children.forEach(child=>removeChildren(child));break; + } } // Tree layout @@ -57,13 +75,7 @@ function updateTree(filterKey) { // Clear previous visualization d3.select("#tree-container svg").remove(); - // Append SVG - const svg = d3.select("#tree-container") - .append("svg") - .attr("width", "100%") - .attr("height", "100%") - .append("g") - .attr("transform", `translate(${margin.left+30},${margin.top-10})`); + const svg = addSvg("#tree-container") // Links svg.selectAll(".link") @@ -90,9 +102,11 @@ function updateTree(filterKey) { .on("click", (event, d) => { if (hierarchyStack.length < 3 && d.depth === 1 || d.depth === 2) { // Go deeper for states or countries d3.select(".tooltip").remove(); + treeHistory.push(hierarchyStack[hierarchyStack.length-1]); hierarchyStack.push(d.data.name); updateTree(d.data.name); updateBackButton(); + d3.selectAll(".tooltip").remove(); } }) .on("mouseover", (event, d) => { @@ -103,14 +117,25 @@ function updateTree(filterKey) { .text(d.data.name); }) .on("mouseout", () => { - d3.select(".tooltip").remove(); + d3.selectAll(".tooltip").remove(); }); node.append("text") .attr("dy", 3) - .attr("x", d => d.children ? -10 : 10) + .attr("x", d => { + if (d.data.name==="Schengen States") return 46 + if (d.children) return -10 + if (!d.children) return 10 + }) + .attr("y", d => { + if (d.data.name==="Schengen States") return -12 + if (d.children) return -7 + if (!d.children) return 0 + }) .style("text-anchor", d => d.children ? "end" : "start") .text(d => d.data.name); + + } @@ -118,10 +143,11 @@ 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) - : { name: `${key}: ${value.length}` }; + : { name: `${key}: ${d3.sum(value, d => +d["Total uniform visas issued (including MEV)"] || 0)}` }; }).filter(d => d) }; } @@ -134,6 +160,7 @@ function updateBackButton() { document.getElementById("back-button").addEventListener("click", () => { if (hierarchyStack.length > 1) { hierarchyStack.pop(); + treeHistory.pop(); const previousKey = hierarchyStack[hierarchyStack.length - 1]; updateTree(previousKey); updateBackButton(); diff --git a/style.css b/style.css index 58bb909669d1d47232f30fa951f94b9bfd49949c..b71d382013faa80ed93bea2a9dc631086abe2f1c 100644 --- a/style.css +++ b/style.css @@ -3,7 +3,7 @@ font-size: 14px; } -html, body, #tree-container, #time-series-container { +html, body, #tree-container, #time-series-container, #bubble-container, #parallel-coordinates-container { width: 100%; height: 100%; margin: 0; @@ -86,9 +86,7 @@ html, body, #tree-container, #time-series-container { .visualization-container { height: 100%; - margin: 20px; - box-shadow: #8f8f8f 0 0 8px 0; - border-radius: 8px; + display: none; } .axis path, .axis line { @@ -128,4 +126,29 @@ html, body, #tree-container, #time-series-container { .dropdown-container { position: absolute; z-index: 1000; +} + +.visible { + display: block; +} +.bottom-nav { + position: fixed; + bottom: 0; + width: 100%; + display: flex; + justify-content: space-around; + box-shadow: 0px -2px 5px rgba(0, 0, 0, 0.2); +} +.bottom-nav .button-nav { + flex: 1; + text-align: center; + padding: 15px; + cursor: pointer; + border-top: transparent solid 4px; +} +.bottom-nav .button-nav:hover { + background-color: #f0f0f0; +} +.bottom-nav .button-nav.active { + border-top: #4698ff solid 4px; } \ No newline at end of file