https://frenchbroadwaterquality.com/fbwq/wp-content/themes/twenty-…/js/ecoli-graph.js?ver=1755311581
1// js/ecoli-graph.js â float key inside graph; keep mobile UX tight 2(function () { 3 'use strict'; 4 5 // ----- helpers ----- 6 function parseDataText(txt) { 7 try { 8 if (/^\s*</.test(txt)) return null; // looks like an HTML error page 9 // JS assignment style: var data = [...] 10 if (/var\s+data\s*=|^data\s*=/.test(txt)) { 11 var box = {}; 12 // eslint-disable-next-line no-new-func 13 new Function('out', txt + '; out.data = (typeof data !== "undefined" ? data : out.data);')(box); 14 return box.data; 15 } 16 // JSON style: { "data": [...] } or [...] 17 var j = JSON.parse(txt); 18 return j.data || j; 19 } catch (e) { 20 console.error('[ecoli-graph] Parse failed', e); 21 return null; 22 } 23 } 24 25 function binaryClosest(times, xms) { 26 var lo = 0, hi = times.length - 1; 27 if (xms <= times[0]) return 0; 28 if (xms >= times[hi]) return hi; 29 while (hi - lo > 1) { 30 var mid = (lo + hi) >> 1; 31 if (times[mid] < xms) lo = mid; else hi = mid; 32 } 33 return (xms - times[lo] <= times[hi] - xms) ? lo : hi; 34 } 35 36 function isInRangeSelector(target) { 37 var el = target; 38 while (el && el !== document.body) { 39 var cls = (el.className || '').toString(); 40 if (cls.indexOf('dygraph-rangesel') !== -1) return true; 41 el = el.parentNode; 42 } 43 return false; 44 } 45 46 // Move your <figure class="ecoli-key"> into the .ecoli-graph div so we can place it relative to axes 47 function moveKeyIntoGraph(graphEl) { 48 try { 49 var wrap = graphEl.closest('.ecoli-graph-wrap'); 50 if (!wrap) return; 51 var key = wrap.querySelector('.ecoli-key'); 52 if (key && !graphEl.contains(key)) { 53 key.classList.add('inside-graph-key'); 54 key.style.pointerEvents = 'none'; // never block hover/touch 55 graphEl.appendChild(key); 56 } 57 } catch (e) { /* noop */ } 58 } 59 60 // Measure Y-axis label width and expose as CSS var so the key can sit to the right of it 61 function updateYAxisCSSVar(graphEl) { 62 try { 63 var labels = graphEl.querySelectorAll('.dygraph-axis-label-y'); 64 var maxW = 0; 65 labels.forEach(function (n) { maxW = Math.max(maxW, n.offsetWidth || 0); }); 66 if (!maxW) maxW = 46; // sensible default 67 graphEl.style.setProperty('--yaxis-w', maxW + 'px'); 68 } catch (e) { /* noop */ } 69 } 70 71 function initOne(el) { 72 // Read attributes from shortcode container 73 var url = el.getAttribute('data-url'); 74 var days = parseInt(el.getAttribute('data-days') || '5', 10); 75 var ylabel = el.getAttribute('data-ylabel') || 'Estimated E. coli (MPN/100 mL)'; 76 var label = el.getAttribute('data-label') || 'Estimated E. coli'; 77 var primary = parseFloat(el.getAttribute('data-primary') || '126'); 78 var secondary = parseFloat(el.getAttribute('data-secondary') || '886'); 79 80 // Prevent vertical page scrolling inside chart; allow horizontal scrubbing. 81 el.style.touchAction = 'pan-x'; 82 el.style.overscrollBehaviorY = 'contain'; 83 84 // Legend (top-right) 85 var labelsDiv = document.createElement('div'); 86 labelsDiv.className = 'dg-legend'; 87 labelsDiv.style.position = 'absolute'; 88 labelsDiv.style.top = '8px'; 89 labelsDiv.style.right = '10px'; 90 labelsDiv.style.whiteSpace = 'nowrap'; 91 labelsDiv.style.pointerEvents = 'none'; 92 93 function draw(rows) { 94 if (!rows || !rows.length) { el.textContent = 'No data'; return; } 95 96 // Normalize dates and build times array 97 var i, n = rows.length, times = new Array(n); 98 for (i = 0; i < n; i++) { 99 var d = rows[i][0]; 100 if (!(d instanceof Date)) d = new Date(d); 101 rows[i][0] = d; 102 times[i] = d.getTime(); 103 } 104 var last = rows[n - 1]; 105 var usesBars = Array.isArray(last[1]) && last[1].length === 3; 106 107 var end = new Date(times[n - 1]); 108 var start = new Date(end.getTime() - days * 24 * 60 * 60 * 1000); 109 110 // Detect touch/mobile (coarse pointers) 111 var isMobile = (('ontouchstart' in window) || (navigator.maxTouchPoints > 0) 112 || (window.matchMedia && window.matchMedia('(pointer: coarse)').matches)); 113 114 // Disable chart pan/zoom on mobile; keep selection. Desktop unchanged. 115 var IM = isMobile ? { 116 mousemove: Dygraph.defaultInteractionModel.mousemove, 117 mouseout: Dygraph.defaultInteractionModel.mouseout, 118 mousedown: function () { }, click: function () { }, dblclick: function () { }, 119 touchstart: function (e, g) { 120 try { 121 if (e.touches && e.touches.length) { 122 var t = e.touches[0], r = el.getBoundingClientRect();
123 var idx = binaryClosest(times, +g.toDataCoords(t.clientX - r.left, t.clientY - r.top)[0]); 124 g.setSelection(idx); 125 } 126 } catch (_) { } 127 }, 128 touchmove: function (e, g) { 129 try { 130 if (e.touches && e.touches.length) { 131 var t = e.touches[0], r = el.getBoundingClientRect(); 132 var idx = binaryClosest(times, +g.toDataCoords(t.clientX - r.left, t.clientY - r.top)[0]); 133 g.setSelection(idx); 134 } 135 } catch (_) { } 136 }, 137 touchend: function () { } 138 } : Dygraph.defaultInteractionModel; 139 140 var g = new Dygraph(el, rows, { 141 customBars: usesBars, 142 errorBars: false, 143 labels: ['Date', label], 144 ylabel: ylabel, 145 colors: ['#e37c3e'], 146 strokeWidth: 1.0, 147 fillGraph: false, 148 149 axes: { 150 x: { gridLineColor: '#b8b8b8' }, 151 y: { gridLineColor: '#b8b8b8' } 152 }, 153 154 legend: 'always', 155 labelsDiv: labelsDiv, 156 labelsSeparateLines: false, 157 digitsAfterDecimal: 0, 158 159 interactionModel: IM, 160 161 // Range selector stays visible on mobile 162 showRangeSelector: true, 163 rangeSelectorHeight: 24, 164 165 dateWindow: [start.getTime(), end.getTime()], 166 167 underlayCallback: function (c, a, graf) { 168 var ySec = graf.toDomYCoord(secondary); 169 var yPri = graf.toDomYCoord(primary); 170 var x0 = a.x, x1 = a.x + a.w; 171 c.save(); 172 c.beginPath(); c.strokeStyle = '#93acd9'; c.lineWidth = 2; c.moveTo(x0, ySec); c.lineTo(x1, ySec); c.stroke(); 173 c.beginPath(); c.strokeStyle = '#f9be34'; c.lineWidth = 2; c.moveTo(x0, yPri); c.lineTo(x1, yPri); c.stroke(); 174 c.restore(); 175 }, 176 177 highlightCallback: function (e, x, points, row) { 178 var d = this.getValue(row, 0); 179 var y = this.getValue(row, 1); 180 if (Array.isArray(y)) y = y[1]; 181 var ds = new Date(d).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); 182 labelsDiv.innerHTML = '<strong>' + label + ': ' + (y == null ? 'â' : Math.round(y)) + ' MPN</strong> | ' + ds; 183 }, 184 unhighlightCallback: function () { 185 var d = rows[n - 1][0], y = rows[n - 1][1]; 186 if (Array.isArray(y)) y = y[1]; 187 var ds = new Date(d).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); 188 labelsDiv.innerHTML = '<strong>' + label + ': ' + (y == null ? 'â' : Math.round(y)) + ' MPN</strong> | ' + ds; 189 }, 190 191 drawCallback: function () { 192 // Recompute y-axis width for key placement after each draw 193 requestAnimationFrame(function () { updateYAxisCSSVar(el); }); 194 }, 195 196 connectSeparatedPoints: true 197 }); 198
199 // Add legend, move key inside, make sure canvases receive events 200 el.appendChild(labelsDiv); 201 moveKeyIntoGraph(el); 202 try { el.querySelectorAll('canvas').forEach(function (c) { c.style.pointerEvents = 'auto'; c.style.zIndex = '2'; }); } catch (e) { } 203 204 // Desktop fallback: update selection on mousemove even if overlaying elements exist 205 if (!isMobile) { 206 el.addEventListener('mousemove', function (ev) { 207 var r = el.getBoundingClientRect(); 208 var idx = binaryClosest(times, +g.toDataCoords(ev.clientX - r.left, ev.clientY - r.top)[0]); 209 g.setSelection(idx); 210 }); 211 el.addEventListener('mouseleave', function () { 212 g.clearSelection(); 213 var d = rows[n - 1][0], y = rows[n - 1][1]; 214 if (Array.isArray(y)) y = y[1]; 215 var ds = new Date(d).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); 216 labelsDiv.innerHTML = '<strong>' + label + ': ' + (y == null ? 'â' : Math.round(y)) + ' MPN</strong> | ' + ds; 217 }); 218 } 219 220 // Block vertical page scroll inside chart area, but let the range selector work 221 var sx = 0, sy = 0; 222 el.addEventListener('touchstart', function (ev) { 223 if (!ev.touches || !ev.touches.length) return; 224 sx = ev.touches[0].clientX; 225 sy = ev.touches[0].clientY; 226 }, { passive: true }); 227 228 el.addEventListener('touchmove', function (ev) { 229 if (!ev.touches || !ev.touches.length) return; 230 if (isInRangeSelector(ev.target)) return; // allow range selector drags 231 var dx = Math.abs(ev.touches[0].clientX - sx); 232 var dy = Math.abs(ev.touches[0].clientY - sy); 233 if (dy > dx && ev.cancelable) ev.preventDefault(); 234 }, { passive: false }); 235 236 // First-time y-axis measurement (after Dygraph lays out) 237 requestAnimationFrame(function () { updateYAxisCSSVar(el); }); 238 window.addEventListener('resize', function () { updateYAxisCSSVar(el); }); 239 } 240 241 // Load data and render 242 fetch(url, { cache: 'no-store' }) 243 .then(function (r) { return r.ok ? r.text() : Promise.reject('HTTP ' + r.status); }) 244 .then(function (txt) { var rows = parseDataText(txt); if (!rows) throw new Error('Data did not parse'); draw(rows); }) 245 .catch(function (err) { console.error('[ecoli-graph] load error:', err); el.textContent = 'Error loading data'; }); 246 } 247 248 document.addEventListener('DOMContentLoaded', function () { 249 document.querySelectorAll('.ecoli-graph').forEach(initOne); 250 }); 251})(); 252 253 254 255// Keeps the legend key aligned just to the right of the y-axis labels 256document.addEventListener('DOMContentLoaded', function () { 257 document.querySelectorAll('.ecoli-graph').forEach(function (graphEl) { 258 function setYAxisVar() { 259 try { 260 var labels = graphEl.querySelectorAll('.dygraph-axis-label-y'); 261 var w = 0; 262 labels.forEach(function (n) { w = Math.max(w, n.offsetWidth || 0); }); 263 if (!w) w = 56; // fallback if labels not rendered yet 264 graphEl.style.setProperty('--yaxis-w', w + 'px'); 265 var wrap = graphEl.closest('.ecoli-graph-wrap'); 266 if (wrap) wrap.style.setProperty('--yaxis-w', w + 'px'); 267 } catch (e) {} 268 } 269 // run after Dygraph lays out; keep it fresh on resize 270 setTimeout(setYAxisVar, 300); 271 window.addEventListener('resize', setYAxisVar); 272 if (window.ResizeObserver) { 273 var ro = new ResizeObserver(setYAxisVar); 274 ro.observe(graphEl); 275 } 276 }); 277}); 278
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.