improve structure; move stuff to relevant folders
This commit is contained in:
619
scripts/osrs/autoclicker/click-visualizer.html
Normal file
619
scripts/osrs/autoclicker/click-visualizer.html
Normal file
@@ -0,0 +1,619 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Autoclicker Timing Visualizer</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #2c3e50;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
input, select, button {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #2980b9;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background-color: #95a5a6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
background-color: #ecf0f1;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #7f8c8d;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
background-color: white;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
position: relative;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
position: relative;
|
||||
height: 300px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.file-item.active {
|
||||
background-color: #e3f2fd;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
color: #3498db;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
font-size: 12px;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #7f8c8d;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #e74c3c;
|
||||
padding: 10px;
|
||||
background-color: #fadbd8;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.server-info {
|
||||
background-color: #e8f5e9;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
border-left: 4px solid #4caf50;
|
||||
}
|
||||
|
||||
.api-status {
|
||||
background-color: #fff3cd;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.api-status.success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.api-status.error {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.controls, .stats {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🖱️ Autoclicker Timing Visualizer</h1>
|
||||
|
||||
<div class="server-info">
|
||||
<strong>📊 Real-time Data Server</strong>
|
||||
<p>This visualizer connects to a local Python server that serves real log files from <code>/tmp/autoclicker_logs/</code></p>
|
||||
<p>Make sure to run: <code>python3 click_server.py</code> in a separate terminal</p>
|
||||
</div>
|
||||
|
||||
<div id="apiStatus" class="api-status">
|
||||
Testing API connection...
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
<label for="logFile">Select Log File:</label>
|
||||
<select id="logFile">
|
||||
<option value="">-- Loading log files... --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label for="chartType">Chart Type:</label>
|
||||
<select id="chartType">
|
||||
<option value="scatter">Scatter Plot</option>
|
||||
<option value="line">Line Chart</option>
|
||||
<option value="histogram">Histogram</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label> </label>
|
||||
<button id="loadBtn" disabled>Load Data</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats" id="statsContainer" style="display: none;">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Total Clicks</div>
|
||||
<div class="stat-value" id="totalClicks">0</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Average Delay</div>
|
||||
<div class="stat-value" id="avgDelay">0 ms</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Min Delay</div>
|
||||
<div class="stat-value" id="minDelay">0 ms</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Max Delay</div>
|
||||
<div class="stat-value" id="maxDelay">0 ms</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Std Deviation</div>
|
||||
<div class="stat-value" id="stdDev">0 ms</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<div class="chart-title">Click Timing Distribution</div>
|
||||
<div class="chart-wrapper">
|
||||
<canvas id="timingChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<div class="chart-title">Delay Over Time</div>
|
||||
<div class="chart-wrapper">
|
||||
<canvas id="timeSeriesChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="file-list">
|
||||
<h3>Available Log Files</h3>
|
||||
<div id="fileList">
|
||||
<div class="loading">Loading log files from server...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Load Chart.js with date adapter -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0"></script>
|
||||
|
||||
<script>
|
||||
// Global variables
|
||||
let clickData = [];
|
||||
let timingChart, timeSeriesChart;
|
||||
let currentFile = '';
|
||||
|
||||
// API base URL
|
||||
const API_BASE = '/api';
|
||||
|
||||
// DOM elements
|
||||
const logFileSelect = document.getElementById('logFile');
|
||||
const chartTypeSelect = document.getElementById('chartType');
|
||||
const loadBtn = document.getElementById('loadBtn');
|
||||
const fileList = document.getElementById('fileList');
|
||||
const statsContainer = document.getElementById('statsContainer');
|
||||
const apiStatus = document.getElementById('apiStatus');
|
||||
|
||||
// Test API connection
|
||||
async function testAPIConnection() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/logs`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
apiStatus.textContent = "✅ API connected successfully!";
|
||||
apiStatus.className = "api-status success";
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
apiStatus.textContent = `❌ API connection failed: ${error.message}. Is the Python server running?`;
|
||||
apiStatus.className = "api-status error";
|
||||
console.error('API connection error:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize charts with proper configuration
|
||||
function initCharts() {
|
||||
const timingCtx = document.getElementById('timingChart').getContext('2d');
|
||||
const timeSeriesCtx = document.getElementById('timeSeriesChart').getContext('2d');
|
||||
|
||||
// Destroy existing charts if they exist
|
||||
if (timingChart) timingChart.destroy();
|
||||
if (timeSeriesChart) timeSeriesChart.destroy();
|
||||
|
||||
timingChart = new Chart(timingCtx, {
|
||||
type: 'scatter',
|
||||
data: { datasets: [] },
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: {
|
||||
type: 'linear',
|
||||
title: { display: true, text: 'Click Number' }
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: { display: true, text: 'Delay (ms)' }
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
return `Click ${context.raw.x}: ${context.raw.y}ms`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
timeSeriesChart = new Chart(timeSeriesCtx, {
|
||||
type: 'line',
|
||||
data: { datasets: [] },
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: {
|
||||
type: 'linear',
|
||||
title: { display: true, text: 'Click Number' }
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: { display: true, text: 'Delay (ms)' }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Load available log files from server
|
||||
async function loadLogFiles() {
|
||||
try {
|
||||
fileList.innerHTML = '<div class="loading">Loading log files...</div>';
|
||||
logFileSelect.innerHTML = '<option value="">-- Loading... --</option>';
|
||||
|
||||
const response = await fetch(`${API_BASE}/logs`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server error: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
|
||||
const files = data.logs;
|
||||
|
||||
if (files.length === 0) {
|
||||
fileList.innerHTML = '<div class="error">No log files found. Run the enhanced autoclicker first.</div>';
|
||||
logFileSelect.innerHTML = '<option value="">-- No log files available --</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Populate select dropdown
|
||||
logFileSelect.innerHTML = '<option value="">-- Select a log file --</option>';
|
||||
files.forEach(file => {
|
||||
const option = document.createElement('option');
|
||||
option.value = file.name;
|
||||
option.textContent = `${file.name} (${file.date})`;
|
||||
logFileSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// Populate file list
|
||||
fileList.innerHTML = files.map(file => `
|
||||
<div class="file-item" data-file="${file.name}">
|
||||
<div class="file-name">${file.name}</div>
|
||||
<div class="file-info">Size: ${file.size} | Created: ${file.date}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Add click handlers to file items
|
||||
document.querySelectorAll('.file-item').forEach(item => {
|
||||
item.addEventListener('click', function() {
|
||||
document.querySelectorAll('.file-item').forEach(i => i.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
logFileSelect.value = this.dataset.file;
|
||||
loadBtn.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading log files:', error);
|
||||
fileList.innerHTML = `<div class="error">Error loading log files: ${error.message}</div>`;
|
||||
logFileSelect.innerHTML = '<option value="">-- Error loading files --</option>';
|
||||
}
|
||||
}
|
||||
|
||||
// Load and parse JSON data from server
|
||||
async function loadData() {
|
||||
const fileName = logFileSelect.value;
|
||||
if (!fileName) return;
|
||||
|
||||
currentFile = fileName;
|
||||
loadBtn.disabled = true;
|
||||
loadBtn.textContent = 'Loading...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/log/${encodeURIComponent(fileName)}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server error: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data.error) {
|
||||
if (data.content_preview) {
|
||||
fileList.innerHTML = `<div class="error">Invalid JSON in ${fileName}: ${data.error}<br><br>File content preview:<pre>${data.content_preview}</pre></div>`;
|
||||
} else {
|
||||
fileList.innerHTML = `<div class="error">Error loading ${fileName}: ${data.error}</div>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.valid) {
|
||||
fileList.innerHTML = `<div class="error">File ${fileName} is not valid JSON: ${data.error || 'Unknown error'}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
clickData = data.data;
|
||||
updateCharts();
|
||||
updateStats();
|
||||
statsContainer.style.display = 'flex';
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error);
|
||||
fileList.innerHTML = `<div class="error">Error loading ${fileName}: ${error.message}</div>`;
|
||||
} finally {
|
||||
loadBtn.disabled = false;
|
||||
loadBtn.textContent = 'Load Data';
|
||||
}
|
||||
}
|
||||
|
||||
// Update charts with current data
|
||||
function updateCharts() {
|
||||
if (clickData.length === 0) return;
|
||||
|
||||
// Update timing chart based on selected type
|
||||
const chartType = chartTypeSelect.value;
|
||||
|
||||
if (chartType === 'scatter' || chartType === 'line') {
|
||||
const dataPoints = clickData.map(item => ({
|
||||
x: item.click,
|
||||
y: item.delay_ms
|
||||
}));
|
||||
|
||||
timingChart.data.datasets = [{
|
||||
label: 'Click Delays',
|
||||
data: dataPoints,
|
||||
type: chartType,
|
||||
backgroundColor: 'rgba(52, 152, 219, 0.5)',
|
||||
borderColor: 'rgba(52, 152, 219, 1)',
|
||||
pointRadius: chartType === 'scatter' ? 4 : 2,
|
||||
tension: chartType === 'line' ? 0.3 : 0,
|
||||
pointStyle: 'circle'
|
||||
}];
|
||||
timingChart.update();
|
||||
} else if (chartType === 'histogram') {
|
||||
// Create histogram data
|
||||
const delays = clickData.map(item => item.delay_ms);
|
||||
const bins = createHistogram(delays, 20);
|
||||
const histogramData = bins.map((count, i) => ({
|
||||
x: i * (700 / 20) + 50,
|
||||
y: count
|
||||
}));
|
||||
|
||||
timingChart.data.datasets = [{
|
||||
label: 'Delay Distribution',
|
||||
data: histogramData,
|
||||
type: 'bar',
|
||||
backgroundColor: 'rgba(52, 152, 219, 0.7)',
|
||||
borderColor: 'rgba(52, 152, 219, 1)',
|
||||
barPercentage: 0.9
|
||||
}];
|
||||
timingChart.options.scales.x.title.text = 'Delay Range (ms)';
|
||||
timingChart.options.scales.y.title.text = 'Frequency';
|
||||
timingChart.update();
|
||||
}
|
||||
|
||||
// Update time series chart (using click number instead of time for simplicity)
|
||||
const timeSeriesData = clickData.map(item => ({
|
||||
x: item.click,
|
||||
y: item.delay_ms
|
||||
}));
|
||||
|
||||
timeSeriesChart.data.datasets = [{
|
||||
label: 'Delay Over Time',
|
||||
data: timeSeriesData,
|
||||
borderColor: 'rgba(46, 204, 113, 1)',
|
||||
backgroundColor: 'rgba(46, 204, 113, 0.1)',
|
||||
tension: 0.3,
|
||||
pointRadius: 2,
|
||||
pointStyle: 'circle'
|
||||
}];
|
||||
timeSeriesChart.update();
|
||||
}
|
||||
|
||||
// Create histogram from data
|
||||
function createHistogram(data, binCount) {
|
||||
if (data.length === 0) return [];
|
||||
|
||||
const min = Math.min(...data);
|
||||
const max = Math.max(...data);
|
||||
const binSize = (max - min) / binCount;
|
||||
const bins = new Array(binCount).fill(0);
|
||||
|
||||
data.forEach(value => {
|
||||
const binIndex = Math.min(Math.floor((value - min) / binSize), binCount - 1);
|
||||
bins[binIndex]++;
|
||||
});
|
||||
|
||||
return bins;
|
||||
}
|
||||
|
||||
// Update statistics
|
||||
function updateStats() {
|
||||
if (clickData.length === 0) return;
|
||||
|
||||
const delays = clickData.map(item => item.delay_ms);
|
||||
const total = delays.length;
|
||||
const sum = delays.reduce((a, b) => a + b, 0);
|
||||
const avg = sum / total;
|
||||
const min = Math.min(...delays);
|
||||
const max = Math.max(...delays);
|
||||
|
||||
// Calculate standard deviation
|
||||
const variance = delays.reduce((sq, n) => sq + Math.pow(n - avg, 2), 0) / total;
|
||||
const stdDev = Math.sqrt(variance);
|
||||
|
||||
document.getElementById('totalClicks').textContent = total;
|
||||
document.getElementById('avgDelay').textContent = `${avg.toFixed(1)} ms`;
|
||||
document.getElementById('minDelay').textContent = `${min} ms`;
|
||||
document.getElementById('maxDelay').textContent = `${max} ms`;
|
||||
document.getElementById('stdDev').textContent = `${stdDev.toFixed(1)} ms`;
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
logFileSelect.addEventListener('change', function() {
|
||||
loadBtn.disabled = !this.value;
|
||||
});
|
||||
|
||||
chartTypeSelect.addEventListener('change', updateCharts);
|
||||
|
||||
loadBtn.addEventListener('click', loadData);
|
||||
|
||||
// Initialize
|
||||
initCharts();
|
||||
|
||||
// Test API connection first
|
||||
testAPIConnection().then(isConnected => {
|
||||
if (isConnected) {
|
||||
loadLogFiles();
|
||||
// Auto-refresh log list every 10 seconds
|
||||
setInterval(loadLogFiles, 10000);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user