{"id":37,"date":"2026-06-04T00:48:57","date_gmt":"2026-06-04T00:48:57","guid":{"rendered":"https:\/\/keithtech.net\/?page_id=37"},"modified":"2026-06-04T00:48:57","modified_gmt":"2026-06-04T00:48:57","slug":"js","status":"publish","type":"page","link":"https:\/\/keithtech.net\/?page_id=37","title":{"rendered":"js"},"content":{"rendered":"\n<!DOCTYPE html>\n<html>\n<head>\n  <title>ESP32 Charts with Data Logger<\/title>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=yes\">\n  <script src=\"https:\/\/cdn.jsdelivr.net\/npm\/chart.js@4.4.1\/dist\/chart.umd.min.js\"><\/script>\n  <script src=\"https:\/\/cdn.jsdelivr.net\/npm\/hammerjs@2.0.8\/hammer.min.js\"><\/script>\n  <script src=\"https:\/\/cdn.jsdelivr.net\/npm\/chartjs-plugin-zoom@2.0.1\/dist\/chartjs-plugin-zoom.min.js\"><\/script>\n  <style>\n    body {font-family: Arial, sans-serif; background:#f4f4f4; margin:0; padding:15px;}\n    .container {max-width:1300px; margin:auto; background:white; padding:20px; border-radius:10px; box-shadow:0 4px 15px rgba(0,0,0,0.1);}\n    h1 {text-align:center;}\n    .controls { text-align:center; margin:20px 0; display:flex; flex-wrap:wrap; justify-content:center; gap:12px; }\n    .btn { padding:12px 22px; font-size:16px; font-weight:600; border:none; border-radius:8px; cursor:pointer; transition:all 0.2s ease; box-shadow:0 2px 6px rgba(0,0,0,0.1); }\n    .btn:hover { transform:translateY(-2px); box-shadow:0 4px 12px rgba(0,0,0,0.15); }\n    .btn-blue { background:#2196F3; color:white; }\n    .btn-green { background:#4CAF50; color:white; }\n    .btn-red { background:#f44336; color:white; }\n    .btn-gray { background:#6c777d; color:white; }\n    .chart-container {position:relative; height:380px; margin-bottom:40px; border:1px solid #ddd; border-radius:8px; padding:15px; touch-action: none;}\n    .chart-header {display:flex; justify-content:space-between; align-items:center; margin-bottom:12px; flex-wrap:wrap; gap:10px;}\n    .toggle-group {display:inline-flex; background:#f1f1f1; border-radius:50px; padding:4px; box-shadow:inset 0 2px 4px rgba(0,0,0,0.1);}\n    .toggle-btn {padding:8px 24px; font-size:15px; font-weight:600; border:none; border-radius:50px; cursor:pointer; transition:all 0.3s;}\n    .toggle-btn.active {background:#393b41; color:white; box-shadow:0 2px 6px rgba(33,150,243,0.3);}\n    .latest-reading { background:#f8f8f8; padding:15px; font-size:16px; border-radius:5px; }\n  <\/style>\n<\/head>\n<body>\n  <div class=\"container\">\n    <h1>ESP32 Charts with Data Logger<\/h1>\n    <p style=\"text-align:center;\">New readings are added to your chart automatically, you don&#8217;t need to refresh the web page.<\/p>\n    <div class=\"controls\">\n      <button class=\"btn btn-blue\" onclick=\"loadCSVData()\">Refresh Charts<\/button>\n      <button class=\"btn btn-green\" onclick=\"downloadCSV()\">Download CSV<\/button>\n      <button class=\"btn btn-red\" onclick=\"deleteData()\">Delete All Data<\/button>\n      <button class=\"btn btn-gray\" onclick=\"resetZoomAll()\">Reset All Zoom<\/button>\n    <\/div>\n    <div class=\"chart-header\">\n      <h2>Temperature<\/h2>\n      <div class=\"toggle-group\">\n        <button id=\"btnC\" class=\"toggle-btn active\" onclick=\"setUnit('C')\">&deg;C<\/button>\n        <button id=\"btnF\" class=\"toggle-btn\" onclick=\"setUnit('F')\">&deg;F<\/button>\n      <\/div>\n    <\/div>\n    <div class=\"chart-container\"><canvas id=\"tempChart\"><\/canvas><\/div>\n    <h2>Humidity<\/h2>\n    <div class=\"chart-container\"><canvas id=\"humChart\"><\/canvas><\/div>\n    <h2>Pressure<\/h2>\n    <div class=\"chart-container\"><canvas id=\"pressChart\"><\/canvas><\/div>\n    <h3>Latest Reading<\/h3>\n    <pre id=\"latest\" class=\"latest-reading\"><\/pre>\n  <\/div>\n  <script>\n    let tempChart, humChart, pressChart;\n    let allData = [];\n    let currentUnit = 'C';\n    function createCharts() {\n      const zoomOptions = {\n        zoom: { wheel: { enabled: true }, pinch: { enabled: true }, mode: 'x', drag: { enabled: true } },\n        pan: { enabled: true, mode: 'x', threshold: 5 }\n      };\n\n      const makeChart = (canvasId, color, yLabel) => {\n        const chart = new Chart(document.getElementById(canvasId), {\n          type: 'line',\n          data: { labels: [], datasets: [{ label: yLabel, borderColor: color, tension: 0.2, data: [] }] },\n          options: {\n            responsive: true,\n            maintainAspectRatio: false,\n            interaction: { mode: 'index', intersect: false },\n            scales: {\n              x: { title: { display: true, text: 'Timestamp' } },\n              y: { title: { display: true, text: yLabel } }\n            },\n            plugins: { zoom: zoomOptions }\n          }\n        });\n\n        \/\/ Double-click to reset zoom\n        document.getElementById(canvasId).addEventListener('dblclick', () => {\n          chart.resetZoom();\n        });\n\n        return chart;\n      };\n\n      tempChart = makeChart('tempChart', '#2ecc71', 'Temperature');\n      humChart  = makeChart('humChart',  '#3498db', 'Humidity (%)');\n      pressChart= makeChart('pressChart', '#8c479d', 'Pressure (hPa)');\n    }\n    async function loadCSVData() {\n      try {\n        const response = await fetch('\/download');\n        const csvText = await response.text();\n        allData = parseCSV(csvText);\n\n        if (allData.length === 0) {\n          document.getElementById('latest').textContent = \"No data logged yet.\";\n          return;\n        }\n\n        const last = allData[allData.length-1];\n        document.getElementById('latest').innerHTML = \n          `Temperature: ${last.temp_c}\u00b0C \/ ${last.temp_f}\u00b0F<br>` +\n          `Humidity: ${last.hum}%<br>` +\n          `Pressure: ${last.press} hPa<br>` +\n          `<strong>${last.day} ${last.time}<\/strong>`;\n\n        const displayData = allData.slice(-1200);\n        const labels = displayData.map(d => d.day + \" \" + d.time);\n\n        const tempValues = displayData.map(d => currentUnit === 'C' ? parseFloat(d.temp_c) : parseFloat(d.temp_f));\n\n        tempChart.data.labels = labels;\n        tempChart.data.datasets[0].label = `Temperature (\u00b0${currentUnit})`;\n        tempChart.data.datasets[0].data = tempValues;\n        tempChart.update();\n\n        humChart.data.labels = labels;\n        humChart.data.datasets[0].data = displayData.map(d => parseFloat(d.hum));\n        humChart.update();\n\n        pressChart.data.labels = labels;\n        pressChart.data.datasets[0].data = displayData.map(d => parseFloat(d.press));\n        pressChart.update();\n\n      } catch(e) { console.error(\"Error:\", e); }\n    }\n    function parseCSV(csv) {\n      const lines = csv.trim().split('\\n');\n      const result = [];\n      for (let i = 1; i < lines.length; i++) {\n        if (!lines[i].trim()) continue;\n        const [temp_c, temp_f, hum, press, time, day] = lines[i].split(',');\n        result.push({temp_c, temp_f, hum, press, time, day});\n      }\n      return result;\n    }\n    function setUnit(unit) {\n      currentUnit = unit;\n      document.getElementById('btnC').classList.toggle('active', unit === 'C');\n      document.getElementById('btnF').classList.toggle('active', unit === 'F');\n      if (allData.length > 0) loadCSVData();\n    }\n    function downloadCSV() { \n      window.location.href = '\/download';\n    }\n    function resetZoomAll() {\n      tempChart.resetZoom();\n      humChart.resetZoom();\n      pressChart.resetZoom();\n    }\n    async function deleteData() {\n      if (!confirm(\"Delete all logged data?\")) return;\n      await fetch('\/delete', {method: 'POST'});\n      alert(\"Data deleted\");\n      loadCSVData();\n    }\n    window.onload = () => {\n      createCharts();\n      setUnit('C');\n      loadCSVData();\n      setInterval(loadCSVData, 30000);\n    };\n  <\/script>\n<\/body>\n<\/html>\n","protected":false},"excerpt":{"rendered":"<p>ESP32 Charts with Data Logger ESP32 Charts with Data Logger New readings are added to your chart automatically, you don&#8217;t need to refresh the web page. Refresh Charts Download CSV Delete All Data Reset All Zoom Temperature &deg;C &deg;F Humidity Pressure Latest Reading<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-37","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/keithtech.net\/index.php?rest_route=\/wp\/v2\/pages\/37","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/keithtech.net\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/keithtech.net\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/keithtech.net\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/keithtech.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=37"}],"version-history":[{"count":1,"href":"https:\/\/keithtech.net\/index.php?rest_route=\/wp\/v2\/pages\/37\/revisions"}],"predecessor-version":[{"id":38,"href":"https:\/\/keithtech.net\/index.php?rest_route=\/wp\/v2\/pages\/37\/revisions\/38"}],"wp:attachment":[{"href":"https:\/\/keithtech.net\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=37"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}