<div id="root"></div>
html,
body {
margin: 0;
padding: 0;
overflow: hidden;
width: 100%;
height: 100%;
}
#root {
width: 100%;
height: 100vh;
}
// Sample data for visualization
const globalData = [
{ region: "North America", value: 78, lat: 40, lng: -100, color: "#FF5733" },
{ region: "Europe", value: 82, lat: 50, lng: 10, color: "#33FF57" },
{ region: "Asia", value: 65, lat: 30, lng: 100, color: "#3357FF" },
{ region: "Africa", value: 45, lat: 0, lng: 20, color: "#F3FF33" },
{ region: "South America", value: 56, lat: -20, lng: -60, color: "#FF33F3" },
{ region: "Oceania", value: 88, lat: -25, lng: 135, color: "#33FFF3" }
];
const connectionData = [
{ source: "North America", target: "Europe", value: 230 },
{ source: "Europe", target: "Asia", value: 190 },
{ source: "North America", target: "Asia", value: 270 },
{ source: "Europe", target: "Africa", value: 120 },
{ source: "Asia", target: "Oceania", value: 150 },
{ source: "North America", target: "South America", value: 180 }
];
const timeSeriesData = Array(20)
.fill()
.map((_, i) => ({
date: new Date(2023, i % 12, 1),
value: 50 + Math.sin(i * 0.5) * 20 + Math.random() * 10
}));
const Dashboard = () => {
const { useEffect, useRef, useState } = React;
const globeContainerRef = useRef(null);
const chartRef = useRef(null);
const gaugeRef = useRef(null);
const timeSeriesRef = useRef(null);
const [selectedRegion, setSelectedRegion] = useState(null);
const [showCharts, setShowCharts] = useState(false);
const [hoveredRegion, setHoveredRegion] = useState(null);
const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 });
// Setup Three.js Globe
useEffect(() => {
if (!globeContainerRef.current) return;
// Clear any existing content
while (globeContainerRef.current.firstChild) {
globeContainerRef.current.removeChild(globeContainerRef.current.firstChild);
}
// Create scene, camera, and renderer
const scene = new THREE.Scene();
scene.background = new THREE.Color("#111827");
const camera = new THREE.PerspectiveCamera(
75,
globeContainerRef.current.clientWidth /
globeContainerRef.current.clientHeight,
0.1,
1000
);
camera.position.z = 200;
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(
globeContainerRef.current.clientWidth,
globeContainerRef.current.clientHeight
);
globeContainerRef.current.appendChild(renderer.domElement);
// Simple orbit controls with inertia for smooth movement
const controls = {
rotateSpeed: 0.5,
autoRotate: false,
momentum: 0.92, // Momentum factor for smooth movement
damping: 0.85, // Damping for deceleration
minDistance: 150,
maxDistance: 400,
update: function () {
// Apply momentum if not dragging
if (!this.isDragging) {
this.velocity.x *= this.momentum;
this.velocity.y *= this.momentum;
// Only rotate if velocity is significant
if (
Math.abs(this.velocity.x) > 0.001 ||
Math.abs(this.velocity.y) > 0.001
) {
this.applyRotation(this.velocity.x, this.velocity.y);
}
}
// Apply zoom changes with smooth interpolation
if (Math.abs(this.zoomDelta) > 0.1) {
// Calculate target distance with constraints
const currentDistance = camera.position.length();
let targetDistance = currentDistance - this.zoomDelta * 2;
targetDistance = Math.max(
this.minDistance,
Math.min(this.maxDistance, targetDistance)
);
// Apply zoom with interpolation
const newDistance =
currentDistance + (targetDistance - currentDistance) * 0.1;
const direction = camera.position.clone().normalize();
camera.position.copy(direction.multiplyScalar(newDistance));
// Reduce zoom delta
this.zoomDelta *= 0.85;
}
camera.lookAt(0, 0, 0);
},
// Apply rotation with proper 3D math for smooth orbiting
applyRotation: function (deltaX, deltaY) {
// Create rotation quaternions
const rotationY = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(0, 1, 0),
-deltaX * this.rotateSpeed
);
const rotationX = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(1, 0, 0).applyQuaternion(
new THREE.Quaternion().setFromRotationMatrix(camera.matrix)
),
-deltaY * this.rotateSpeed
);
// Apply rotations
const tempPos = camera.position.clone();
tempPos.applyQuaternion(rotationY);
tempPos.applyQuaternion(rotationX);
// Check if we're going too far up/down and limit if needed
const verticalLimit = 0.9;
const upVector = new THREE.Vector3(0, 1, 0);
const angle = tempPos.normalize().dot(upVector);
if (Math.abs(angle) < verticalLimit) {
camera.position.copy(
tempPos.normalize().multiplyScalar(camera.position.length())
);
} else {
// Only apply horizontal rotation if we hit the vertical limit
camera.position.applyQuaternion(rotationY);
}
},
isDragging: false,
velocity: { x: 0, y: 0 },
previousMouse: { x: 0, y: 0 },
zoomDelta: 0,
// Focus on a specific point
focusOnPoint: function (point) {
// Calculate the direction from origin to point
const direction = point.clone().normalize();
// Set target position at optimal viewing distance
const targetPos = direction.multiplyScalar(250);
// Create animation for smooth transition
const startPos = camera.position.clone();
const startTime = Date.now();
const duration = 1000; // ms
const animateFocus = () => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
// Ease in-out function for smooth acceleration/deceleration
const easeInOut = (t) =>
t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
const t = easeInOut(progress);
// Interpolate position
camera.position.x = startPos.x + (targetPos.x - startPos.x) * t;
camera.position.y = startPos.y + (targetPos.y - startPos.y) * t;
camera.position.z = startPos.z + (targetPos.z - startPos.z) * t;
camera.lookAt(0, 0, 0);
if (progress < 1) {
requestAnimationFrame(animateFocus);
}
};
// Start animation
animateFocus();
// Reset any velocity to stop existing momentum
this.velocity = { x: 0, y: 0 };
}
};
// Set initial camera position
controls.cameraStartPosition = {
x: camera.position.x,
y: camera.position.y,
z: camera.position.z
};
// Mouse event handlers
const onMouseDown = (event) => {
event.preventDefault();
controls.isDragging = true;
controls.previousMouse = { x: event.clientX, y: event.clientY };
// Reset velocity when starting to drag
controls.velocity = { x: 0, y: 0 };
};
const onMouseMove = (event) => {
// Calculate mouse position in normalized device coordinates
const rect = renderer.domElement.getBoundingClientRect();
const mouse = new THREE.Vector2();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
// Update the raycaster
raycaster.setFromCamera(mouse, camera);
// Check for intersections with data points
const intersects = raycaster.intersectObjects(dataPoints);
if (intersects.length > 0) {
document.body.style.cursor = "pointer";
const point = intersects[0].object;
// Update tooltip information
setHoveredRegion(point.userData);
// Get screen position for tooltip
const vector = new THREE.Vector3();
vector.setFromMatrixPosition(point.matrixWorld);
vector.project(camera);
const x = (vector.x * 0.5 + 0.5) * renderer.domElement.clientWidth;
const y = (-vector.y * 0.5 + 0.5) * renderer.domElement.clientHeight;
setTooltipPosition({ x, y });
// Scale up the hovered point
point.scale.set(1.5, 1.5, 1.5);
// Scale down other points
dataPoints.forEach((p) => {
if (p !== point) {
p.scale.set(1, 1, 1);
}
});
} else {
document.body.style.cursor = "default";
setHoveredRegion(null);
// Reset all points
dataPoints.forEach((p) => {
p.scale.set(1, 1, 1);
});
}
// Update controls
if (controls.isDragging) {
const currentMouse = { x: event.clientX, y: event.clientY };
// Calculate delta and update velocity
const deltaX = (currentMouse.x - controls.previousMouse.x) * 0.01;
const deltaY = (currentMouse.y - controls.previousMouse.y) * 0.01;
// Apply rotation directly during drag for immediate feedback
controls.applyRotation(deltaX, deltaY);
// Update velocity based on movement
controls.velocity = {
x: deltaX,
y: deltaY
};
// Save current position for next frame
controls.previousMouse = currentMouse;
}
};
const onMouseUp = () => {
controls.isDragging = false;
// Don't reset velocity to allow momentum to continue
};
const onWheel = (event) => {
event.preventDefault();
controls.zoomDelta += event.deltaY * 0.1;
};
// Then attach events explicitly to the renderer's DOM element
renderer.domElement.addEventListener("mousedown", onMouseDown);
document.addEventListener("mousemove", onMouseMove); // Use document for better tracking
document.addEventListener("mouseup", onMouseUp);
renderer.domElement.addEventListener("wheel", onWheel, { passive: false });
// Create Earth
const earthGeometry = new THREE.SphereGeometry(100, 64, 64);
const earthMaterial = new THREE.MeshPhongMaterial({
color: 0x2233ff,
emissive: 0x112244,
transparent: true,
opacity: 0.9
});
const earth = new THREE.Mesh(earthGeometry, earthMaterial);
scene.add(earth);
// Add atmosphere glow
const atmosphereGeometry = new THREE.SphereGeometry(102, 64, 64);
const atmosphereMaterial = new THREE.MeshPhongMaterial({
color: 0x3366ff,
emissive: 0x3366ff,
transparent: true,
opacity: 0.2,
side: THREE.BackSide
});
const atmosphere = new THREE.Mesh(atmosphereGeometry, atmosphereMaterial);
scene.add(atmosphere);
// Add wireframe overlay
const wireframeGeometry = new THREE.SphereGeometry(101, 32, 32);
const wireframeMaterial = new THREE.MeshBasicMaterial({
color: 0x3399ff,
wireframe: true,
transparent: true,
opacity: 0.1
});
const wireframe = new THREE.Mesh(wireframeGeometry, wireframeMaterial);
scene.add(wireframe);
// Add lighting
const ambientLight = new THREE.AmbientLight(0x404040, 1);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
// Add data points
const dataPoints = [];
globalData.forEach((point) => {
// Convert lat/lng to 3D coordinates
const phi = (90 - point.lat) * (Math.PI / 180);
const theta = (point.lng + 180) * (Math.PI / 180);
const x = -(100 * Math.sin(phi) * Math.cos(theta));
const z = 100 * Math.sin(phi) * Math.sin(theta);
const y = 100 * Math.cos(phi);
// Create point geometry
const pointGeometry = new THREE.SphereGeometry(
Math.sqrt(point.value) * 0.5,
16,
16
);
const pointMaterial = new THREE.MeshBasicMaterial({ color: point.color });
const pointMesh = new THREE.Mesh(pointGeometry, pointMaterial);
// Position point slightly above surface
pointMesh.position.set(x, y, z);
pointMesh.userData = { region: point.region, value: point.value };
// Add to scene
scene.add(pointMesh);
dataPoints.push(pointMesh);
// Add extruded cylinder from surface
const direction = new THREE.Vector3(x, y, z).normalize();
const height = Math.sqrt(point.value) * 0.8;
const cylinderGeometry = new THREE.CylinderGeometry(0.5, 0.5, height, 8);
const cylinderMaterial = new THREE.MeshBasicMaterial({
color: point.color,
transparent: true,
opacity: 0.6
});
const cylinder = new THREE.Mesh(cylinderGeometry, cylinderMaterial);
// Position and orient cylinder
cylinder.position.set(
x - (direction.x * height) / 2,
y - (direction.y * height) / 2,
z - (direction.z * height) / 2
);
cylinder.lookAt(0, 0, 0);
cylinder.rotateX(Math.PI / 2);
scene.add(cylinder);
});
// Add arc connections
connectionData.forEach((connection) => {
const source = globalData.find((d) => d.region === connection.source);
const target = globalData.find((d) => d.region === connection.target);
if (source && target) {
// Convert source and target to 3D coordinates
const sourcePhi = (90 - source.lat) * (Math.PI / 180);
const sourceTheta = (source.lng + 180) * (Math.PI / 180);
const sourceX = -(100 * Math.sin(sourcePhi) * Math.cos(sourceTheta));
const sourceZ = 100 * Math.sin(sourcePhi) * Math.sin(sourceTheta);
const sourceY = 100 * Math.cos(sourcePhi);
const targetPhi = (90 - target.lat) * (Math.PI / 180);
const targetTheta = (target.lng + 180) * (Math.PI / 180);
const targetX = -(100 * Math.sin(targetPhi) * Math.cos(targetTheta));
const targetZ = 100 * Math.sin(targetPhi) * Math.sin(targetTheta);
const targetY = 100 * Math.cos(targetPhi);
// Create a curved path between points
const curvePoints = [];
for (let i = 0; i <= 20; i++) {
const t = i / 20;
// Interpolate between source and target
const x = sourceX * (1 - t) + targetX * t;
const y = sourceY * (1 - t) + targetY * t;
const z = sourceZ * (1 - t) + targetZ * t;
// Add height to the curve
const midPoint = new THREE.Vector3(x, y, z);
const length = midPoint.length();
midPoint.normalize();
const heightFactor = Math.sin(Math.PI * t) * (connection.value / 10);
midPoint.multiplyScalar(length + heightFactor);
curvePoints.push(midPoint);
}
// Create curve from points
const curve = new THREE.CatmullRomCurve3(curvePoints);
const curveGeometry = new THREE.TubeGeometry(curve, 20, 0.5, 8, false);
// Create gradient material
const curveMaterial = new THREE.MeshBasicMaterial({
color: new THREE.Color(source.color).lerp(
new THREE.Color(target.color),
0.5
),
transparent: true,
opacity: 0.6
});
const curveMesh = new THREE.Mesh(curveGeometry, curveMaterial);
scene.add(curveMesh);
}
});
// Add stars
const starGeometry = new THREE.BufferGeometry();
const starMaterial = new THREE.PointsMaterial({
color: 0xffffff,
size: 0.7,
transparent: true,
opacity: 0.8
});
const starVertices = [];
for (let i = 0; i < 3000; i++) {
const x = (Math.random() - 0.5) * 2000;
const y = (Math.random() - 0.5) * 2000;
const z = (Math.random() - 0.5) * 2000;
starVertices.push(x, y, z);
}
starGeometry.setAttribute(
"position",
new THREE.Float32BufferAttribute(starVertices, 3)
);
const stars = new THREE.Points(starGeometry, starMaterial);
scene.add(stars);
// Raycaster for interaction
const raycaster = new THREE.Raycaster();
// Click handler
const onClick = (event) => {
// Don't process clicks when dragging
if (controls.isDragging) return;
// Calculate mouse position in normalized device coordinates
const rect = renderer.domElement.getBoundingClientRect();
const mouse = new THREE.Vector2();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
// Update the raycaster
raycaster.setFromCamera(mouse, camera);
// Check for intersections with data points
const intersects = raycaster.intersectObjects(dataPoints);
if (intersects.length > 0) {
const point = intersects[0].object;
setSelectedRegion(point.userData.region);
setShowCharts(true);
// Focus camera on the selected region
controls.focusOnPoint(point.position);
// Highlight the selected point
dataPoints.forEach((p) => {
if (p === point) {
// Scale up the selected point
p.scale.set(2, 2, 2);
// Create a pulse effect
const pulseEffect = () => {
const scale = 1.8 + Math.sin(Date.now() * 0.005) * 0.2;
p.scale.set(scale, scale, scale);
if (p.userData.region === selectedRegion) {
requestAnimationFrame(pulseEffect);
} else {
// Reset scale when no longer selected
p.scale.set(1, 1, 1);
}
};
pulseEffect();
} else {
// Dim the other points
p.scale.set(0.8, 0.8, 0.8);
}
});
}
};
window.addEventListener("click", onClick);
// Animation loop
const animate = () => {
requestAnimationFrame(animate);
// Make wireframe rotate slightly differently
wireframe.rotation.y += 0.0003;
wireframe.rotation.x += 0.0001;
// Make atmosphere pulse
const time = Date.now() * 0.0005;
atmosphere.material.opacity = 0.2 + Math.sin(time) * 0.05;
// Rotate stars slowly
stars.rotation.y += 0.0001;
// update the controls on each frame
controls.update();
renderer.render(scene, camera);
};
animate();
// Resize handler
const handleResize = () => {
if (!globeContainerRef.current) return;
camera.aspect =
globeContainerRef.current.clientWidth /
globeContainerRef.current.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(
globeContainerRef.current.clientWidth,
globeContainerRef.current.clientHeight
);
};
window.addEventListener("resize", handleResize);
// Cleanup
return () => {
renderer.domElement.removeEventListener("mousedown", onMouseDown);
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
renderer.domElement.removeEventListener("wheel", onWheel);
window.removeEventListener("click", onClick);
window.removeEventListener("resize", handleResize);
if (globeContainerRef.current) {
while (globeContainerRef.current.firstChild) {
globeContainerRef.current.removeChild(
globeContainerRef.current.firstChild
);
}
}
// Dispose of geometries and materials
earthGeometry.dispose();
earthMaterial.dispose();
atmosphereGeometry.dispose();
atmosphereMaterial.dispose();
wireframeGeometry.dispose();
wireframeMaterial.dispose();
starGeometry.dispose();
starMaterial.dispose();
};
}, []);
// Setup D3 Charts
useEffect(() => {
if (!showCharts) return;
// Create radar chart
if (chartRef.current) {
const width = chartRef.current.clientWidth;
const height = chartRef.current.clientHeight;
const radius = Math.min(width, height) / 2 - 30;
d3.select(chartRef.current).selectAll("*").remove();
const svg = d3
.select(chartRef.current)
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", `translate(${width / 2}, ${height / 2})`);
// Create circular grid
const levels = 5;
for (let i = 0; i < levels; i++) {
svg
.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", (radius * (i + 1)) / levels)
.attr("fill", "none")
.attr("stroke", "#333")
.attr("stroke-width", 0.5)
.attr("opacity", 0.3);
}
// Create radar axes
const features = [
"Technology",
"Economy",
"Education",
"Health",
"Environment",
"Infrastructure"
];
const angleSlice = (Math.PI * 2) / features.length;
// Create axes
features.forEach((feature, i) => {
const angle = angleSlice * i - Math.PI / 2;
// Draw axis line
svg
.append("line")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", radius * Math.cos(angle))
.attr("y2", radius * Math.sin(angle))
.attr("stroke", "#333")
.attr("stroke-width", 0.5)
.attr("opacity", 0.3);
// Add axis label
svg
.append("text")
.attr("x", (radius + 10) * Math.cos(angle))
.attr("y", (radius + 10) * Math.sin(angle))
.attr("text-anchor", "middle")
.attr("dy", "0.35em")
.attr("fill", "#fff")
.attr("font-size", "12px")
.attr("opacity", 0)
.text(feature)
.transition()
.delay(i * 100)
.duration(500)
.attr("opacity", 1);
});
// Generate random data for selected region
const radarData = features.map((feature) => ({
feature,
value: 0.2 + Math.random() * 0.7 // Random value between 0.2 and 0.9
}));
// Create radar path
const radarLine = d3
.lineRadial()
.radius((d) => d.value * radius)
.angle((d, i) => i * angleSlice - Math.PI / 2)
.curve(d3.curveLinearClosed);
// Add radar path with animation
const regionColor =
globalData.find((d) => d.region === selectedRegion)?.color || "#33FFF3";
const radarPath = svg
.append("path")
.datum(radarData)
.attr("d", (d) => radarLine(d.map((p) => ({ value: 0, angle: p.feature }))))
.attr("fill", regionColor)
.attr("fill-opacity", 0.6)
.attr("stroke", regionColor)
.attr("stroke-width", 2);
// Animate radar path
radarPath
.transition()
.duration(1000)
.attr("d", (d) =>
radarLine(d.map((p) => ({ value: p.value, angle: p.feature })))
);
// Add data points
radarData.forEach((d, i) => {
const angle = angleSlice * i - Math.PI / 2;
svg
.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", 4)
.attr("fill", regionColor)
.attr("opacity", 0)
.transition()
.delay(1000 + i * 100)
.duration(300)
.attr("cx", d.value * radius * Math.cos(angle))
.attr("cy", d.value * radius * Math.sin(angle))
.attr("opacity", 1);
});
}
// Create gauge chart
if (gaugeRef.current) {
const width = gaugeRef.current.clientWidth;
const height = gaugeRef.current.clientHeight;
d3.select(gaugeRef.current).selectAll("*").remove();
const svg = d3
.select(gaugeRef.current)
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", `translate(${width / 2}, ${height / 2})`);
const radius = Math.min(width, height) / 2 - 10;
// Create gauge background
const arc = d3
.arc()
.innerRadius(radius * 0.7)
.outerRadius(radius)
.startAngle(-Math.PI / 2)
.endAngle(Math.PI / 2);
svg
.append("path")
.attr("d", arc())
.attr("fill", "#333")
.attr("stroke", "#555")
.attr("stroke-width", 1);
// Create value scale
const scale = d3
.scaleLinear()
.domain([0, 100])
.range([-Math.PI / 2, Math.PI / 2]);
// Generate value for selected region
const gaugeValue =
globalData.find((d) => d.region === selectedRegion)?.value || 50;
const regionColor =
globalData.find((d) => d.region === selectedRegion)?.color || "#33FFF3";
// Create value arc
const valueArc = d3
.arc()
.innerRadius(radius * 0.7)
.outerRadius(radius)
.startAngle(-Math.PI / 2)
.endAngle(-Math.PI / 2); // Start at zero
const valueArcPath = svg
.append("path")
.attr("d", valueArc())
.attr("fill", regionColor);
// Animate value arc
valueArcPath
.transition()
.duration(1500)
.attrTween("d", function () {
return function (t) {
valueArc.endAngle(-Math.PI / 2 + scale(gaugeValue * t) + Math.PI / 2);
return valueArc();
};
});
// Add gauge needle
const needleLine = svg
.append("line")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", 0)
.attr("y2", -radius * 0.8)
.attr("stroke", "#fff")
.attr("stroke-width", 2)
.attr("transform", "rotate(0)");
// Animate needle
needleLine
.transition()
.duration(1500)
.attrTween("transform", function () {
return function (t) {
const angle = scale(gaugeValue * t) * (180 / Math.PI);
return `rotate(${angle})`;
};
});
// Add needle center
svg
.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", radius * 0.1)
.attr("fill", "#fff")
.attr("stroke", "#333")
.attr("stroke-width", 1);
// Add value text
const valueText = svg
.append("text")
.attr("x", 0)
.attr("y", radius * 0.3)
.attr("text-anchor", "middle")
.attr("fill", "#fff")
.attr("font-size", "24px")
.attr("font-weight", "bold")
.text("0");
// Animate value text
valueText
.transition()
.duration(1500)
.tween("text", function () {
const i = d3.interpolate(0, gaugeValue);
return function (t) {
this.textContent = Math.round(i(t));
};
});
// Add label
svg
.append("text")
.attr("x", 0)
.attr("y", radius * 0.5)
.attr("text-anchor", "middle")
.attr("fill", "#999")
.attr("font-size", "14px")
.text("Development Index");
}
// Create time series chart
if (timeSeriesRef.current) {
const width = timeSeriesRef.current.clientWidth;
const height = timeSeriesRef.current.clientHeight - 30;
const margin = { top: 20, right: 20, bottom: 30, left: 40 };
d3.select(timeSeriesRef.current).selectAll("*").remove();
const svg = d3
.select(timeSeriesRef.current)
.append("svg")
.attr("width", width)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);
// Create scales
const x = d3
.scaleTime()
.domain(d3.extent(timeSeriesData, (d) => d.date))
.range([0, width - margin.left - margin.right]);
const y = d3
.scaleLinear()
.domain([0, d3.max(timeSeriesData, (d) => d.value) * 1.1])
.range([height, 0]);
// Add X axis
svg
.append("g")
.attr("transform", `translate(0, ${height})`)
.attr("color", "#666")
.call(d3.axisBottom(x).ticks(6).tickFormat(d3.timeFormat("%b %y")));
// Add Y axis
svg.append("g").attr("color", "#666").call(d3.axisLeft(y));
// Add grid lines
svg
.append("g")
.attr("class", "grid")
.attr("opacity", 0.1)
.call(
d3
.axisLeft(y)
.tickSize(-width + margin.left + margin.right)
.tickFormat("")
);
// Create line generator
const line = d3
.line()
.x((d) => x(d.date))
.y((d) => y(d.value))
.curve(d3.curveMonotoneX);
// Add line path
const regionColor =
globalData.find((d) => d.region === selectedRegion)?.color || "#33FFF3";
const path = svg
.append("path")
.datum(timeSeriesData)
.attr("fill", "none")
.attr("stroke", regionColor)
.attr("stroke-width", 3)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round");
// Animate line drawing
const pathLength = path.node().getTotalLength();
path
.attr("stroke-dasharray", pathLength)
.attr("stroke-dashoffset", pathLength)
.attr("d", line)
.transition()
.duration(2000)
.attr("stroke-dashoffset", 0);
// Add area under line
const area = d3
.area()
.x((d) => x(d.date))
.y0(height)
.y1((d) => y(d.value))
.curve(d3.curveMonotoneX);
const areaPath = svg
.append("path")
.datum(timeSeriesData)
.attr("fill", regionColor)
.attr("fill-opacity", 0.2)
.attr("d", area);
// Animate area filling
areaPath
.attr("opacity", 0)
.transition()
.delay(1500)
.duration(500)
.attr("opacity", 1);
// Add data points
svg
.selectAll(".data-point")
.data(timeSeriesData)
.enter()
.append("circle")
.attr("class", "data-point")
.attr("cx", (d) => x(d.date))
.attr("cy", (d) => y(d.value))
.attr("r", 4)
.attr("fill", regionColor)
.attr("stroke", "#fff")
.attr("stroke-width", 1)
.attr("opacity", 0)
.transition()
.delay((_, i) => 2000 + i * 50)
.duration(300)
.attr("opacity", 1);
}
}, [showCharts, selectedRegion]);
return (
<div className="flex flex-col w-full h-screen bg-gray-900 text-white">
{/* Header */}
<div className="bg-gray-800 p-4 flex justify-between items-center">
<div className="text-2xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-purple-600">
Global Technology Index Dashboard
</div>
{selectedRegion && (
<div className="text-xl">
Region: <span className="font-bold">{selectedRegion}</span>
</div>
)}
<div className="flex items-center space-x-4">
{selectedRegion && (
<button
className="px-4 py-2 bg-blue-600 rounded hover:bg-blue-700 transition-colors"
onClick={() => {
// Reset selected region state
setSelectedRegion(null);
setShowCharts(false);
// Reset all data points to normal size
dataPoints.forEach((p) => {
p.scale.set(1, 1, 1);
});
// Animate camera back to default position
const startPos = camera.position.clone();
const targetPos = new THREE.Vector3(0, 0, 200);
const startTime = Date.now();
const duration = 1000; // ms
const resetCamera = () => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
// Ease in-out function
const easeInOut = (t) =>
t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
const t = easeInOut(progress);
// Interpolate position
camera.position.x = startPos.x + (targetPos.x - startPos.x) * t;
camera.position.y = startPos.y + (targetPos.y - startPos.y) * t;
camera.position.z = startPos.z + (targetPos.z - startPos.z) * t;
camera.lookAt(0, 0, 0);
if (progress < 1) {
requestAnimationFrame(resetCamera);
}
};
resetCamera();
// Reset controls
controls.velocity = { x: 0, y: 0 };
}}
>
Reset View
</button>
)}
</div>
</div>
{/* Floating tooltip for hovered region */}
{hoveredRegion && (
<div
className="absolute z-20 bg-gray-800 border border-blue-500 rounded-lg shadow-lg p-4 pointer-events-none"
style={{
left: `${tooltipPosition.x}px`,
top: `${tooltipPosition.y + 20}px`,
maxWidth: "250px",
opacity: 0.9
}}
>
<div className="text-lg font-bold text-blue-400 mb-1">
{hoveredRegion.region}
</div>
<div className="flex items-center mb-2">
<div
className="w-4 h-4 rounded-full mr-2"
style={{
backgroundColor: globalData.find(
(d) => d.region === hoveredRegion.region
)?.color
}}
></div>
<div>
Index: <span className="font-semibold">{hoveredRegion.value}</span>
</div>
</div>
<div className="text-xs text-gray-400">
Click to view detailed analytics
</div>
</div>
)}
{/* Main container */}
<div className="flex flex-1 relative overflow-hidden">
{/* 3D Globe container */}
<div
ref={globeContainerRef}
className="absolute inset-0 w-full h-full"
style={{
width: showCharts ? "calc(100% - 400px)" : "100%",
transition: "width 500ms ease-in-out"
}}
/>
{/* Charts panel */}
<div
className="absolute right-0 top-0 bottom-0 bg-gray-800 border-l border-gray-700 shadow-xl overflow-auto"
style={{
width: "400px",
transform: showCharts ? "translateX(0)" : "translateX(100%)",
transition: "transform 500ms ease-in-out"
}}
>
{selectedRegion && (
<>
<div className="p-4 border-b border-gray-700">
<h2 className="text-xl font-bold">{selectedRegion} Analytics</h2>
</div>
<div className="p-4">
<h3 className="text-lg font-medium mb-2">Performance Metrics</h3>
<div ref={chartRef} className="h-64 mb-6"></div>
<h3 className="text-lg font-medium mb-2">Development Index</h3>
<div ref={gaugeRef} className="h-48 mb-6"></div>
<h3 className="text-lg font-medium mb-2">Growth Trend</h3>
<div ref={timeSeriesRef} className="h-64"></div>
</div>
</>
)}
</div>
</div>
{/* Footer */}
<div className="bg-gray-800 p-3 text-xs text-gray-400 flex justify-between items-center">
<div>
Data refreshed: April 30, 2025 • Regions: {globalData.length} •
Connections: {connectionData.length}
</div>
<div className="flex space-x-4">
<button className="hover:text-white transition-colors">Dashboard</button>
<button className="hover:text-white transition-colors">Reports</button>
<button className="hover:text-white transition-colors">Settings</button>
</div>
</div>
</div>
);
};
// Render the component into the DOM
ReactDOM.render(<Dashboard />, document.getElementById("root"));
View Compiled
This Pen doesn't use any external CSS resources.