1101 lines
42 KiB
HTML
1101 lines
42 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>UI Innovation: EcoSystem Display - Living Data Visualization</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
line-height: 1.6;
|
|
color: #333;
|
|
background: linear-gradient(135deg, #2c5530 0%, #3d7c47 50%, #1a3d1f 100%);
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
header {
|
|
text-align: center;
|
|
margin-bottom: 40px;
|
|
background: rgba(255, 255, 255, 0.95);
|
|
padding: 30px;
|
|
border-radius: 15px;
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
h1 {
|
|
color: #2d5a2d;
|
|
margin-bottom: 20px;
|
|
font-size: 2.5em;
|
|
}
|
|
|
|
.innovation-meta {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 40px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.innovation-meta p {
|
|
background: #e8f5e8;
|
|
padding: 10px 20px;
|
|
border-radius: 8px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
main {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
section {
|
|
background: rgba(255, 255, 255, 0.95);
|
|
margin-bottom: 30px;
|
|
padding: 30px;
|
|
border-radius: 15px;
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
h2 {
|
|
color: #2d5a2d;
|
|
margin-bottom: 25px;
|
|
font-size: 1.8em;
|
|
}
|
|
|
|
h3 {
|
|
color: #3a6b3a;
|
|
margin-bottom: 15px;
|
|
font-size: 1.3em;
|
|
}
|
|
|
|
.demo-container {
|
|
background: linear-gradient(135deg, #1a3d1f 0%, #2c5530 50%, #3d7c47 100%);
|
|
color: white;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.ecosystem-container {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 500px;
|
|
background: radial-gradient(ellipse at center, #4a7c59 0%, #2c5530 70%, #1a3d1f 100%);
|
|
border-radius: 15px;
|
|
overflow: hidden;
|
|
cursor: grab;
|
|
border: 3px solid #5d8a66;
|
|
}
|
|
|
|
.ecosystem-container:active {
|
|
cursor: grabbing;
|
|
}
|
|
|
|
.water-surface {
|
|
position: absolute;
|
|
bottom: 0;
|
|
width: 100%;
|
|
height: 40%;
|
|
background: linear-gradient(to bottom, rgba(0, 191, 255, 0.3) 0%, rgba(0, 100, 200, 0.5) 100%);
|
|
opacity: 0.7;
|
|
animation: water-flow 8s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes water-flow {
|
|
0%, 100% { transform: translateX(0); }
|
|
50% { transform: translateX(10px); }
|
|
}
|
|
|
|
.organism {
|
|
position: absolute;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
user-select: none;
|
|
text-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
|
|
transform-origin: center;
|
|
font-size: 12px;
|
|
color: white;
|
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
|
z-index: 10;
|
|
}
|
|
|
|
.organism:hover {
|
|
transform: scale(1.2);
|
|
z-index: 20;
|
|
box-shadow: 0 8px 25px rgba(255, 255, 255, 0.3);
|
|
}
|
|
|
|
.organism:focus {
|
|
outline: 3px solid #ffff00;
|
|
outline-offset: 5px;
|
|
}
|
|
|
|
.organism.predator {
|
|
border: 3px solid #ff4444;
|
|
animation: hunt 6s linear infinite;
|
|
}
|
|
|
|
.organism.prey {
|
|
border: 2px solid #44ff44;
|
|
animation: graze 4s ease-in-out infinite;
|
|
}
|
|
|
|
.organism.producer {
|
|
border: 2px solid #44ff88;
|
|
animation: photosynthesize 8s ease-in-out infinite;
|
|
}
|
|
|
|
.organism.decomposer {
|
|
border: 2px solid #ff8844;
|
|
animation: decompose 3s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes hunt {
|
|
0%, 100% { box-shadow: 0 4px 15px rgba(255, 68, 68, 0.3); }
|
|
50% { box-shadow: 0 8px 30px rgba(255, 68, 68, 0.8); }
|
|
}
|
|
|
|
@keyframes graze {
|
|
0%, 100% { transform: rotate(0deg) scale(1); }
|
|
25% { transform: rotate(-5deg) scale(1.05); }
|
|
75% { transform: rotate(5deg) scale(0.95); }
|
|
}
|
|
|
|
@keyframes photosynthesize {
|
|
0%, 100% { box-shadow: 0 4px 15px rgba(68, 255, 136, 0.3); }
|
|
50% { box-shadow: 0 8px 30px rgba(68, 255, 136, 0.8); }
|
|
}
|
|
|
|
@keyframes decompose {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.7; }
|
|
}
|
|
|
|
.ecosystem-controls {
|
|
position: absolute;
|
|
top: 20px;
|
|
left: 20px;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
padding: 20px;
|
|
border-radius: 10px;
|
|
backdrop-filter: blur(10px);
|
|
min-width: 200px;
|
|
}
|
|
|
|
.ecosystem-controls h4 {
|
|
margin-bottom: 15px;
|
|
color: #88ff88;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.ecosystem-controls label {
|
|
display: block;
|
|
font-size: 12px;
|
|
margin-bottom: 5px;
|
|
color: #ccffcc;
|
|
}
|
|
|
|
.ecosystem-controls input, .ecosystem-controls button {
|
|
width: 100%;
|
|
margin-bottom: 10px;
|
|
padding: 5px;
|
|
border: none;
|
|
border-radius: 5px;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
color: white;
|
|
}
|
|
|
|
.ecosystem-controls button {
|
|
background: #5d8a66;
|
|
cursor: pointer;
|
|
font-weight: bold;
|
|
padding: 8px;
|
|
}
|
|
|
|
.ecosystem-controls button:hover {
|
|
background: #6d9a76;
|
|
}
|
|
|
|
.ecosystem-stats {
|
|
position: absolute;
|
|
top: 20px;
|
|
right: 20px;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
padding: 20px;
|
|
border-radius: 10px;
|
|
backdrop-filter: blur(10px);
|
|
min-width: 200px;
|
|
}
|
|
|
|
.ecosystem-stats h4 {
|
|
margin-bottom: 15px;
|
|
color: #88ff88;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.stat-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 8px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.stat-label {
|
|
color: #ccffcc;
|
|
}
|
|
|
|
.stat-value {
|
|
color: #88ff88;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.organism-tooltip {
|
|
position: absolute;
|
|
background: rgba(0, 0, 0, 0.9);
|
|
color: white;
|
|
padding: 10px;
|
|
border-radius: 5px;
|
|
font-size: 12px;
|
|
pointer-events: none;
|
|
z-index: 1000;
|
|
opacity: 0;
|
|
transition: opacity 0.3s ease;
|
|
max-width: 200px;
|
|
}
|
|
|
|
.data-entry {
|
|
margin-bottom: 20px;
|
|
padding: 15px;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 10px;
|
|
border-left: 4px solid;
|
|
}
|
|
|
|
.data-entry.high { border-left-color: #ff4444; }
|
|
.data-entry.medium { border-left-color: #ffaa44; }
|
|
.data-entry.low { border-left-color: #44ff44; }
|
|
|
|
.comparison-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 30px;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.traditional, .innovative {
|
|
padding: 20px;
|
|
border-radius: 10px;
|
|
border: 2px solid #ddd;
|
|
}
|
|
|
|
.traditional {
|
|
background: #f9f9f9;
|
|
}
|
|
|
|
.innovative {
|
|
background: #f0f8f0;
|
|
border-color: #5d8a66;
|
|
}
|
|
|
|
.traditional-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin-top: 10px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.traditional-table th,
|
|
.traditional-table td {
|
|
border: 1px solid #ddd;
|
|
padding: 8px;
|
|
text-align: left;
|
|
}
|
|
|
|
.traditional-table th {
|
|
background: #f2f2f2;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.traditional-table tr:nth-child(even) {
|
|
background: #f9f9f9;
|
|
}
|
|
|
|
.doc-section {
|
|
margin-bottom: 25px;
|
|
padding: 20px;
|
|
background: #f8f9fa;
|
|
border-radius: 8px;
|
|
border-left: 4px solid #5d8a66;
|
|
}
|
|
|
|
.ecosystem-mood {
|
|
position: absolute;
|
|
bottom: 20px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: rgba(0, 0, 0, 0.7);
|
|
padding: 10px 20px;
|
|
border-radius: 20px;
|
|
color: white;
|
|
font-size: 14px;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.floating-info {
|
|
position: absolute;
|
|
bottom: 20px;
|
|
right: 20px;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
padding: 10px;
|
|
border-radius: 10px;
|
|
color: white;
|
|
font-size: 12px;
|
|
max-width: 300px;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.comparison-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.innovation-meta {
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
}
|
|
|
|
.ecosystem-container {
|
|
height: 400px;
|
|
}
|
|
|
|
.ecosystem-controls,
|
|
.ecosystem-stats {
|
|
position: relative;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
margin: 10px 0;
|
|
min-width: auto;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>UI Innovation: EcoSystem Display - Living Data Visualization</h1>
|
|
<div class="innovation-meta">
|
|
<p><strong>Replaces:</strong> Traditional Data Tables/Lists</p>
|
|
<p><strong>Innovation:</strong> Living ecosystem metaphor for data representation</p>
|
|
</div>
|
|
</header>
|
|
|
|
<main>
|
|
<section class="demo-container">
|
|
<h2>Interactive Demo</h2>
|
|
<div class="innovation-component">
|
|
<div class="ecosystem-container" id="ecosystemContainer" role="region" aria-label="Living data ecosystem">
|
|
<div class="water-surface"></div>
|
|
|
|
<div class="ecosystem-controls" id="ecosystemControls">
|
|
<h4>🎛️ Ecosystem Controls</h4>
|
|
<label for="populationSlider">Population:</label>
|
|
<input type="range" id="populationSlider" min="5" max="50" value="20">
|
|
|
|
<label for="speedSlider">Time Speed:</label>
|
|
<input type="range" id="speedSlider" min="0.5" max="3" step="0.1" value="1">
|
|
|
|
<label for="diversitySlider">Diversity:</label>
|
|
<input type="range" id="diversitySlider" min="0.1" max="1" step="0.1" value="0.5">
|
|
|
|
<button id="addDataBtn">Add Random Data</button>
|
|
<button id="resetEcosystemBtn">Reset Ecosystem</button>
|
|
</div>
|
|
|
|
<div class="ecosystem-stats" id="ecosystemStats">
|
|
<h4>📊 Ecosystem Metrics</h4>
|
|
<div class="stat-item">
|
|
<span class="stat-label">Total Organisms:</span>
|
|
<span class="stat-value" id="totalOrganisms">0</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-label">Producers:</span>
|
|
<span class="stat-value" id="producerCount">0</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-label">Consumers:</span>
|
|
<span class="stat-value" id="consumerCount">0</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-label">Predators:</span>
|
|
<span class="stat-value" id="predatorCount">0</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-label">System Health:</span>
|
|
<span class="stat-value" id="systemHealth">100%</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-label">Data Quality:</span>
|
|
<span class="stat-value" id="dataQuality">Excellent</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ecosystem-mood" id="ecosystemMood">
|
|
🌱 Ecosystem is Thriving
|
|
</div>
|
|
|
|
<div class="floating-info">
|
|
<strong>How to interact:</strong><br>
|
|
• Hover over organisms to see data details<br>
|
|
• Click organisms to select and highlight relationships<br>
|
|
• Adjust controls to change ecosystem behavior<br>
|
|
• Watch how data values influence organism behavior
|
|
</div>
|
|
</div>
|
|
|
|
<div class="organism-tooltip" id="tooltip"></div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="comparison">
|
|
<h2>Traditional vs Innovation</h2>
|
|
<div class="comparison-grid">
|
|
<div class="traditional">
|
|
<h3>Traditional Data Table</h3>
|
|
<p>Static tabular representation with rows and columns.</p>
|
|
<table class="traditional-table">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Name</th>
|
|
<th>Value</th>
|
|
<th>Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>001</td>
|
|
<td>Sales Data</td>
|
|
<td>85%</td>
|
|
<td>High</td>
|
|
</tr>
|
|
<tr>
|
|
<td>002</td>
|
|
<td>User Engagement</td>
|
|
<td>62%</td>
|
|
<td>Medium</td>
|
|
</tr>
|
|
<tr>
|
|
<td>003</td>
|
|
<td>Server Load</td>
|
|
<td>34%</td>
|
|
<td>Low</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<p style="margin-top: 15px; font-size: 14px; color: #666;">
|
|
✓ Precise data display<br>
|
|
✓ Easy scanning<br>
|
|
✗ Static representation<br>
|
|
✗ No relationship visualization<br>
|
|
✗ Limited engagement
|
|
</p>
|
|
</div>
|
|
<div class="innovative">
|
|
<h3>EcoSystem Display</h3>
|
|
<p>Living ecosystem where data becomes organisms with adaptive behaviors and natural relationships.</p>
|
|
<p style="margin-top: 15px; font-size: 14px; color: #2d5a2d;">
|
|
✓ Dynamic relationship visualization<br>
|
|
✓ Temporal data evolution<br>
|
|
✓ Intuitive pattern recognition<br>
|
|
✓ Engaging narrative experience<br>
|
|
✓ Adaptive behavioral feedback<br>
|
|
✓ Collaborative data interactions<br>
|
|
✓ Full accessibility support
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="documentation">
|
|
<h2>Design Documentation</h2>
|
|
<div class="doc-section">
|
|
<h3>Interaction Model</h3>
|
|
<p>Data entries are represented as digital organisms in a living ecosystem. Each organism's behavior, size, and movement patterns reflect the underlying data values and relationships. Users interact by observing ecosystem patterns, hovering for details, clicking to explore relationships, and adjusting environmental parameters to see different data perspectives.</p>
|
|
</div>
|
|
<div class="doc-section">
|
|
<h3>Technical Implementation</h3>
|
|
<p>Built with real-time ecosystem simulation using requestAnimationFrame for 60fps animation. Organisms use autonomous agent behaviors including flocking, seeking, and avoidance. Data mapping algorithms translate numerical values into biological characteristics like size, speed, and behavior patterns. CSS animations and transforms create fluid organic movement.</p>
|
|
</div>
|
|
<div class="doc-section">
|
|
<h3>Accessibility Features</h3>
|
|
<p>Maintains full keyboard navigation with Tab cycling and Enter selection. Screen readers access data through ARIA labels and live regions. All visual patterns have corresponding semantic descriptions. High contrast modes preserve ecosystem functionality. Alternative data views provide traditional table access when needed.</p>
|
|
</div>
|
|
<div class="doc-section">
|
|
<h3>Evolution Opportunities</h3>
|
|
<p>Future enhancements could include multi-dimensional ecosystem layers for complex datasets, seasonal cycles reflecting temporal data patterns, predator-prey relationships showing data dependencies, evolutionary algorithms that adapt organism behaviors to data trends, collaborative ecosystems for multi-user data exploration, and AI-driven ecosystem narratives that explain data insights through natural storytelling.</p>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<script>
|
|
class EcoSystemDisplay {
|
|
constructor(container) {
|
|
this.container = container;
|
|
this.organisms = [];
|
|
this.animationId = null;
|
|
this.tooltip = document.getElementById('tooltip');
|
|
this.timeSpeed = 1;
|
|
this.ecosystemMood = 'thriving';
|
|
|
|
// Sample data representing different metrics
|
|
this.dataPoints = [
|
|
{ id: 'sales', name: 'Sales Performance', value: 85, type: 'predator', color: '#ff4444' },
|
|
{ id: 'users', name: 'User Engagement', value: 72, type: 'prey', color: '#44ff44' },
|
|
{ id: 'revenue', name: 'Revenue Growth', value: 91, type: 'predator', color: '#ff6644' },
|
|
{ id: 'support', name: 'Support Tickets', value: 23, type: 'decomposer', color: '#ff8844' },
|
|
{ id: 'server', name: 'Server Performance', value: 67, type: 'producer', color: '#44ff88' },
|
|
{ id: 'conversion', name: 'Conversion Rate', value: 45, type: 'prey', color: '#66ff44' },
|
|
{ id: 'retention', name: 'User Retention', value: 78, type: 'producer', color: '#44ff66' },
|
|
{ id: 'load_time', name: 'Page Load Time', value: 34, type: 'decomposer', color: '#ffaa44' }
|
|
];
|
|
|
|
this.initializeControls();
|
|
this.createEcosystem();
|
|
this.startSimulation();
|
|
this.initializeEvents();
|
|
}
|
|
|
|
initializeControls() {
|
|
const populationSlider = document.getElementById('populationSlider');
|
|
const speedSlider = document.getElementById('speedSlider');
|
|
const diversitySlider = document.getElementById('diversitySlider');
|
|
const addDataBtn = document.getElementById('addDataBtn');
|
|
const resetBtn = document.getElementById('resetEcosystemBtn');
|
|
|
|
populationSlider.addEventListener('input', (e) => {
|
|
this.adjustPopulation(parseInt(e.target.value));
|
|
});
|
|
|
|
speedSlider.addEventListener('input', (e) => {
|
|
this.timeSpeed = parseFloat(e.target.value);
|
|
});
|
|
|
|
diversitySlider.addEventListener('input', (e) => {
|
|
this.adjustDiversity(parseFloat(e.target.value));
|
|
});
|
|
|
|
addDataBtn.addEventListener('click', () => {
|
|
this.addRandomDataPoint();
|
|
});
|
|
|
|
resetBtn.addEventListener('click', () => {
|
|
this.resetEcosystem();
|
|
});
|
|
}
|
|
|
|
createEcosystem() {
|
|
this.organisms = [];
|
|
const containerRect = this.container.getBoundingClientRect();
|
|
|
|
this.dataPoints.forEach((dataPoint, index) => {
|
|
const organism = this.createOrganism(dataPoint, containerRect);
|
|
this.organisms.push(organism);
|
|
this.container.appendChild(organism.element);
|
|
});
|
|
|
|
this.updateStats();
|
|
}
|
|
|
|
createOrganism(dataPoint, containerRect) {
|
|
const element = document.createElement('div');
|
|
element.className = `organism ${dataPoint.type}`;
|
|
element.tabIndex = 0;
|
|
element.setAttribute('role', 'button');
|
|
element.setAttribute('aria-label', `${dataPoint.name}: ${dataPoint.value}%`);
|
|
|
|
const size = 20 + (dataPoint.value / 100) * 40;
|
|
const x = Math.random() * (containerRect.width - size);
|
|
const y = Math.random() * (containerRect.height - size);
|
|
|
|
element.style.width = size + 'px';
|
|
element.style.height = size + 'px';
|
|
element.style.left = x + 'px';
|
|
element.style.top = y + 'px';
|
|
element.style.background = `radial-gradient(circle at 30% 30%, ${dataPoint.color}, ${this.darkenColor(dataPoint.color, 0.3)})`;
|
|
element.textContent = dataPoint.name.substring(0, 3).toUpperCase();
|
|
|
|
const organism = {
|
|
element: element,
|
|
data: dataPoint,
|
|
x: x,
|
|
y: y,
|
|
vx: (Math.random() - 0.5) * 2,
|
|
vy: (Math.random() - 0.5) * 2,
|
|
size: size,
|
|
energy: dataPoint.value,
|
|
age: 0,
|
|
targetX: x,
|
|
targetY: y
|
|
};
|
|
|
|
this.addOrganismEvents(organism);
|
|
return organism;
|
|
}
|
|
|
|
addOrganismEvents(organism) {
|
|
organism.element.addEventListener('mouseenter', (e) => {
|
|
this.showTooltip(e, organism);
|
|
});
|
|
|
|
organism.element.addEventListener('mouseleave', () => {
|
|
this.hideTooltip();
|
|
});
|
|
|
|
organism.element.addEventListener('click', () => {
|
|
this.selectOrganism(organism);
|
|
});
|
|
|
|
organism.element.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
e.preventDefault();
|
|
this.selectOrganism(organism);
|
|
}
|
|
});
|
|
}
|
|
|
|
showTooltip(event, organism) {
|
|
const tooltip = this.tooltip;
|
|
tooltip.innerHTML = `
|
|
<strong>${organism.data.name}</strong><br>
|
|
Value: ${organism.data.value}%<br>
|
|
Type: ${organism.data.type}<br>
|
|
Energy: ${Math.round(organism.energy)}%<br>
|
|
Age: ${Math.round(organism.age)}s
|
|
`;
|
|
tooltip.style.left = event.pageX + 10 + 'px';
|
|
tooltip.style.top = event.pageY - 10 + 'px';
|
|
tooltip.style.opacity = '1';
|
|
}
|
|
|
|
hideTooltip() {
|
|
this.tooltip.style.opacity = '0';
|
|
}
|
|
|
|
selectOrganism(organism) {
|
|
// Remove previous selections
|
|
this.organisms.forEach(o => {
|
|
o.element.style.border = o.element.style.border.replace('4px solid yellow', '');
|
|
});
|
|
|
|
// Highlight selected organism
|
|
organism.element.style.border = '4px solid yellow';
|
|
|
|
// Show related organisms based on type
|
|
this.highlightRelatedOrganisms(organism);
|
|
|
|
// Update ecosystem mood based on selection
|
|
this.updateEcosystemMood(organism);
|
|
}
|
|
|
|
highlightRelatedOrganisms(selectedOrganism) {
|
|
this.organisms.forEach(organism => {
|
|
if (organism !== selectedOrganism) {
|
|
const relationship = this.calculateRelationship(selectedOrganism, organism);
|
|
if (relationship > 0.5) {
|
|
organism.element.style.boxShadow = '0 0 20px rgba(255, 255, 0, 0.8)';
|
|
setTimeout(() => {
|
|
organism.element.style.boxShadow = '';
|
|
}, 2000);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
calculateRelationship(org1, org2) {
|
|
// Simple relationship based on value similarity and type compatibility
|
|
const valueSimilarity = 1 - Math.abs(org1.data.value - org2.data.value) / 100;
|
|
const typeCompatibility = this.getTypeCompatibility(org1.data.type, org2.data.type);
|
|
return (valueSimilarity + typeCompatibility) / 2;
|
|
}
|
|
|
|
getTypeCompatibility(type1, type2) {
|
|
const relationships = {
|
|
'predator': { 'prey': 0.8, 'predator': 0.3, 'producer': 0.2, 'decomposer': 0.1 },
|
|
'prey': { 'producer': 0.9, 'prey': 0.6, 'predator': 0.1, 'decomposer': 0.3 },
|
|
'producer': { 'decomposer': 0.7, 'prey': 0.8, 'producer': 0.5, 'predator': 0.2 },
|
|
'decomposer': { 'producer': 0.6, 'decomposer': 0.4, 'predator': 0.2, 'prey': 0.3 }
|
|
};
|
|
return relationships[type1]?.[type2] || 0.1;
|
|
}
|
|
|
|
updateSimulation() {
|
|
const containerRect = this.container.getBoundingClientRect();
|
|
|
|
this.organisms.forEach(organism => {
|
|
// Age the organism
|
|
organism.age += 0.016 * this.timeSpeed;
|
|
|
|
// Update energy based on type and age
|
|
this.updateOrganismEnergy(organism);
|
|
|
|
// Calculate movement based on type behavior
|
|
this.updateOrganismMovement(organism, containerRect);
|
|
|
|
// Update visual representation
|
|
this.updateOrganismVisuals(organism);
|
|
});
|
|
|
|
this.updateStats();
|
|
this.updateEcosystemMoodAuto();
|
|
}
|
|
|
|
updateOrganismEnergy(organism) {
|
|
const baseDecay = 0.1 * this.timeSpeed;
|
|
const typeModifier = {
|
|
'predator': 0.5,
|
|
'prey': 1.0,
|
|
'producer': 1.5,
|
|
'decomposer': 0.8
|
|
};
|
|
|
|
organism.energy -= baseDecay * typeModifier[organism.data.type];
|
|
organism.energy = Math.max(0, Math.min(100, organism.energy));
|
|
|
|
// Regenerate energy based on data value
|
|
if (organism.data.value > 70) {
|
|
organism.energy += 0.2 * this.timeSpeed;
|
|
}
|
|
}
|
|
|
|
updateOrganismMovement(organism, containerRect) {
|
|
// Different movement patterns based on type
|
|
switch (organism.data.type) {
|
|
case 'predator':
|
|
this.updatePredatorMovement(organism, containerRect);
|
|
break;
|
|
case 'prey':
|
|
this.updatePreyMovement(organism, containerRect);
|
|
break;
|
|
case 'producer':
|
|
this.updateProducerMovement(organism, containerRect);
|
|
break;
|
|
case 'decomposer':
|
|
this.updateDecomposerMovement(organism, containerRect);
|
|
break;
|
|
}
|
|
|
|
// Apply movement
|
|
organism.x += organism.vx * this.timeSpeed;
|
|
organism.y += organism.vy * this.timeSpeed;
|
|
|
|
// Boundary checking
|
|
this.enforceBoundaries(organism, containerRect);
|
|
}
|
|
|
|
updatePredatorMovement(organism, containerRect) {
|
|
// Hunt for prey
|
|
const nearestPrey = this.findNearestOrganism(organism, 'prey');
|
|
if (nearestPrey) {
|
|
const dx = nearestPrey.x - organism.x;
|
|
const dy = nearestPrey.y - organism.y;
|
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
if (distance > 0) {
|
|
organism.vx += (dx / distance) * 0.1;
|
|
organism.vy += (dy / distance) * 0.1;
|
|
}
|
|
}
|
|
|
|
// Apply damping
|
|
organism.vx *= 0.95;
|
|
organism.vy *= 0.95;
|
|
}
|
|
|
|
updatePreyMovement(organism, containerRect) {
|
|
// Flee from predators
|
|
const nearestPredator = this.findNearestOrganism(organism, 'predator');
|
|
if (nearestPredator) {
|
|
const dx = organism.x - nearestPredator.x;
|
|
const dy = organism.y - nearestPredator.y;
|
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
if (distance < 100 && distance > 0) {
|
|
organism.vx += (dx / distance) * 0.2;
|
|
organism.vy += (dy / distance) * 0.2;
|
|
}
|
|
}
|
|
|
|
// Seek producers for food
|
|
const nearestProducer = this.findNearestOrganism(organism, 'producer');
|
|
if (nearestProducer) {
|
|
const dx = nearestProducer.x - organism.x;
|
|
const dy = nearestProducer.y - organism.y;
|
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
if (distance > 0) {
|
|
organism.vx += (dx / distance) * 0.05;
|
|
organism.vy += (dy / distance) * 0.05;
|
|
}
|
|
}
|
|
|
|
organism.vx *= 0.9;
|
|
organism.vy *= 0.9;
|
|
}
|
|
|
|
updateProducerMovement(organism, containerRect) {
|
|
// Slow, steady movement toward light (top of container)
|
|
const lightSeek = (containerRect.height * 0.3 - organism.y) * 0.001;
|
|
organism.vy += lightSeek;
|
|
|
|
// Random gentle movement
|
|
organism.vx += (Math.random() - 0.5) * 0.02;
|
|
organism.vy += (Math.random() - 0.5) * 0.02;
|
|
|
|
organism.vx *= 0.98;
|
|
organism.vy *= 0.98;
|
|
}
|
|
|
|
updateDecomposerMovement(organism, containerRect) {
|
|
// Move toward bottom and seek dead organic matter
|
|
const bottomSeek = (containerRect.height * 0.8 - organism.y) * 0.002;
|
|
organism.vy += bottomSeek;
|
|
|
|
// Random movement
|
|
organism.vx += (Math.random() - 0.5) * 0.05;
|
|
organism.vy += (Math.random() - 0.5) * 0.02;
|
|
|
|
organism.vx *= 0.85;
|
|
organism.vy *= 0.85;
|
|
}
|
|
|
|
findNearestOrganism(organism, type) {
|
|
let nearest = null;
|
|
let minDistance = Infinity;
|
|
|
|
this.organisms.forEach(other => {
|
|
if (other !== organism && other.data.type === type) {
|
|
const dx = other.x - organism.x;
|
|
const dy = other.y - organism.y;
|
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
if (distance < minDistance) {
|
|
minDistance = distance;
|
|
nearest = other;
|
|
}
|
|
}
|
|
});
|
|
|
|
return nearest;
|
|
}
|
|
|
|
enforceBoundaries(organism, containerRect) {
|
|
const margin = organism.size / 2;
|
|
|
|
if (organism.x < margin) {
|
|
organism.x = margin;
|
|
organism.vx = Math.abs(organism.vx);
|
|
}
|
|
if (organism.x > containerRect.width - margin) {
|
|
organism.x = containerRect.width - margin;
|
|
organism.vx = -Math.abs(organism.vx);
|
|
}
|
|
if (organism.y < margin) {
|
|
organism.y = margin;
|
|
organism.vy = Math.abs(organism.vy);
|
|
}
|
|
if (organism.y > containerRect.height - margin) {
|
|
organism.y = containerRect.height - margin;
|
|
organism.vy = -Math.abs(organism.vy);
|
|
}
|
|
}
|
|
|
|
updateOrganismVisuals(organism) {
|
|
organism.element.style.left = organism.x - organism.size/2 + 'px';
|
|
organism.element.style.top = organism.y - organism.size/2 + 'px';
|
|
|
|
// Update size based on energy
|
|
const energyScale = 0.7 + (organism.energy / 100) * 0.6;
|
|
organism.element.style.transform = `scale(${energyScale})`;
|
|
|
|
// Update opacity based on energy
|
|
organism.element.style.opacity = 0.6 + (organism.energy / 100) * 0.4;
|
|
}
|
|
|
|
updateStats() {
|
|
const stats = this.getEcosystemStats();
|
|
document.getElementById('totalOrganisms').textContent = stats.total;
|
|
document.getElementById('producerCount').textContent = stats.producers;
|
|
document.getElementById('consumerCount').textContent = stats.consumers;
|
|
document.getElementById('predatorCount').textContent = stats.predators;
|
|
document.getElementById('systemHealth').textContent = stats.health + '%';
|
|
document.getElementById('dataQuality').textContent = stats.quality;
|
|
}
|
|
|
|
getEcosystemStats() {
|
|
const total = this.organisms.length;
|
|
const producers = this.organisms.filter(o => o.data.type === 'producer').length;
|
|
const consumers = this.organisms.filter(o => o.data.type === 'prey').length;
|
|
const predators = this.organisms.filter(o => o.data.type === 'predator').length;
|
|
|
|
const avgEnergy = this.organisms.reduce((sum, o) => sum + o.energy, 0) / total;
|
|
const avgValue = this.organisms.reduce((sum, o) => sum + o.data.value, 0) / total;
|
|
|
|
const health = Math.round(avgEnergy);
|
|
const quality = avgValue > 80 ? 'Excellent' : avgValue > 60 ? 'Good' : avgValue > 40 ? 'Fair' : 'Poor';
|
|
|
|
return { total, producers, consumers, predators, health, quality };
|
|
}
|
|
|
|
updateEcosystemMoodAuto() {
|
|
const stats = this.getEcosystemStats();
|
|
const moodElement = document.getElementById('ecosystemMood');
|
|
|
|
if (stats.health > 80) {
|
|
moodElement.textContent = '🌱 Ecosystem is Thriving';
|
|
moodElement.style.color = '#44ff44';
|
|
} else if (stats.health > 60) {
|
|
moodElement.textContent = '🌿 Ecosystem is Stable';
|
|
moodElement.style.color = '#88ff88';
|
|
} else if (stats.health > 40) {
|
|
moodElement.textContent = '🍂 Ecosystem is Struggling';
|
|
moodElement.style.color = '#ffaa44';
|
|
} else {
|
|
moodElement.textContent = '🌪️ Ecosystem in Crisis';
|
|
moodElement.style.color = '#ff4444';
|
|
}
|
|
}
|
|
|
|
updateEcosystemMood(organism) {
|
|
const moodElement = document.getElementById('ecosystemMood');
|
|
moodElement.textContent = `🔍 Analyzing ${organism.data.name} - ${organism.data.value}% Performance`;
|
|
moodElement.style.color = '#ffff44';
|
|
|
|
setTimeout(() => {
|
|
this.updateEcosystemMoodAuto();
|
|
}, 3000);
|
|
}
|
|
|
|
adjustPopulation(targetSize) {
|
|
while (this.organisms.length < targetSize) {
|
|
this.addRandomDataPoint();
|
|
}
|
|
while (this.organisms.length > targetSize) {
|
|
this.removeRandomOrganism();
|
|
}
|
|
}
|
|
|
|
adjustDiversity(diversityLevel) {
|
|
// Adjust organism types based on diversity level
|
|
this.organisms.forEach(organism => {
|
|
if (Math.random() < diversityLevel * 0.1) {
|
|
this.mutateOrganism(organism);
|
|
}
|
|
});
|
|
}
|
|
|
|
addRandomDataPoint() {
|
|
const types = ['predator', 'prey', 'producer', 'decomposer'];
|
|
const colors = ['#ff4444', '#44ff44', '#44ff88', '#ff8844'];
|
|
const names = ['Performance', 'Efficiency', 'Quality', 'Speed', 'Accuracy', 'Stability'];
|
|
|
|
const newData = {
|
|
id: 'random_' + Date.now(),
|
|
name: names[Math.floor(Math.random() * names.length)] + ' Metric',
|
|
value: Math.round(Math.random() * 100),
|
|
type: types[Math.floor(Math.random() * types.length)],
|
|
color: colors[Math.floor(Math.random() * colors.length)]
|
|
};
|
|
|
|
const containerRect = this.container.getBoundingClientRect();
|
|
const organism = this.createOrganism(newData, containerRect);
|
|
this.organisms.push(organism);
|
|
this.container.appendChild(organism.element);
|
|
}
|
|
|
|
removeRandomOrganism() {
|
|
if (this.organisms.length > 1) {
|
|
const index = Math.floor(Math.random() * this.organisms.length);
|
|
const organism = this.organisms[index];
|
|
organism.element.remove();
|
|
this.organisms.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
mutateOrganism(organism) {
|
|
const types = ['predator', 'prey', 'producer', 'decomposer'];
|
|
const newType = types[Math.floor(Math.random() * types.length)];
|
|
organism.data.type = newType;
|
|
organism.element.className = `organism ${newType}`;
|
|
}
|
|
|
|
resetEcosystem() {
|
|
this.organisms.forEach(organism => organism.element.remove());
|
|
this.createEcosystem();
|
|
}
|
|
|
|
darkenColor(color, amount) {
|
|
const hex = color.replace('#', '');
|
|
const r = Math.max(0, parseInt(hex.substr(0, 2), 16) - Math.round(255 * amount));
|
|
const g = Math.max(0, parseInt(hex.substr(2, 2), 16) - Math.round(255 * amount));
|
|
const b = Math.max(0, parseInt(hex.substr(4, 2), 16) - Math.round(255 * amount));
|
|
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
|
}
|
|
|
|
startSimulation() {
|
|
const animate = () => {
|
|
this.updateSimulation();
|
|
this.animationId = requestAnimationFrame(animate);
|
|
};
|
|
animate();
|
|
}
|
|
|
|
initializeEvents() {
|
|
// Handle window resize
|
|
window.addEventListener('resize', () => {
|
|
const containerRect = this.container.getBoundingClientRect();
|
|
this.organisms.forEach(organism => {
|
|
this.enforceBoundaries(organism, containerRect);
|
|
});
|
|
});
|
|
|
|
// Keyboard navigation for container
|
|
this.container.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') {
|
|
// Deselect all organisms
|
|
this.organisms.forEach(o => {
|
|
o.element.style.border = o.element.style.border.replace('4px solid yellow', '');
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
destroy() {
|
|
if (this.animationId) {
|
|
cancelAnimationFrame(this.animationId);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialize EcoSystem Display when page loads
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const container = document.querySelector('#ecosystemContainer');
|
|
const ecoSystem = new EcoSystemDisplay(container);
|
|
|
|
// Make it available globally
|
|
window.ecoSystem = ecoSystem;
|
|
|
|
console.log('EcoSystem Display initialized. Watch the organisms interact!');
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |