Feat: rnd file viewer built
This commit is contained in:
@@ -72,6 +72,10 @@
|
||||
<svg class="w-6 h-6 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
{% elif file.file_type == 'measurement' %}
|
||||
<svg class="w-6 h-6 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
|
||||
</svg>
|
||||
{% else %}
|
||||
<svg class="w-6 h-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
@@ -95,6 +99,7 @@
|
||||
<span class="px-1.5 py-0.5 rounded font-medium
|
||||
{% if file.file_type == 'audio' %}bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300
|
||||
{% elif file.file_type == 'data' %}bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300
|
||||
{% elif file.file_type == 'measurement' %}bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300
|
||||
{% elif file.file_type == 'log' %}bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300
|
||||
{% elif file.file_type == 'archive' %}bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300
|
||||
{% elif file.file_type == 'image' %}bg-pink-100 text-pink-700 dark:bg-pink-900/30 dark:text-pink-300
|
||||
@@ -141,9 +146,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Download Button -->
|
||||
<!-- Action Buttons -->
|
||||
{% if exists %}
|
||||
<div class="opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<div class="opacity-0 group-hover:opacity-100 transition-opacity flex items-center gap-2">
|
||||
{% if file.file_type == 'measurement' or file.file_path.endswith('.rnd') %}
|
||||
<a href="/api/projects/{{ project_id }}/files/{{ file.id }}/view-rnd"
|
||||
onclick="event.stopPropagation();"
|
||||
class="px-3 py-1 text-xs bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 transition-colors flex items-center">
|
||||
<svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
|
||||
</svg>
|
||||
View
|
||||
</a>
|
||||
{% endif %}
|
||||
<button onclick="event.stopPropagation(); downloadFile('{{ file.id }}')"
|
||||
class="px-3 py-1 text-xs bg-seismo-orange text-white rounded-lg hover:bg-seismo-navy transition-colors">
|
||||
<svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
||||
445
templates/rnd_viewer.html
Normal file
445
templates/rnd_viewer.html
Normal file
@@ -0,0 +1,445 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ filename }} - Sound Level Data Viewer{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<!-- Chart.js -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns"></script>
|
||||
<style>
|
||||
.data-table {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.data-table table {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.data-table th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
.metric-card {
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.metric-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<!-- Header with breadcrumb -->
|
||||
<div class="mb-6">
|
||||
<nav class="flex items-center space-x-2 text-sm text-gray-500 dark:text-gray-400 mb-2">
|
||||
<a href="/projects" class="hover:text-seismo-orange">Projects</a>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
<a href="/projects/{{ project_id }}" class="hover:text-seismo-orange">{{ project.name if project else 'Project' }}</a>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
<span class="text-gray-900 dark:text-white">{{ filename }}</span>
|
||||
</nav>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-3">
|
||||
<svg class="w-8 h-8 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
|
||||
</svg>
|
||||
{{ filename }}
|
||||
</h1>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
Sound Level Meter Measurement Data
|
||||
{% if unit %} - {{ unit.id }}{% endif %}
|
||||
{% if location %} @ {{ location.name }}{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="/api/projects/{{ project_id }}/files/{{ file_id }}/download"
|
||||
class="px-4 py-2 bg-seismo-orange text-white rounded-lg hover:bg-seismo-navy transition-colors flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
|
||||
</svg>
|
||||
Download File
|
||||
</a>
|
||||
<a href="/projects/{{ project_id }}"
|
||||
class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">
|
||||
Back to Project
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div id="loading-state" class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-12 text-center">
|
||||
<svg class="w-12 h-12 mx-auto mb-4 text-seismo-orange animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
<p class="text-gray-500 dark:text-gray-400">Loading measurement data...</p>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div id="error-state" class="hidden bg-white dark:bg-slate-800 rounded-xl shadow-lg p-12 text-center">
|
||||
<svg class="w-12 h-12 mx-auto mb-4 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
|
||||
</svg>
|
||||
<p class="text-red-500 font-semibold mb-2">Error Loading Data</p>
|
||||
<p id="error-message" class="text-gray-500 dark:text-gray-400"></p>
|
||||
</div>
|
||||
|
||||
<!-- Data Content (hidden until loaded) -->
|
||||
<div id="data-content" class="hidden space-y-6">
|
||||
<!-- Summary Cards -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
||||
<div class="metric-card bg-white dark:bg-slate-800 rounded-xl shadow p-4">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">File Type</div>
|
||||
<div id="summary-file-type" class="text-lg font-bold text-gray-900 dark:text-white mt-1">-</div>
|
||||
</div>
|
||||
<div class="metric-card bg-white dark:bg-slate-800 rounded-xl shadow p-4">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">Total Rows</div>
|
||||
<div id="summary-rows" class="text-lg font-bold text-gray-900 dark:text-white mt-1">-</div>
|
||||
</div>
|
||||
<div class="metric-card bg-white dark:bg-slate-800 rounded-xl shadow p-4">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">Start Time</div>
|
||||
<div id="summary-start" class="text-sm font-bold text-gray-900 dark:text-white mt-1">-</div>
|
||||
</div>
|
||||
<div class="metric-card bg-white dark:bg-slate-800 rounded-xl shadow p-4">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">End Time</div>
|
||||
<div id="summary-end" class="text-sm font-bold text-gray-900 dark:text-white mt-1">-</div>
|
||||
</div>
|
||||
<div class="metric-card bg-white dark:bg-slate-800 rounded-xl shadow p-4 bg-blue-50 dark:bg-blue-900/20">
|
||||
<div class="text-xs text-blue-600 dark:text-blue-400 uppercase tracking-wide">Avg Level</div>
|
||||
<div id="summary-avg" class="text-lg font-bold text-blue-700 dark:text-blue-300 mt-1">- dB</div>
|
||||
</div>
|
||||
<div class="metric-card bg-white dark:bg-slate-800 rounded-xl shadow p-4 bg-red-50 dark:bg-red-900/20">
|
||||
<div class="text-xs text-red-600 dark:text-red-400 uppercase tracking-wide">Max Level</div>
|
||||
<div id="summary-max" class="text-lg font-bold text-red-700 dark:text-red-300 mt-1">- dB</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chart -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Sound Level Over Time</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-sm text-gray-500 dark:text-gray-400">Show:</label>
|
||||
<select id="chart-metric-select" class="text-sm border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-1 bg-white dark:bg-gray-700 text-gray-900 dark:text-white">
|
||||
<option value="all">All Metrics</option>
|
||||
<option value="leq">Leq Only</option>
|
||||
<option value="lmax">Lmax Only</option>
|
||||
<option value="lmin">Lmin Only</option>
|
||||
<option value="lpeak">Lpeak Only</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative" style="height: 400px;">
|
||||
<canvas id="soundLevelChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Table -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Measurement Data</h2>
|
||||
<div class="flex items-center gap-4">
|
||||
<input type="text" id="table-search" placeholder="Search..."
|
||||
class="text-sm border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-1 bg-white dark:bg-gray-700 text-gray-900 dark:text-white w-48">
|
||||
<span id="row-count" class="text-sm text-gray-500 dark:text-gray-400"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-table">
|
||||
<table class="w-full">
|
||||
<thead id="table-header" class="bg-gray-50 dark:bg-gray-900 text-gray-600 dark:text-gray-400 text-xs uppercase">
|
||||
<!-- Headers will be inserted here -->
|
||||
</thead>
|
||||
<tbody id="table-body" class="divide-y divide-gray-100 dark:divide-gray-700">
|
||||
<!-- Data rows will be inserted here -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let chartInstance = null;
|
||||
let allData = [];
|
||||
let allHeaders = [];
|
||||
|
||||
// Load data on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadRndData();
|
||||
});
|
||||
|
||||
async function loadRndData() {
|
||||
try {
|
||||
const response = await fetch('/api/projects/{{ project_id }}/files/{{ file_id }}/rnd-data');
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.detail || 'Failed to load data');
|
||||
}
|
||||
|
||||
// Store data globally
|
||||
allData = result.data;
|
||||
allHeaders = result.headers;
|
||||
|
||||
// Update summary cards
|
||||
updateSummary(result.summary);
|
||||
|
||||
// Render chart
|
||||
renderChart(result.data, result.summary.file_type);
|
||||
|
||||
// Render table
|
||||
renderTable(result.headers, result.data);
|
||||
|
||||
// Show content, hide loading
|
||||
document.getElementById('loading-state').classList.add('hidden');
|
||||
document.getElementById('data-content').classList.remove('hidden');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading RND data:', error);
|
||||
document.getElementById('loading-state').classList.add('hidden');
|
||||
document.getElementById('error-state').classList.remove('hidden');
|
||||
document.getElementById('error-message').textContent = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
function updateSummary(summary) {
|
||||
document.getElementById('summary-file-type').textContent =
|
||||
summary.file_type === 'leq' ? 'Leq (Time-Averaged)' :
|
||||
summary.file_type === 'lp' ? 'Lp (Instantaneous)' : 'Unknown';
|
||||
|
||||
document.getElementById('summary-rows').textContent = summary.total_rows.toLocaleString();
|
||||
document.getElementById('summary-start').textContent = summary.time_start || '-';
|
||||
document.getElementById('summary-end').textContent = summary.time_end || '-';
|
||||
|
||||
// Find the main metric based on file type
|
||||
const avgKey = summary.file_type === 'leq' ? 'Leq(Main)_avg' : 'Lp(Main)_avg';
|
||||
const maxKey = summary.file_type === 'leq' ? 'Lmax(Main)_max' : 'Lmax(Main)_max';
|
||||
|
||||
if (summary[avgKey] !== undefined) {
|
||||
document.getElementById('summary-avg').textContent = summary[avgKey].toFixed(1) + ' dB';
|
||||
}
|
||||
if (summary[maxKey] !== undefined) {
|
||||
document.getElementById('summary-max').textContent = summary[maxKey].toFixed(1) + ' dB';
|
||||
} else if (summary['Lpeak(Main)_max'] !== undefined) {
|
||||
document.getElementById('summary-max').textContent = summary['Lpeak(Main)_max'].toFixed(1) + ' dB';
|
||||
}
|
||||
}
|
||||
|
||||
function renderChart(data, fileType) {
|
||||
const ctx = document.getElementById('soundLevelChart').getContext('2d');
|
||||
|
||||
// Prepare datasets based on file type
|
||||
const datasets = [];
|
||||
const labels = data.map(row => row['Start Time'] || '');
|
||||
|
||||
// Define metrics to chart based on file type
|
||||
const metricsConfig = fileType === 'leq' ? [
|
||||
{ key: 'Leq(Main)', label: 'Leq', color: 'rgb(59, 130, 246)', fill: false },
|
||||
{ key: 'Lmax(Main)', label: 'Lmax', color: 'rgb(239, 68, 68)', fill: false },
|
||||
{ key: 'Lmin(Main)', label: 'Lmin', color: 'rgb(34, 197, 94)', fill: false },
|
||||
{ key: 'Lpeak(Main)', label: 'Lpeak', color: 'rgb(168, 85, 247)', fill: false },
|
||||
] : [
|
||||
{ key: 'Lp(Main)', label: 'Lp', color: 'rgb(59, 130, 246)', fill: false },
|
||||
{ key: 'Leq(Main)', label: 'Leq', color: 'rgb(249, 115, 22)', fill: false },
|
||||
{ key: 'Lmax(Main)', label: 'Lmax', color: 'rgb(239, 68, 68)', fill: false },
|
||||
{ key: 'Lmin(Main)', label: 'Lmin', color: 'rgb(34, 197, 94)', fill: false },
|
||||
{ key: 'Lpeak(Main)', label: 'Lpeak', color: 'rgb(168, 85, 247)', fill: false },
|
||||
];
|
||||
|
||||
metricsConfig.forEach(metric => {
|
||||
const values = data.map(row => {
|
||||
const val = row[metric.key];
|
||||
return typeof val === 'number' ? val : null;
|
||||
});
|
||||
|
||||
// Only add if we have data
|
||||
if (values.some(v => v !== null)) {
|
||||
datasets.push({
|
||||
label: metric.label,
|
||||
data: values,
|
||||
borderColor: metric.color,
|
||||
backgroundColor: metric.color.replace('rgb', 'rgba').replace(')', ', 0.1)'),
|
||||
fill: metric.fill,
|
||||
tension: 0.1,
|
||||
pointRadius: data.length > 100 ? 0 : 2,
|
||||
borderWidth: 1.5,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Downsample if too many points
|
||||
let chartLabels = labels;
|
||||
let chartDatasets = datasets;
|
||||
|
||||
if (data.length > 1000) {
|
||||
const sampleRate = Math.ceil(data.length / 1000);
|
||||
chartLabels = labels.filter((_, i) => i % sampleRate === 0);
|
||||
chartDatasets = datasets.map(ds => ({
|
||||
...ds,
|
||||
data: ds.data.filter((_, i) => i % sampleRate === 0)
|
||||
}));
|
||||
}
|
||||
|
||||
chartInstance = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: chartLabels,
|
||||
datasets: chartDatasets
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top',
|
||||
labels: {
|
||||
color: document.documentElement.classList.contains('dark') ? '#9ca3af' : '#374151',
|
||||
usePointStyle: true,
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: document.documentElement.classList.contains('dark') ? '#1f2937' : '#ffffff',
|
||||
titleColor: document.documentElement.classList.contains('dark') ? '#f9fafb' : '#111827',
|
||||
bodyColor: document.documentElement.classList.contains('dark') ? '#d1d5db' : '#4b5563',
|
||||
borderColor: document.documentElement.classList.contains('dark') ? '#374151' : '#e5e7eb',
|
||||
borderWidth: 1,
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
return context.dataset.label + ': ' + (context.parsed.y !== null ? context.parsed.y.toFixed(1) + ' dB' : 'N/A');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
display: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Time',
|
||||
color: document.documentElement.classList.contains('dark') ? '#9ca3af' : '#374151',
|
||||
},
|
||||
ticks: {
|
||||
color: document.documentElement.classList.contains('dark') ? '#9ca3af' : '#374151',
|
||||
maxTicksLimit: 10,
|
||||
maxRotation: 45,
|
||||
},
|
||||
grid: {
|
||||
color: document.documentElement.classList.contains('dark') ? '#374151' : '#e5e7eb',
|
||||
}
|
||||
},
|
||||
y: {
|
||||
display: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Sound Level (dB)',
|
||||
color: document.documentElement.classList.contains('dark') ? '#9ca3af' : '#374151',
|
||||
},
|
||||
ticks: {
|
||||
color: document.documentElement.classList.contains('dark') ? '#9ca3af' : '#374151',
|
||||
},
|
||||
grid: {
|
||||
color: document.documentElement.classList.contains('dark') ? '#374151' : '#e5e7eb',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Chart metric filter
|
||||
document.getElementById('chart-metric-select').addEventListener('change', function(e) {
|
||||
const value = e.target.value;
|
||||
chartInstance.data.datasets.forEach((ds, i) => {
|
||||
if (value === 'all') {
|
||||
ds.hidden = false;
|
||||
} else if (value === 'leq') {
|
||||
ds.hidden = !['Leq', 'Lp'].includes(ds.label);
|
||||
} else if (value === 'lmax') {
|
||||
ds.hidden = ds.label !== 'Lmax';
|
||||
} else if (value === 'lmin') {
|
||||
ds.hidden = ds.label !== 'Lmin';
|
||||
} else if (value === 'lpeak') {
|
||||
ds.hidden = ds.label !== 'Lpeak';
|
||||
}
|
||||
});
|
||||
chartInstance.update();
|
||||
});
|
||||
}
|
||||
|
||||
function renderTable(headers, data) {
|
||||
const headerRow = document.getElementById('table-header');
|
||||
const tbody = document.getElementById('table-body');
|
||||
|
||||
// Render headers
|
||||
headerRow.innerHTML = '<tr>' + headers.map(h =>
|
||||
`<th class="px-4 py-3 text-left font-medium">${escapeHtml(h)}</th>`
|
||||
).join('') + '</tr>';
|
||||
|
||||
// Render rows (limit to first 500 for performance)
|
||||
const displayData = data.slice(0, 500);
|
||||
tbody.innerHTML = displayData.map(row =>
|
||||
'<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/50">' +
|
||||
headers.map(h => {
|
||||
const val = row[h];
|
||||
let displayVal = val;
|
||||
if (val === null || val === undefined) {
|
||||
displayVal = '-';
|
||||
} else if (typeof val === 'number') {
|
||||
displayVal = val.toFixed(1);
|
||||
}
|
||||
return `<td class="px-4 py-2 text-gray-700 dark:text-gray-300">${escapeHtml(String(displayVal))}</td>`;
|
||||
}).join('') +
|
||||
'</tr>'
|
||||
).join('');
|
||||
|
||||
// Update row count
|
||||
document.getElementById('row-count').textContent =
|
||||
data.length > 500 ? `Showing 500 of ${data.length.toLocaleString()} rows` : `${data.length.toLocaleString()} rows`;
|
||||
|
||||
// Search functionality
|
||||
document.getElementById('table-search').addEventListener('input', function(e) {
|
||||
const searchTerm = e.target.value.toLowerCase();
|
||||
const filtered = data.filter(row =>
|
||||
Object.values(row).some(v =>
|
||||
String(v).toLowerCase().includes(searchTerm)
|
||||
)
|
||||
);
|
||||
|
||||
const displayFiltered = filtered.slice(0, 500);
|
||||
tbody.innerHTML = displayFiltered.map(row =>
|
||||
'<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/50">' +
|
||||
headers.map(h => {
|
||||
const val = row[h];
|
||||
let displayVal = val;
|
||||
if (val === null || val === undefined) {
|
||||
displayVal = '-';
|
||||
} else if (typeof val === 'number') {
|
||||
displayVal = val.toFixed(1);
|
||||
}
|
||||
return `<td class="px-4 py-2 text-gray-700 dark:text-gray-300">${escapeHtml(String(displayVal))}</td>`;
|
||||
}).join('') +
|
||||
'</tr>'
|
||||
).join('');
|
||||
|
||||
document.getElementById('row-count').textContent =
|
||||
filtered.length > 500 ? `Showing 500 of ${filtered.length.toLocaleString()} filtered rows` : `${filtered.length.toLocaleString()} rows`;
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user