Show code
viewof config = {
const width = 600;
const height = 400;
const numSensors = 25;
const sensorRadius = 60; // Detection radius in pixels
return { width, height, numSensors, sensorRadius };
}
// Controls
viewof autoMove = Inputs.toggle({label: "Auto-Move Target", value: true})
viewof predictionMode = Inputs.toggle({label: "Show Prediction", value: true})
viewof targetSpeed = Inputs.range([0.5, 3], {value: 1.5, step: 0.1, label: "Target Speed"})
viewof showDetectionZones = Inputs.toggle({label: "Show Detection Zones", value: true})
// Generate fixed sensor positions (seeded for consistency)
sensors = {
const positions = [];
const seed = 12345;
const random = (i) => {
const x = Math.sin(seed * i) * 10000;
return x - Math.floor(x);
};
// Grid-based placement with jitter for realistic distribution
const cols = 5;
const rows = 5;
const cellW = config.width / cols;
const cellH = config.height / rows;
for (let i = 0; i < config.numSensors; i++) {
const row = Math.floor(i / cols);
const col = i % cols;
positions.push({
id: i,
x: (col + 0.5) * cellW + (random(i * 2) - 0.5) * cellW * 0.6,
y: (row + 0.5) * cellH + (random(i * 2 + 1) - 0.5) * cellH * 0.6,
active: false
});
}
return positions;
}
// Target state with animation
mutable targetState = ({
x: config.width / 2,
y: config.height / 2,
vx: 1.5,
vy: 1.0,
history: [],
dragging: false
})
// Animation loop
animationTick = {
const interval = setInterval(() => {
if (autoMove && !targetState.dragging) {
let newX = targetState.x + targetState.vx * targetSpeed;
let newY = targetState.y + targetState.vy * targetSpeed;
// Bounce off walls
if (newX < 20 || newX > config.width - 20) {
targetState.vx *= -1;
newX = Math.max(20, Math.min(config.width - 20, newX));
}
if (newY < 20 || newY > config.height - 20) {
targetState.vy *= -1;
newY = Math.max(20, Math.min(config.height - 20, newY));
}
// Add slight randomness for realistic movement
targetState.vx += (Math.random() - 0.5) * 0.1;
targetState.vy += (Math.random() - 0.5) * 0.1;
// Clamp velocity
targetState.vx = Math.max(-2, Math.min(2, targetState.vx));
targetState.vy = Math.max(-2, Math.min(2, targetState.vy));
// Store history for trail
targetState.history.push({x: targetState.x, y: targetState.y});
if (targetState.history.length > 50) targetState.history.shift();
mutable targetState = {
...targetState,
x: newX,
y: newY
};
}
}, 50);
invalidation.then(() => clearInterval(interval));
return "running";
}
// Calculate which sensors detect the target
activeSensors = {
const tx = targetState.x;
const ty = targetState.y;
return sensors.map(s => {
const dist = Math.sqrt((s.x - tx) ** 2 + (s.y - ty) ** 2);
return {
...s,
active: dist <= config.sensorRadius,
distance: dist,
signalStrength: dist <= config.sensorRadius ? Math.max(0, 1 - dist / config.sensorRadius) : 0
};
});
}
// Estimate position using weighted average of detecting sensors
estimatedPosition = {
const detecting = activeSensors.filter(s => s.active);
if (detecting.length === 0) {
return { x: null, y: null, error: null, method: "No detection" };
}
if (detecting.length === 1) {
// Single sensor - position is uncertain (anywhere on detection circle edge)
return {
x: detecting[0].x,
y: detecting[0].y,
error: config.sensorRadius / 2,
method: "Single sensor (high uncertainty)"
};
}
// Weighted average based on signal strength (inverse distance)
let sumX = 0, sumY = 0, sumWeights = 0;
for (const s of detecting) {
const weight = s.signalStrength * s.signalStrength; // Square for emphasis
sumX += s.x * weight;
sumY += s.y * weight;
sumWeights += weight;
}
const estX = sumX / sumWeights;
const estY = sumY / sumWeights;
const error = Math.sqrt((estX - targetState.x) ** 2 + (estY - targetState.y) ** 2);
return {
x: estX,
y: estY,
error: error,
method: detecting.length >= 3 ? "Trilateration (3+ sensors)" : "Bilateration (2 sensors)"
};
}
// Prediction based on velocity
prediction = {
if (!predictionMode) return null;
const predTime = 30; // Predict 30 frames ahead
return {
x: targetState.x + targetState.vx * targetSpeed * predTime,
y: targetState.y + targetState.vy * targetSpeed * predTime
};
}
// Metrics
trackingMetrics = {
const activeCount = activeSensors.filter(s => s.active).length;
const accuracy = estimatedPosition.error !== null ?
Math.max(0, 100 - estimatedPosition.error * 2) : 0;
return {
activeSensors: activeCount,
totalSensors: sensors.length,
trackingAccuracy: accuracy.toFixed(1),
estimationMethod: estimatedPosition.method,
energySavings: (100 * (1 - activeCount / sensors.length)).toFixed(0)
};
}
// Display metrics
html`<div style="display: flex; gap: 20px; flex-wrap: wrap; margin-bottom: 10px; font-family: sans-serif;">
<div style="background: #2C3E50; color: white; padding: 8px 16px; border-radius: 4px;">
<strong>Active Sensors:</strong> ${trackingMetrics.activeSensors} / ${trackingMetrics.totalSensors}
</div>
<div style="background: ${trackingMetrics.trackingAccuracy > 80 ? '#16A085' : trackingMetrics.trackingAccuracy > 50 ? '#E67E22' : '#E74C3C'}; color: white; padding: 8px 16px; border-radius: 4px;">
<strong>Tracking Accuracy:</strong> ${trackingMetrics.trackingAccuracy}%
</div>
<div style="background: #16A085; color: white; padding: 8px 16px; border-radius: 4px;">
<strong>Energy Savings:</strong> ${trackingMetrics.energySavings}%
</div>
<div style="background: #7F8C8D; color: white; padding: 8px 16px; border-radius: 4px; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
<strong>Method:</strong> ${trackingMetrics.estimationMethod}
</div>
</div>`Show code
tracking_viz = {
const svg = d3.create("svg")
.attr("width", config.width)
.attr("height", config.height)
.attr("viewBox", [0, 0, config.width, config.height])
.style("background", "#f8f9fa")
.style("border", "2px solid #2C3E50")
.style("border-radius", "8px");
// Grid lines for reference
const gridGroup = svg.append("g").attr("class", "grid");
for (let x = 0; x <= config.width; x += 60) {
gridGroup.append("line")
.attr("x1", x).attr("y1", 0)
.attr("x2", x).attr("y2", config.height)
.attr("stroke", "#ddd").attr("stroke-width", 1);
}
for (let y = 0; y <= config.height; y += 60) {
gridGroup.append("line")
.attr("x1", 0).attr("y1", y)
.attr("x2", config.width).attr("y2", y)
.attr("stroke", "#ddd").attr("stroke-width", 1);
}
// Detection zones (circles around sensors)
if (showDetectionZones) {
svg.selectAll(".detection-zone")
.data(activeSensors)
.join("circle")
.attr("class", "detection-zone")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", config.sensorRadius)
.attr("fill", d => d.active ? "rgba(22, 160, 133, 0.15)" : "rgba(127, 140, 141, 0.08)")
.attr("stroke", d => d.active ? "#16A085" : "#bdc3c7")
.attr("stroke-width", d => d.active ? 2 : 1)
.attr("stroke-dasharray", d => d.active ? "none" : "4,4");
}
// Target trail (history)
if (targetState.history.length > 1) {
const lineGenerator = d3.line()
.x(d => d.x)
.y(d => d.y)
.curve(d3.curveCatmullRom);
svg.append("path")
.datum(targetState.history)
.attr("d", lineGenerator)
.attr("fill", "none")
.attr("stroke", "#E67E22")
.attr("stroke-width", 2)
.attr("stroke-opacity", 0.4)
.attr("stroke-dasharray", "4,4");
}
// Prediction line
if (prediction && predictionMode) {
svg.append("line")
.attr("x1", targetState.x)
.attr("y1", targetState.y)
.attr("x2", Math.max(20, Math.min(config.width - 20, prediction.x)))
.attr("y2", Math.max(20, Math.min(config.height - 20, prediction.y)))
.attr("stroke", "#3498db")
.attr("stroke-width", 3)
.attr("stroke-dasharray", "8,4")
.attr("marker-end", "url(#arrow)");
// Arrow marker
svg.append("defs").append("marker")
.attr("id", "arrow")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 8)
.attr("refY", 0)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5")
.attr("fill", "#3498db");
// Predicted position marker
svg.append("circle")
.attr("cx", Math.max(20, Math.min(config.width - 20, prediction.x)))
.attr("cy", Math.max(20, Math.min(config.height - 20, prediction.y)))
.attr("r", 8)
.attr("fill", "none")
.attr("stroke", "#3498db")
.attr("stroke-width", 2)
.attr("stroke-dasharray", "4,4");
}
// Sensors
svg.selectAll(".sensor")
.data(activeSensors)
.join("g")
.attr("class", "sensor")
.attr("transform", d => `translate(${d.x}, ${d.y})`)
.each(function(d) {
const g = d3.select(this);
// Sensor body
g.append("rect")
.attr("x", -8)
.attr("y", -8)
.attr("width", 16)
.attr("height", 16)
.attr("rx", 3)
.attr("fill", d.active ? "#16A085" : "#2C3E50")
.attr("stroke", d.active ? "#0e6655" : "#1a252f")
.attr("stroke-width", 2);
// Signal indicator (when active)
if (d.active) {
g.append("circle")
.attr("r", 12)
.attr("fill", "none")
.attr("stroke", "#16A085")
.attr("stroke-width", 2)
.attr("opacity", 0.7);
// Signal strength indicator
const barHeight = d.signalStrength * 10;
g.append("rect")
.attr("x", -2)
.attr("y", -barHeight/2)
.attr("width", 4)
.attr("height", barHeight)
.attr("fill", "#fff");
}
// Sensor ID
g.append("text")
.attr("y", 24)
.attr("text-anchor", "middle")
.attr("font-size", "9px")
.attr("fill", "#666")
.text(`S${d.id}`);
});
// Estimated position (red cross)
if (estimatedPosition.x !== null) {
const estGroup = svg.append("g")
.attr("transform", `translate(${estimatedPosition.x}, ${estimatedPosition.y})`);
// Cross marker
estGroup.append("line")
.attr("x1", -10).attr("y1", 0)
.attr("x2", 10).attr("y2", 0)
.attr("stroke", "#E74C3C")
.attr("stroke-width", 3);
estGroup.append("line")
.attr("x1", 0).attr("y1", -10)
.attr("x2", 0).attr("y2", 10)
.attr("stroke", "#E74C3C")
.attr("stroke-width", 3);
// Error circle
if (estimatedPosition.error > 5) {
estGroup.append("circle")
.attr("r", Math.min(estimatedPosition.error, 30))
.attr("fill", "rgba(231, 76, 60, 0.2)")
.attr("stroke", "#E74C3C")
.attr("stroke-width", 1)
.attr("stroke-dasharray", "3,3");
}
// Label
estGroup.append("text")
.attr("y", -15)
.attr("text-anchor", "middle")
.attr("font-size", "10px")
.attr("fill", "#E74C3C")
.attr("font-weight", "bold")
.text("Estimated");
}
// Target (draggable orange circle)
const target = svg.append("g")
.attr("class", "target")
.attr("transform", `translate(${targetState.x}, ${targetState.y})`)
.style("cursor", "grab");
target.append("circle")
.attr("r", 15)
.attr("fill", "#E67E22")
.attr("stroke", "#d35400")
.attr("stroke-width", 3);
target.append("text")
.attr("text-anchor", "middle")
.attr("dy", "0.35em")
.attr("fill", "white")
.attr("font-size", "10px")
.attr("font-weight", "bold")
.text("T");
target.append("text")
.attr("y", -22)
.attr("text-anchor", "middle")
.attr("font-size", "10px")
.attr("fill", "#E67E22")
.attr("font-weight", "bold")
.text("Target");
// Drag behavior
const drag = d3.drag()
.on("start", function() {
d3.select(this).style("cursor", "grabbing");
mutable targetState = { ...targetState, dragging: true };
})
.on("drag", function(event) {
const newX = Math.max(20, Math.min(config.width - 20, event.x));
const newY = Math.max(20, Math.min(config.height - 20, event.y));
targetState.history.push({x: targetState.x, y: targetState.y});
if (targetState.history.length > 50) targetState.history.shift();
mutable targetState = {
...targetState,
x: newX,
y: newY,
vx: (newX - targetState.x) * 0.3,
vy: (newY - targetState.y) * 0.3
};
})
.on("end", function() {
d3.select(this).style("cursor", "grab");
mutable targetState = { ...targetState, dragging: false };
});
target.call(drag);
// Legend
const legend = svg.append("g")
.attr("transform", `translate(10, ${config.height - 60})`);
legend.append("rect")
.attr("width", 180)
.attr("height", 55)
.attr("fill", "rgba(255,255,255,0.9)")
.attr("stroke", "#ccc")
.attr("rx", 4);
const legendItems = [
{ color: "#E67E22", shape: "circle", label: "Actual Target" },
{ color: "#E74C3C", shape: "cross", label: "Estimated Position" },
{ color: "#3498db", shape: "line", label: "Prediction" },
{ color: "#16A085", shape: "rect", label: "Active Sensor" }
];
legendItems.forEach((item, i) => {
const g = legend.append("g")
.attr("transform", `translate(10, ${12 + i * 11})`);
if (item.shape === "circle") {
g.append("circle").attr("r", 4).attr("fill", item.color);
} else if (item.shape === "cross") {
g.append("line").attr("x1", -4).attr("y1", 0).attr("x2", 4).attr("y2", 0)
.attr("stroke", item.color).attr("stroke-width", 2);
g.append("line").attr("x1", 0).attr("y1", -4).attr("x2", 0).attr("y2", 4)
.attr("stroke", item.color).attr("stroke-width", 2);
} else if (item.shape === "line") {
g.append("line").attr("x1", -6).attr("y1", 0).attr("x2", 6).attr("y2", 0)
.attr("stroke", item.color).attr("stroke-width", 2).attr("stroke-dasharray", "3,2");
} else if (item.shape === "rect") {
g.append("rect").attr("x", -4).attr("y", -4).attr("width", 8).attr("height", 8)
.attr("fill", item.color).attr("rx", 2);
}
g.append("text")
.attr("x", 12)
.attr("dy", "0.35em")
.attr("font-size", "9px")
.attr("fill", "#333")
.text(item.label);
});
return svg.node();
}