147 lines
4.3 KiB
JavaScript
147 lines
4.3 KiB
JavaScript
document.addEventListener('DOMContentLoaded', async function () {
|
|
const ctx = document.getElementById('phChart');
|
|
if (!ctx) return;
|
|
|
|
const NODERED_URL = (window.NODERED_URL || 'https://node-red.aquamonn.online').replace(/\/$/, '');
|
|
|
|
const chart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: [],
|
|
datasets: [
|
|
{
|
|
label: 'pH',
|
|
data: [],
|
|
borderColor: '#2980b9',
|
|
backgroundColor: 'rgba(41, 128, 185, 0.15)',
|
|
tension: 0.3,
|
|
fill: true,
|
|
pointRadius: 3,
|
|
yAxisID: 'yPh'
|
|
},
|
|
{
|
|
label: 'Suhu (°C)',
|
|
data: [],
|
|
borderColor: '#e67e22',
|
|
backgroundColor: 'rgba(230, 126, 34, 0.10)',
|
|
tension: 0.3,
|
|
fill: false,
|
|
pointRadius: 3,
|
|
yAxisID: 'ySuhu'
|
|
},
|
|
{
|
|
label: 'Amonia (ppm)',
|
|
data: [],
|
|
borderColor: '#27ae60',
|
|
backgroundColor: 'rgba(39, 174, 96, 0.10)',
|
|
tension: 0.3,
|
|
fill: false,
|
|
pointRadius: 3,
|
|
yAxisID: 'yAmonia'
|
|
},
|
|
{
|
|
label: 'Distance (cm)',
|
|
data: [],
|
|
borderColor: '#3b27ae',
|
|
backgroundColor: 'rgba(59, 39, 174, 0.10)',
|
|
tension: 0.3,
|
|
fill: false,
|
|
pointRadius: 3,
|
|
yAxisID: 'ydistance'
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
interaction: { mode: 'index', intersect: false },
|
|
plugins: {
|
|
legend: { display: true, labels: { color: '#000000', font: { size: 12 } } },
|
|
title: {
|
|
display: true,
|
|
text: 'Grafik Sensor Realtime',
|
|
color: '#0b0b0b',
|
|
font: { size: 14, weight: 'bold' }
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
ticks: { color: '#ccc', font: { size: 10 }, maxTicksLimit: 10 },
|
|
grid: { color: 'rgba(255,255,255,0.08)' }
|
|
},
|
|
yPh: {
|
|
type: 'linear',
|
|
position: 'left',
|
|
title: { display: true, text: 'pH', color: '#2980b9' },
|
|
ticks: { color: '#2980b9' },
|
|
grid: { color: 'rgba(255,255,255,0.08)' },
|
|
min: 0, max: 14
|
|
},
|
|
ySuhu: {
|
|
type: 'linear',
|
|
position: 'right',
|
|
title: { display: true, text: '°C', color: '#e67e22' },
|
|
ticks: { color: '#e67e22' },
|
|
grid: { drawOnChartArea: false },
|
|
min: 20, max: 40
|
|
},
|
|
yAmonia: {
|
|
type: 'linear',
|
|
position: 'right',
|
|
title: { display: true, text: 'ppm', color: '#27ae60' },
|
|
ticks: { color: '#27ae60' },
|
|
grid: { drawOnChartArea: false },
|
|
min: 0, max: 10,
|
|
offset: true
|
|
},
|
|
ydistance: {
|
|
type: 'linear',
|
|
position: 'right',
|
|
title: { display: true, text: 'cm', color: '#3b27ae' },
|
|
ticks: { color: '#2927ae' },
|
|
grid: { drawOnChartArea: false },
|
|
min: 0, max: 30,
|
|
offset: true
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
async function loadChartData() {
|
|
try {
|
|
const res = await fetch(NODERED_URL + '/get_history', { cache: 'no-store' });
|
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
const arr = await res.json();
|
|
|
|
if (!Array.isArray(arr) || arr.length === 0) {
|
|
chart.options.plugins.title.text = 'Grafik Sensor — Menunggu data…';
|
|
chart.update();
|
|
return;
|
|
}
|
|
|
|
const slice = arr.slice(0, 20).reverse();
|
|
|
|
chart.data.labels = slice.map(r => {
|
|
if (r.jam) return r.jam.substring(0, 5);
|
|
const d = new Date(r.timestamp ?? 0);
|
|
return d.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' });
|
|
});
|
|
|
|
chart.data.datasets[0].data = slice.map(r => parseFloat(r.ph ?? r.pH ?? 0));
|
|
chart.data.datasets[1].data = slice.map(r => parseFloat(r.suhu ?? r.temperature ?? 0));
|
|
chart.data.datasets[2].data = slice.map(r => parseFloat(r.amonia ?? 0));
|
|
chart.data.datasets[3].data = slice.map(r => parseFloat(r.distance ?? 0));
|
|
|
|
chart.options.plugins.title.text = 'Grafik Sensor Realtime ';
|
|
chart.update();
|
|
|
|
} catch(e) {
|
|
chart.options.plugins.title.text = 'Grafik — Gagal: ' + e.message;
|
|
chart.update();
|
|
console.warn('Chart error:', e.message);
|
|
}
|
|
}
|
|
|
|
await loadChartData();
|
|
setInterval(loadChartData, 30000);
|
|
});
|