caliceo / index.html
julien-c's picture
julien-c HF Staff
Upload index.html with huggingface_hub
200e2f6 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Caliceo Affluence</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f5f7fa; color: #1a1a2e; }
header { background: linear-gradient(135deg, #0077b6, #00b4d8); color: #fff; padding: 1.5rem 2rem; }
header h1 { font-size: 1.5rem; font-weight: 600; }
header p { font-size: 0.9rem; opacity: 0.85; margin-top: 0.25rem; }
.container { max-width: 1200px; margin: 2rem auto; padding: 0 1.5rem; }
.card { background: #fff; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 1.5rem; }
.stats { display: flex; gap: 1rem; margin-bottom: 1.5rem; flex-wrap: wrap; }
.stat { background: #f0f4f8; border-radius: 8px; padding: 0.75rem 1.25rem; flex: 1; min-width: 140px; }
.stat .label { font-size: 0.75rem; text-transform: uppercase; color: #666; letter-spacing: 0.05em; }
.stat .value { font-size: 1.4rem; font-weight: 700; color: #0077b6; margin-top: 0.15rem; }
.chart-wrapper { position: relative; height: 400px; }
.controls { display: flex; gap: 0.5rem; margin-bottom: 1rem; flex-wrap: wrap; }
.controls button { padding: 0.4rem 1rem; border: 1px solid #ddd; border-radius: 6px; background: #fff; cursor: pointer; font-size: 0.85rem; transition: all 0.15s; }
.controls button:hover { border-color: #0077b6; color: #0077b6; }
.controls button.active { background: #0077b6; color: #fff; border-color: #0077b6; }
.controls button.predict { background: #9b59b6; color: #fff; border-color: #9b59b6; }
.controls button.predict:hover { background: #8e44ad; border-color: #8e44ad; color: #fff; }
.controls button.predict:disabled { opacity: 0.6; cursor: wait; }
</style>
<script>if (window.self !== window.top) document.documentElement.classList.add('embedded');</script>
<style>.embedded header { display: none; } .embedded .container { margin-top: 1rem; }</style>
</head>
<body>
<header>
<h1>Caliceo Affluence</h1>
<p>Time-series data from julien-c/caliceo dataset</p>
</header>
<div class="container">
<div class="stats" id="stats"></div>
<div class="card">
<div class="controls" id="controls"></div>
<div class="chart-wrapper">
<canvas id="chart"></canvas>
</div>
</div>
<p style="margin-top: 0.75rem; font-size: 0.75rem; color: #999;">*PatchTST using transformers.js</p>
</div>
<script type="module">
import { PatchTSTForPrediction, Tensor } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3';
let chart;
let allData = [];
let predictions = null;
async function loadCSV() {
const res = await fetch('https://huggingface.co/datasets/julien-c/caliceo/resolve/main/affluence.csv');
const text = await res.text();
const lines = text.trim().split('\n').slice(1);
allData = lines.map(line => {
const [timestamp, affluence] = line.split(',');
return { x: new Date(timestamp), y: parseInt(affluence, 10) };
});
renderStats(allData);
buildControls();
renderChart(allData);
}
function renderStats(data) {
const values = data.map(d => d.y);
const max = Math.max(...values);
const min = Math.min(...values);
const avg = (values.reduce((a, b) => a + b, 0) / values.length).toFixed(1);
const first = data[0].x.toLocaleDateString();
const last = data[data.length - 1].x.toLocaleDateString();
document.getElementById('stats').innerHTML = `
<div class="stat"><div class="label">Data points</div><div class="value">${data.length.toLocaleString()}</div></div>
<div class="stat"><div class="label">Peak</div><div class="value">${max} %</div></div>
<div class="stat"><div class="label">Low</div><div class="value">${min} %</div></div>
<div class="stat"><div class="label">Average</div><div class="value">${avg} %</div></div>
<div class="stat"><div class="label">Period</div><div class="value" style="font-size:0.95rem">${first} &ndash; ${last}</div></div>
<div class="stat"><div class="label">Last data point</div><div class="value" style="font-size:0.95rem">${data[data.length - 1].x.toLocaleString()}</div></div>
`;
}
function buildControls() {
const ranges = [
{ label: 'All', days: null },
{ label: '1d', days: 1 },
{ label: '7d', days: 7 },
{ label: '14d', days: 14 },
{ label: '30d', days: 30 },
];
const container = document.getElementById('controls');
ranges.forEach((r, i) => {
const btn = document.createElement('button');
btn.textContent = r.label;
if (i === 0) btn.classList.add('active');
btn.addEventListener('click', () => {
container.querySelectorAll('.range').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const filtered = filterData(r.days);
renderStats(filtered);
renderChart(filtered);
});
btn.classList.add('range');
container.appendChild(btn);
});
const predictBtn = document.createElement('button');
predictBtn.textContent = 'Predict next hours*';
predictBtn.classList.add('predict');
predictBtn.addEventListener('click', () => runPrediction(predictBtn));
container.appendChild(predictBtn);
}
async function runPrediction(btn) {
if (predictions) return;
btn.disabled = true;
btn.textContent = 'Loading model (~2.5 MB)...';
const model = await PatchTSTForPrediction.from_pretrained(
'onnx-community/granite-timeseries-patchtst',
{ dtype: 'fp32' },
);
btn.textContent = 'Predicting...';
// Use last 512 values as context (model's context_length)
const values = allData.map(d => d.y);
const context = values.slice(-512);
// Normalize: standardize to zero mean, unit variance (model expects scaled input)
const mean = context.reduce((a, b) => a + b, 0) / context.length;
const std = Math.sqrt(context.reduce((a, b) => a + (b - mean) ** 2, 0) / context.length) || 1;
const normalized = context.map(v => (v - mean) / std);
// Shape: [batch=1, seq_len=512, num_channels=7] — model expects 7 channels
// Replicate univariate data across all 7 channels
const channelData = new Float32Array(512 * 7);
for (let i = 0; i < 512; i++) {
for (let c = 0; c < 7; c++) {
channelData[i * 7 + c] = normalized[i];
}
}
const past_values = new Tensor('float32', channelData, [1, 512, 7]);
const { prediction_outputs } = await model({ past_values });
// prediction_outputs shape: [1, 96, 7] — take channel 0
const predData = prediction_outputs.data;
const numChannels = 7;
const lastTime = allData[allData.length - 1].x.getTime();
const interval = 10 * 60 * 1000; // 10 minutes
// Take all 96 predicted steps (~16 hours), denormalize
const predictionLength = 96;
predictions = [];
for (let i = 0; i < predictionLength; i++) {
const raw = predData[i * numChannels] * std + mean;
predictions.push({
x: new Date(lastTime + (i + 1) * interval),
y: Math.round(Math.max(0, raw)),
});
}
btn.textContent = 'Predicted!';
// Auto-switch to 1d view
const oneDayBtn = document.querySelector('.controls .range:nth-child(2)');
document.querySelectorAll('.controls .range').forEach(b => b.classList.remove('active'));
oneDayBtn.classList.add('active');
const filtered = filterData(1);
renderStats(filtered);
renderChart(filtered);
}
function filterData(days) {
if (!days) return allData;
const cutoff = new Date(allData[allData.length - 1].x);
cutoff.setDate(cutoff.getDate() - days);
return allData.filter(d => d.x >= cutoff);
}
const weekendBandsPlugin = {
id: 'weekendBands',
beforeDraw(chart) {
const { ctx, chartArea: { left, right, top, bottom }, scales: { x } } = chart;
if (!x) return;
const min = x.min;
const max = x.max;
// Find the first Saturday at midnight on or before the data start
const start = new Date(min);
start.setHours(0, 0, 0, 0);
while (start.getDay() !== 6) start.setDate(start.getDate() - 1);
ctx.save();
ctx.fillStyle = 'rgba(255, 140, 66, 0.1)';
const d = new Date(start);
while (d.getTime() <= max) {
const satStart = d.getTime();
const sunEnd = satStart + 2 * 24 * 60 * 60 * 1000;
const x0 = Math.max(x.getPixelForValue(satStart), left);
const x1 = Math.min(x.getPixelForValue(sunEnd), right);
if (x1 > left && x0 < right) {
ctx.fillRect(x0, top, x1 - x0, bottom - top);
}
d.setDate(d.getDate() + 7);
}
ctx.restore();
}
};
function isWeekend(date) {
const day = date.getDay();
return day === 0 || day === 6;
}
function renderChart(data) {
if (chart) chart.destroy();
const ctx = document.getElementById('chart').getContext('2d');
const gradient = ctx.createLinearGradient(0, 0, 0, 400);
gradient.addColorStop(0, 'rgba(0, 119, 182, 0.3)');
gradient.addColorStop(1, 'rgba(0, 119, 182, 0.01)');
const weekendGradient = ctx.createLinearGradient(0, 0, 0, 400);
weekendGradient.addColorStop(0, 'rgba(255, 140, 66, 0.3)');
weekendGradient.addColorStop(1, 'rgba(255, 140, 66, 0.01)');
const datasets = [{
label: 'Affluence',
data: data,
segment: {
borderColor: ctx2 => isWeekend(data[ctx2.p0DataIndex].x) && isWeekend(data[ctx2.p1DataIndex].x) ? '#e07020' : '#0077b6',
backgroundColor: ctx2 => isWeekend(data[ctx2.p0DataIndex].x) && isWeekend(data[ctx2.p1DataIndex].x) ? weekendGradient : gradient,
},
borderColor: '#0077b6',
backgroundColor: gradient,
borderWidth: 1.5,
fill: true,
pointRadius: 0,
pointHitRadius: 8,
tension: 0.3,
}];
if (predictions) {
// Bridge from last real point to first prediction
const bridge = [data[data.length - 1], ...predictions];
datasets.push({
label: 'Prediction (median)',
data: bridge,
borderColor: '#9b59b6',
borderWidth: 2.5,
borderDash: [6, 4],
pointRadius: (ctx2) => ctx2.dataIndex === 0 ? 0 : 5,
pointBackgroundColor: '#9b59b6',
fill: false,
tension: 0.3,
});
}
chart = new Chart(ctx, {
type: 'line',
data: { datasets },
plugins: [weekendBandsPlugin],
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
scales: {
x: {
type: 'time',
time: { tooltipFormat: 'PPP p' },
grid: { display: false },
ticks: { maxTicksLimit: 10 },
},
y: {
beginAtZero: true,
title: { display: true, text: 'Affluence' },
grid: { color: '#eee' },
}
},
plugins: {
legend: { display: !!predictions, labels: { usePointStyle: true } },
tooltip: {
backgroundColor: 'rgba(0,0,0,0.8)',
padding: 10,
cornerRadius: 8,
callbacks: {
afterTitle: (items) => {
if (items.length && isWeekend(data[items[0].dataIndex].x)) return '(Weekend)';
return '';
},
label: (item) => ` Affluence: ${item.formattedValue} %`
}
}
}
}
});
}
loadCSV();
</script>
</body>
</html>