PageSourceSearch

https://frenchbroadwaterquality.com/fbwq/wp-content/themes/twenty-…/js/ecoli-graph.js?ver=1755311581

js frenchbroadwaterquality.com collected 2026-10-05 20:02:00 UTC 11,018 bytes, 278 lines download raw bytes

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>&nbsp;&nbsp;|&nbsp;&nbsp;' + 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>&nbsp;&nbsp;|&nbsp;&nbsp;' + 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>&nbsp;&nbsp;|&nbsp;&nbsp;' + 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.