423 lines
17 KiB
HTML
423 lines
17 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Dog Training Progress - Sit Command</title>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
|
|
<!--
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/date-fns/2.29.3/index.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-adapter-date-fns/2.0.0/chartjs-adapter-date-fns.bundle.min.js"></script>
|
|
-->
|
|
<style>
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
margin: 0;
|
|
padding: 20px;
|
|
background-color: #f8fafc;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
background: white;
|
|
border-radius: 12px;
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
padding: 24px;
|
|
}
|
|
|
|
.header {
|
|
margin-bottom: 24px;
|
|
border-bottom: 1px solid #e2e8f0;
|
|
padding-bottom: 16px;
|
|
}
|
|
|
|
.header h1 {
|
|
margin: 0 0 8px 0;
|
|
color: #1e293b;
|
|
font-size: 24px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.header p {
|
|
margin: 0;
|
|
color: #64748b;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.chart-container {
|
|
position: relative;
|
|
height: 500px;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.legend-container {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 12px;
|
|
margin-top: 16px;
|
|
padding: 16px;
|
|
background-color: #f8fafc;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.legend-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 12px;
|
|
color: #475569;
|
|
}
|
|
|
|
.legend-color {
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.metrics-summary {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 16px;
|
|
margin-top: 24px;
|
|
}
|
|
|
|
.metric-card {
|
|
background: #f8fafc;
|
|
padding: 16px;
|
|
border-radius: 8px;
|
|
border-left: 4px solid #3b82f6;
|
|
}
|
|
|
|
.metric-label {
|
|
font-size: 12px;
|
|
color: #64748b;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.metric-value {
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
color: #1e293b;
|
|
}
|
|
|
|
.metric-change {
|
|
font-size: 12px;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.positive { color: #059669; }
|
|
.negative { color: #dc2626; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>Training Progress Report - "Sit" Command</h1>
|
|
<p>Tracking compliance duration and response time from April 1st - July 30th, 2025</p>
|
|
</div>
|
|
|
|
<div class="chart-container">
|
|
<canvas id="trainingChart"></canvas>
|
|
</div>
|
|
|
|
<div class="legend-container" id="obedienceLegend">
|
|
<!-- Legend will be populated by JavaScript -->
|
|
</div>
|
|
|
|
<div class="metrics-summary">
|
|
<div class="metric-card">
|
|
<div class="metric-label">Average Compliance Duration</div>
|
|
<div class="metric-value">65.2s</div>
|
|
<div class="metric-change positive">+450% from start</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-label">Average Response Time</div>
|
|
<div class="metric-value">4.3s</div>
|
|
<div class="metric-change positive">-64% from start</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-label">Most Common Obedience Level</div>
|
|
<div class="metric-value">After Firm Look</div>
|
|
<div class="metric-change">Recent sessions</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-label">Training Sessions</div>
|
|
<div class="metric-value">32</div>
|
|
<div class="metric-change">4 months period</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Define obedience levels and their corresponding colors
|
|
const obedienceLevels = [
|
|
{ name: "Eager", color: "#16a34a", value: 15 },
|
|
{ name: "After Firm Look", color: "#22c55e", value: 14 },
|
|
{ name: "After Power Pose", color: "#65a30d", value: 13 },
|
|
{ name: "After Threatening to Approach", color: "#84cc16", value: 12 },
|
|
{ name: "After Moving a Few Steps", color: "#eab308", value: 11 },
|
|
{ name: "After Moving Half Way", color: "#f59e0b", value: 10 },
|
|
{ name: "After Approaching Most of The Way", color: "#f97316", value: 9 },
|
|
{ name: "After Touching Collar", color: "#ea580c", value: 8 },
|
|
{ name: "After Running Away And Returning", color: "#dc2626", value: 7 },
|
|
{ name: "After Being Lead by Collar or Lead", color: "#b91c1c", value: 6 },
|
|
{ name: "After Check or Bribe", color: "#991b1b", value: 5 },
|
|
{ name: "After Multiple Checks and/or Bribes", color: "#7f1d1d", value: 4 },
|
|
{ name: "None - Refusal", color: "#6b7280", value: 3 },
|
|
{ name: "Refused and Ran Away", color: "#4b5563", value: 2 },
|
|
{ name: "Refusing to Return", color: "#374151", value: 1 }
|
|
];
|
|
|
|
// Generate training data from April 1st to July 30th, 2025
|
|
function generateTrainingData() {
|
|
const data = [];
|
|
const startDate = new Date('2025-04-01');
|
|
const endDate = new Date('2025-07-30');
|
|
|
|
let currentDate = new Date(startDate);
|
|
let complianceDuration = 10; // Starting at 10 seconds
|
|
let responseTime = 12; // Starting at 12 seconds
|
|
|
|
while (currentDate <= endDate) {
|
|
// Add some volatility and general improvement trend
|
|
const volatility = (Math.random() - 0.5) * 0.3;
|
|
const progressFactor = (currentDate - startDate) / (endDate - startDate);
|
|
|
|
// Compliance duration: improve from 10s to 120s with volatility
|
|
const targetCompliance = 10 + (110 * progressFactor);
|
|
complianceDuration = Math.max(5, Math.min(120,
|
|
complianceDuration + (targetCompliance - complianceDuration) * 0.1 +
|
|
volatility * 15 + (Math.random() - 0.5) * 10
|
|
));
|
|
|
|
// Response time: improve from 12s to 2s with volatility
|
|
const targetResponse = 12 - (10 * progressFactor);
|
|
responseTime = Math.max(1, Math.min(15,
|
|
responseTime + (targetResponse - responseTime) * 0.1 +
|
|
volatility * 2 + (Math.random() - 0.5) * 2
|
|
));
|
|
|
|
// Determine obedience level based on performance
|
|
let obedienceLevel;
|
|
if (complianceDuration > 90 && responseTime < 3) {
|
|
obedienceLevel = obedienceLevels[0]; // Eager
|
|
} else if (complianceDuration > 70 && responseTime < 4) {
|
|
obedienceLevel = obedienceLevels[1]; // After Firm Look
|
|
} else if (complianceDuration > 50 && responseTime < 5) {
|
|
obedienceLevel = obedienceLevels[2]; // After Power Pose
|
|
} else if (complianceDuration > 40 && responseTime < 6) {
|
|
obedienceLevel = obedienceLevels[3]; // After Threatening to Approach
|
|
} else if (complianceDuration > 30 && responseTime < 7) {
|
|
obedienceLevel = obedienceLevels[4]; // After Moving a Few Steps
|
|
} else if (complianceDuration > 25 && responseTime < 8) {
|
|
obedienceLevel = obedienceLevels[5]; // After Moving Half Way
|
|
} else if (complianceDuration > 20 && responseTime < 9) {
|
|
obedienceLevel = obedienceLevels[6]; // After Approaching Most of The Way
|
|
} else if (complianceDuration > 15 && responseTime < 10) {
|
|
obedienceLevel = obedienceLevels[7]; // After Touching Collar
|
|
} else if (complianceDuration > 10 && responseTime < 11) {
|
|
obedienceLevel = obedienceLevels[8]; // After Running Away And Returning
|
|
} else if (complianceDuration > 8) {
|
|
obedienceLevel = obedienceLevels[9]; // After Being Lead by Collar or Lead
|
|
} else if (complianceDuration > 5) {
|
|
obedienceLevel = obedienceLevels[10]; // After Check or Bribe
|
|
} else if (complianceDuration > 3) {
|
|
obedienceLevel = obedienceLevels[11]; // After Multiple Checks and/or Bribes
|
|
} else if (complianceDuration > 1) {
|
|
obedienceLevel = obedienceLevels[12]; // None - Refusal
|
|
} else if (responseTime > 12) {
|
|
obedienceLevel = obedienceLevels[13]; // Refused and Ran Away
|
|
} else {
|
|
obedienceLevel = obedienceLevels[14]; // Refusing to Return
|
|
}
|
|
|
|
data.push({
|
|
date: new Date(currentDate),
|
|
complianceDuration: Math.round(complianceDuration * 10) / 10,
|
|
responseTime: Math.round(responseTime * 10) / 10,
|
|
obedienceLevel: obedienceLevel
|
|
});
|
|
|
|
// Advance date by 2-4 days randomly
|
|
const daysToAdd = Math.floor(Math.random() * 3) + 2;
|
|
currentDate.setDate(currentDate.getDate() + daysToAdd);
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
// Generate the training data
|
|
const trainingData = generateTrainingData();
|
|
|
|
// Create legend
|
|
function createLegend() {
|
|
const legendContainer = document.getElementById('obedienceLegend');
|
|
obedienceLevels.forEach(level => {
|
|
const legendItem = document.createElement('div');
|
|
legendItem.className = 'legend-item';
|
|
legendItem.innerHTML = `
|
|
<div class="legend-color" style="background-color: ${level.color}"></div>
|
|
<span>${level.name}</span>
|
|
`;
|
|
legendContainer.appendChild(legendItem);
|
|
});
|
|
}
|
|
|
|
// Wait for Chart.js to load and initialise
|
|
function initializeChart() {
|
|
if (typeof Chart === 'undefined') {
|
|
console.error('Chart.js not loaded');
|
|
document.querySelector('.chart-container').innerHTML = '<p style="text-align: center; color: #dc2626; padding: 50px;">Error loading Chart.js library. Please refresh the page.</p>';
|
|
return;
|
|
}
|
|
|
|
const ctx = document.getElementById('trainingChart').getContext('2d');
|
|
|
|
const chart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
datasets: [
|
|
{
|
|
label: 'Compliance Duration (seconds)',
|
|
data: trainingData.map(d => ({
|
|
x: d.date,
|
|
y: d.complianceDuration
|
|
})),
|
|
pointBackgroundColor: trainingData.map(d => d.obedienceLevel.color),
|
|
pointBorderColor: trainingData.map(d => d.obedienceLevel.color),
|
|
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
|
borderColor: '#3b82f6',
|
|
borderWidth: 2,
|
|
pointRadius: 6,
|
|
pointHoverRadius: 8,
|
|
yAxisID: 'y',
|
|
tension: 0.1
|
|
},
|
|
{
|
|
label: 'Response Time (seconds)',
|
|
data: trainingData.map(d => ({
|
|
x: d.date,
|
|
y: d.responseTime
|
|
})),
|
|
pointBackgroundColor: trainingData.map(d => d.obedienceLevel.color),
|
|
pointBorderColor: trainingData.map(d => d.obedienceLevel.color),
|
|
backgroundColor: 'rgba(239, 68, 68, 0.1)',
|
|
borderColor: '#ef4444',
|
|
borderWidth: 2,
|
|
pointRadius: 6,
|
|
pointHoverRadius: 8,
|
|
yAxisID: 'y1',
|
|
tension: 0.1
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
interaction: {
|
|
mode: 'index',
|
|
intersect: false,
|
|
},
|
|
plugins: {
|
|
title: {
|
|
display: true,
|
|
text: 'Sit Command Training Progress Over Time',
|
|
font: {
|
|
size: 16,
|
|
weight: 'bold'
|
|
},
|
|
padding: 20
|
|
},
|
|
legend: {
|
|
display: true,
|
|
position: 'top',
|
|
labels: {
|
|
usePointStyle: true,
|
|
padding: 20
|
|
}
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
afterBody: function(context) {
|
|
const dataIndex = context[0].dataIndex;
|
|
const obedienceLevel = trainingData[dataIndex].obedienceLevel.name;
|
|
return `Obedience Level: ${obedienceLevel}`;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
type: 'linear',
|
|
ticks: {
|
|
callback: function(value, index) {
|
|
const date = trainingData[index]?.date;
|
|
if (date) {
|
|
return date.toLocaleDateString('en-GB', {
|
|
day: 'numeric',
|
|
month: 'short'
|
|
});
|
|
}
|
|
return value;
|
|
}
|
|
},
|
|
title: {
|
|
display: true,
|
|
text: 'Training Session',
|
|
font: {
|
|
weight: 'bold'
|
|
}
|
|
},
|
|
grid: {
|
|
color: '#e2e8f0'
|
|
}
|
|
},
|
|
y: {
|
|
type: 'linear',
|
|
display: true,
|
|
position: 'left',
|
|
title: {
|
|
display: true,
|
|
text: 'Compliance Duration (seconds)',
|
|
color: '#3b82f6',
|
|
font: {
|
|
weight: 'bold'
|
|
}
|
|
},
|
|
grid: {
|
|
color: '#f1f5f9'
|
|
},
|
|
ticks: {
|
|
color: '#3b82f6'
|
|
}
|
|
},
|
|
y1: {
|
|
type: 'linear',
|
|
display: true,
|
|
position: 'right',
|
|
title: {
|
|
display: true,
|
|
text: 'Response Time (seconds)',
|
|
color: '#ef4444',
|
|
font: {
|
|
weight: 'bold'
|
|
}
|
|
},
|
|
grid: {
|
|
drawOnChartArea: false,
|
|
},
|
|
ticks: {
|
|
color: '#ef4444'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Create the legend
|
|
createLegend();
|
|
</script>
|
|
</body>
|
|
</html> |