Skip to content
Snippets Groups Projects
Commit 2958896a authored by ericj's avatar ericj
Browse files

code refactoring, perf opt, autofill dropdown selectors with data columns, add...

code refactoring, perf opt, autofill dropdown selectors with data columns, add data filter for bubble graph
parent c03fd5bc
No related branches found
No related tags found
No related merge requests found
......@@ -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>
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()
});
//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
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
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
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")
......
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);
});
......
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)
......
......@@ -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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment