youplot 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
youplot/render/js.py ADDED
@@ -0,0 +1,1080 @@
1
+ """Static JS for youplot — all chart interactivity."""
2
+
3
+ # CDN fallback (used only if vendored assets unavailable)
4
+ UPLOT_CDN = "https://cdn.jsdelivr.net/npm/uplot@1.6.31/dist/uPlot.iife.min.js"
5
+ UPLOT_CSS = "https://cdn.jsdelivr.net/npm/uplot@1.6.31/dist/uPlot.min.css"
6
+
7
+ JS = r"""
8
+ function hexAlpha(hex, alpha) {
9
+ if (!hex) return 'rgba(0,0,0,'+alpha+')';
10
+ if (hex.startsWith('rgba')||hex.startsWith('rgb')) return hex;
11
+ var h=hex.replace('#','');
12
+ if(h.length===3)h=h[0]+h[0]+h[1]+h[1]+h[2]+h[2];
13
+ var r=parseInt(h.slice(0,2),16),g=parseInt(h.slice(2,4),16),b=parseInt(h.slice(4,6),16);
14
+ return 'rgba('+r+','+g+','+b+','+alpha+')';
15
+ }
16
+
17
+ // ── Fill plugin ───────────────────────────────────────────────────────────────
18
+ function makeFillPlugin(seriesConfigs) {
19
+ return {hooks:{draw:[function(u){
20
+ seriesConfigs.forEach(function(cfg,si){
21
+ if(!cfg._fill)return;
22
+ var s=u.series[si+1];
23
+ if(!s||s.show===false)return;
24
+ var ctx=u.ctx,sk=s.scale,data=u.data[si+1],b=u.bbox;
25
+ ctx.save();
26
+ ctx.beginPath();ctx.rect(b.left,b.top,b.width,b.height);ctx.clip();
27
+ var grad=ctx.createLinearGradient(0,b.top,0,b.top+b.height);
28
+ var fc=cfg._fillColor||cfg.stroke;
29
+ grad.addColorStop(0,hexAlpha(fc,cfg._fillOpacity||0.15));
30
+ grad.addColorStop(1,hexAlpha(fc,0));
31
+ ctx.fillStyle=grad;ctx.beginPath();
32
+ var first=true,lastX=null;
33
+ for(var i=0;i<data.length;i++){
34
+ var yv=data[i];
35
+ if(yv==null||isNaN(yv)){first=true;continue;}
36
+ var px=u.valToPos(u.data[0][i],'x',true);
37
+ var py=u.valToPos(yv,sk,true);
38
+ if(first){ctx.moveTo(px,b.top+b.height);ctx.lineTo(px,py);first=false;}
39
+ else ctx.lineTo(px,py);
40
+ lastX=px;
41
+ }
42
+ if(lastX!==null)ctx.lineTo(lastX,b.top+b.height);
43
+ ctx.closePath();ctx.fill();ctx.restore();
44
+ });
45
+ }]}};
46
+ }
47
+
48
+ // ── Threshold bands plugin ────────────────────────────────────────────────────
49
+ function makeThresholdPlugin(bands) {
50
+ if (!bands || bands.length === 0) return {hooks:{}};
51
+ return {hooks:{draw:[function(u){
52
+ var ctx=u.ctx, b=u.bbox;
53
+ ctx.save();
54
+ ctx.beginPath();ctx.rect(b.left,b.top,b.width,b.height);ctx.clip();
55
+ bands.forEach(function(band){
56
+ var sk = (band.scale && u.scales[band.scale]) ? band.scale : 'y';
57
+ var y0 = u.valToPos(band.y_lo, sk, true);
58
+ var y1 = u.valToPos(band.y_hi, sk, true);
59
+ var top = Math.min(y0, y1);
60
+ var height = Math.abs(y1 - y0);
61
+ ctx.fillStyle = hexAlpha(band.color, band.opacity || 0.12);
62
+ ctx.fillRect(b.left, top, b.width, height);
63
+ // draw border lines
64
+ [y0, y1].forEach(function(y){
65
+ ctx.strokeStyle = hexAlpha(band.color, 0.45);
66
+ ctx.lineWidth = 1;
67
+ ctx.setLineDash([4, 4]);
68
+ ctx.beginPath();
69
+ ctx.moveTo(b.left, y); ctx.lineTo(b.left + b.width, y);
70
+ ctx.stroke();
71
+ ctx.setLineDash([]);
72
+ });
73
+ if (band.label) {
74
+ ctx.fillStyle = hexAlpha(band.color, 0.9);
75
+ ctx.font = '600 10px sans-serif';
76
+ ctx.fillText(band.label, b.left + 6, Math.min(y0,y1) + 12);
77
+ }
78
+ });
79
+ ctx.restore();
80
+ }]}};
81
+ }
82
+
83
+ // ── Annotation (regions/vlines/hlines) plugin ─────────────────────────────────
84
+ function makeAnnotationPlugin(vlines,hlines,regions,isTimeseries){
85
+ return {hooks:{draw:[function(u){
86
+ var ctx=u.ctx,b=u.bbox;
87
+ ctx.save();ctx.beginPath();ctx.rect(b.left,b.top,b.width,b.height);ctx.clip();
88
+ regions.forEach(function(r){
89
+ var x0=u.valToPos(isTimeseries?r.x_start/1000:r.x_start,'x',true);
90
+ var x1=u.valToPos(isTimeseries?r.x_end/1000:r.x_end,'x',true);
91
+ var left = Math.min(x0,x1), width = Math.abs(x1-x0);
92
+ ctx.fillStyle=hexAlpha(r.color,r.opacity);
93
+ ctx.fillRect(left,b.top,width,b.height);
94
+ if (r._tagId && r.label) {
95
+ var hh = 18;
96
+ ctx.fillStyle=hexAlpha(r.color, 1.0);
97
+ ctx.fillRect(left, b.top, width, hh);
98
+ ctx.save();
99
+ ctx.fillStyle="#000";
100
+ ctx.font='600 10px sans-serif';
101
+ ctx.textAlign='center';ctx.textBaseline='middle';
102
+ ctx.beginPath();ctx.rect(left,b.top,width,hh);ctx.clip();
103
+ ctx.fillText(r.label, left+width/2, b.top+hh/2);
104
+ ctx.restore();
105
+ }
106
+ });
107
+ // Measurements
108
+ var measuresToDraw = UP_MEASUREMENTS.slice();
109
+ if(measureAnchorIdx !== null && window._up_currentMeasureIdx !== null) {
110
+ measuresToDraw.push({startX:u.data[0][measureAnchorIdx],endX:u.data[0][window._up_currentMeasureIdx]});
111
+ }
112
+ measuresToDraw.forEach(function(m){
113
+ var x0=u.valToPos(m.startX,'x',true), x1=u.valToPos(m.endX,'x',true);
114
+ var left=Math.min(x0,x1), width=Math.abs(x1-x0);
115
+ ctx.fillStyle="rgba(0,0,0,0.05)"; ctx.fillRect(left,b.top,width,b.height);
116
+ var fh=18;
117
+ ctx.fillStyle="rgba(0,0,0,0.85)"; ctx.fillRect(left,b.top+b.height-fh,width,fh);
118
+ ctx.strokeStyle="rgba(0,0,0,0.6)"; ctx.lineWidth=1; ctx.setLineDash([4,4]);
119
+ ctx.beginPath();
120
+ ctx.moveTo(x0,b.top);ctx.lineTo(x0,b.top+b.height);
121
+ ctx.moveTo(x1,b.top);ctx.lineTo(x1,b.top+b.height);
122
+ ctx.stroke();ctx.setLineDash([]);
123
+ });
124
+ vlines.forEach(function(v){
125
+ var x=u.valToPos(isTimeseries?v.x/1000:v.x,'x',true);
126
+ ctx.strokeStyle=v.color;ctx.lineWidth=v.width;
127
+ ctx.setLineDash(v.dash?[5,4]:[]);
128
+ ctx.beginPath();ctx.moveTo(x,b.top);ctx.lineTo(x,b.top+b.height);ctx.stroke();
129
+ ctx.setLineDash([]);
130
+ if(v.label){ctx.save();ctx.fillStyle=v.color;ctx.font='600 10px sans-serif';ctx.fillText(v.label,x+4,b.top+14);ctx.restore();}
131
+ });
132
+ hlines.forEach(function(h){
133
+ var sk=(h.scale&&u.scales[h.scale])?h.scale:'y';
134
+ var y=u.valToPos(h.y,sk,true);
135
+ ctx.strokeStyle=h.color;ctx.lineWidth=h.width;
136
+ ctx.setLineDash(h.dash?[5,4]:[]);
137
+ ctx.beginPath();ctx.moveTo(b.left,y);ctx.lineTo(b.left+b.width,y);ctx.stroke();
138
+ ctx.setLineDash([]);
139
+ if(h.label){ctx.save();ctx.fillStyle=h.color;ctx.font='600 10px sans-serif';ctx.fillText(h.label,b.left+6,y-5);ctx.restore();}
140
+ });
141
+ ctx.restore();
142
+ }]}};
143
+ }
144
+
145
+ // ── Helpers ───────────────────────────────────────────────────────────────────
146
+ function round2(v){return Math.round(v*100)/100;}
147
+ function formatVal(v,fmt){
148
+ if(!fmt)return round2(v);
149
+ var m=fmt.match(/\.(\d+)f/);
150
+ return m?v.toFixed(parseInt(m[1])):round2(v);
151
+ }
152
+ function formatTagDateTime(unixSec, isTimeseries) {
153
+ if (!isTimeseries) return round2(unixSec);
154
+ var d=new Date(unixSec*1000), pad=function(n){return String(n).padStart(2,'0');};
155
+ var months=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
156
+ return d.getDate()+' '+months[d.getMonth()]+' '+d.getFullYear()+', '+pad(d.getHours())+':'+pad(d.getMinutes())+':'+pad(d.getSeconds());
157
+ }
158
+
159
+ // ── Crosshair sync ────────────────────────────────────────────────────────────
160
+ // Global registry: [{u, showTooltipFn, hideTooltipFn}]
161
+ if (!window._UP_SYNC_REGISTRY) window._UP_SYNC_REGISTRY = [];
162
+
163
+ function registerForSync(u, showFn, hideFn) {
164
+ window._UP_SYNC_REGISTRY.push({u: u, show: showFn, hide: hideFn});
165
+ }
166
+
167
+ function syncCrosshair(sourceU, idx, sourceClientX, sourceClientY) {
168
+ var xVal = sourceU.data[0][idx];
169
+ window._UP_SYNC_REGISTRY.forEach(function(entry) {
170
+ if (entry.u === sourceU) return;
171
+ try {
172
+ var u = entry.u;
173
+ var left = u.valToPos(xVal, 'x');
174
+ var midY = (u.over.offsetHeight || 100) / 2;
175
+ u.setCursor({left: left, top: midY});
176
+ // Show tooltip on synced chart using its own renderer
177
+ if (entry.show) entry.show(idx, xVal, sourceClientX, sourceClientY);
178
+ } catch(e){}
179
+ });
180
+ }
181
+
182
+ function clearSyncCrosshair(sourceU) {
183
+ window._UP_SYNC_REGISTRY.forEach(function(entry) {
184
+ if (entry.u === sourceU) return;
185
+ try {
186
+ entry.u.setCursor({left: -10, top: -10});
187
+ if (entry.hide) entry.hide();
188
+ } catch(e){}
189
+ });
190
+ }
191
+
192
+ // ── State ─────────────────────────────────────────────────────────────────────
193
+ var uplotInst=null;
194
+ var seriesVisible=[];
195
+ var legendEls=[];
196
+ var lastClickTime=0,lastClickIdx=-1,DBLCLICK_MS=300;
197
+ var tooltipEl=null;
198
+ var initialScales=null;
199
+
200
+ // Tool state
201
+ var currentTool = 'zoom';
202
+ var activeTagRegion = null;
203
+ var tagColors = ['#FF00FF','#00FFFF','#00FF00','#FFFF00','#FF00AA','#00FF88','#FF8800','#8800FF'];
204
+ var tagColorIdx = 0;
205
+
206
+ // Measure state (line only)
207
+ var measureAnchorIdx = null;
208
+ var UP_MEASUREMENTS = [];
209
+ window._up_currentMeasureIdx = null;
210
+
211
+ // Annotation pins state
212
+ var UP_PINS = []; // {id, x, y_px_frac, label, color}
213
+ var pinDragId = null;
214
+
215
+ var scatterVisible = {};
216
+
217
+ // ── Legend ────────────────────────────────────────────────────────────────────
218
+ function scatterLegendClick(el, scIdx){
219
+ var now=Date.now(), isDbl=(now-lastClickTime<DBLCLICK_MS)&&(lastClickIdx==='sc'+scIdx);
220
+ lastClickTime=now;lastClickIdx='sc'+scIdx;
221
+ if(isDbl){
222
+ var allHidden=true;
223
+ UP_SCATTER_CFG.forEach(function(s){if(s._scatterIdx!==scIdx&&s.visible!==false)allHidden=false;});
224
+ UP_SCATTER_CFG.forEach(function(s){s.visible=allHidden?true:(s._scatterIdx===scIdx);});
225
+ var lc=document.getElementById(UP_CONTAINER_ID).closest('.up-card');
226
+ if(lc)lc.querySelectorAll('.up-leg-item[data-kind="scatter"]').forEach(function(e){
227
+ var idx=parseInt(e.dataset.si.replace('sc-',''));
228
+ var cfg=UP_SCATTER_CFG.find(function(s){return s._scatterIdx===idx;});
229
+ e.classList.toggle('up-leg-off',cfg&&cfg.visible===false);
230
+ });
231
+ } else {
232
+ var scfg=UP_SCATTER_CFG.find(function(s){return s._scatterIdx===scIdx;});
233
+ if(scfg)scfg.visible=(scfg.visible===false)?true:false;
234
+ el.classList.toggle('up-leg-off',scfg&&scfg.visible===false);
235
+ }
236
+ if(uplotInst)uplotInst.redraw();
237
+ }
238
+
239
+ function legendClick(el,si){
240
+ var now=Date.now(), isDbl=(now-lastClickTime<DBLCLICK_MS)&&(lastClickIdx===si);
241
+ lastClickTime=now;lastClickIdx=si;
242
+ if(isDbl){
243
+ var anyOther=false;
244
+ for(var i=1;i<seriesVisible.length;i++){if(i!==si&&seriesVisible[i]){anyOther=true;break;}}
245
+ for(var i=1;i<seriesVisible.length;i++){
246
+ var show=anyOther?(i===si):true;
247
+ seriesVisible[i]=show;
248
+ uplotInst.setSeries(i,{show:show});
249
+ legendEls[i]&&legendEls[i].classList.toggle('up-leg-off',!show);
250
+ }
251
+ } else {
252
+ seriesVisible[si]=!seriesVisible[si];
253
+ uplotInst.setSeries(si,{show:seriesVisible[si]});
254
+ el.classList.toggle('up-leg-off',!seriesVisible[si]);
255
+ }
256
+ }
257
+
258
+ // ── Tooltip ───────────────────────────────────────────────────────────────────
259
+ function buildTooltipHtml(u, idx) {
260
+ var tsVal = u.data[0][idx];
261
+ var timeStr = UP_IS_TIMESERIES
262
+ ? (new Date(tsVal*1000)).toLocaleString('en-IN',{timeZone:'Asia/Kolkata',hour12:false,day:'2-digit',month:'short',hour:'2-digit',minute:'2-digit',second:'2-digit'})+' IST'
263
+ : String(round2(tsVal));
264
+ var html = '<div class="up-tooltip-time">'+timeStr+'</div>';
265
+ var xVal = UP_IS_TIMESERIES ? tsVal*1000 : tsVal;
266
+ UP_REGIONS.forEach(function(r){
267
+ if(r._tagId&&r.label&&xVal>=Math.min(r.x_start,r.x_end)&&xVal<=Math.max(r.x_start,r.x_end)){
268
+ html += '<div class="up-tooltip-row" style="background:'+hexAlpha(r.color,0.15)+';border-radius:4px;padding:3px 6px;margin-bottom:6px;border:1px solid '+hexAlpha(r.color,0.3)+';">'
269
+ +'<span class="up-tooltip-dot" style="background:'+r.color+'"></span>'
270
+ +'<span class="up-tooltip-name" style="font-weight:600;color:'+r.color+';">Tag: '+r.label+'</span></div>';
271
+ }
272
+ });
273
+ var hasVal = false;
274
+ UP_SERIES_CFG.slice(1).forEach(function(cfg,i){
275
+ var si=i+1;
276
+ if(!seriesVisible[si])return;
277
+ var val=u.data[si]?u.data[si][idx]:null;
278
+ if(val==null||isNaN(val))return;
279
+ hasVal=true;
280
+ var display=cfg._hoverFormat?formatVal(val,cfg._hoverFormat):round2(val);
281
+ html+='<div class="up-tooltip-row"><span class="up-tooltip-dot" style="background:'+cfg.stroke+'"></span>'
282
+ +'<span class="up-tooltip-name">'+(cfg.label||'')+'</span>'
283
+ +'<span class="up-tooltip-val" style="color:'+cfg.stroke+'">'+display+(cfg._hoverUnit||'')+'</span></div>';
284
+ });
285
+ return hasVal ? html : null;
286
+ }
287
+
288
+ function showTooltipAt(html, clientX, clientY) {
289
+ if(!tooltipEl)return;
290
+ tooltipEl.innerHTML = html;
291
+ tooltipEl.style.display = 'block';
292
+ var tw = tooltipEl.offsetWidth||200;
293
+ var left = clientX+18;
294
+ if(left+tw>window.innerWidth-12) left=clientX-tw-18;
295
+ var top = clientY-10;
296
+ if(top+130>window.innerHeight) top=clientY-130;
297
+ tooltipEl.style.left=left+'px'; tooltipEl.style.top=top+'px';
298
+ }
299
+
300
+ function attachLineTooltip(u){
301
+ // Capture THIS chart's tooltip element in a local variable — never reassign it.
302
+ // (tooltipEl is the IIFE-scoped var used elsewhere; myTip is this chart's own)
303
+ var myTip = document.getElementById(UP_TOOLTIP_ID);
304
+ tooltipEl = myTip; // keep the shared var pointing here too for non-sync use
305
+ if(!myTip) return;
306
+
307
+ function showForIdx(idx, xValOverride, clientX, clientY) {
308
+ // Render THIS chart's series data into THIS chart's tooltip element
309
+ var html = buildTooltipHtml(u, idx);
310
+ if(!html){ myTip.style.display='none'; return; }
311
+ // Position near the mouse that triggered the sync
312
+ myTip.innerHTML = html;
313
+ myTip.style.display = 'block';
314
+ var tw = myTip.offsetWidth || 200;
315
+ // Place tooltip just below the other chart's cursor, near top of this chart
316
+ var rect = u.over.getBoundingClientRect();
317
+ var left = rect.left + u.valToPos(u.data[0][idx], 'x') + 18;
318
+ if(left + tw > window.innerWidth - 12) left = left - tw - 36;
319
+ var top = rect.top + 8;
320
+ myTip.style.left = left + 'px';
321
+ myTip.style.top = top + 'px';
322
+ }
323
+
324
+ registerForSync(u, showForIdx, function(){ myTip.style.display='none'; });
325
+
326
+ u.over.addEventListener('mousemove',function(e){
327
+ if(UP_HAS_SCATTER)return;
328
+ var rect=u.over.getBoundingClientRect();
329
+ var cx=e.clientX-rect.left;
330
+ var idx=u.posToIdx(cx);
331
+ if(idx==null||idx<0||idx>=u.data[0].length){ myTip.style.display='none'; return; }
332
+ syncCrosshair(u, idx, e.clientX, e.clientY);
333
+ var html = buildTooltipHtml(u, idx);
334
+ if(!html){ myTip.style.display='none'; return; }
335
+ myTip.innerHTML = html;
336
+ myTip.style.display = 'block';
337
+ var tw = myTip.offsetWidth||200;
338
+ var left = e.clientX+18;
339
+ if(left+tw>window.innerWidth-12) left=e.clientX-tw-18;
340
+ var top = e.clientY-10;
341
+ if(top+130>window.innerHeight) top=e.clientY-130;
342
+ myTip.style.left=left+'px'; myTip.style.top=top+'px';
343
+ });
344
+ u.over.addEventListener('mouseleave',function(){
345
+ myTip.style.display='none';
346
+ clearSyncCrosshair(u);
347
+ });
348
+ }
349
+
350
+ function attachMixedTooltip(u){
351
+ tooltipEl=document.getElementById(UP_TOOLTIP_ID);
352
+ if(!tooltipEl)return;
353
+ registerForSync(u, null, function(){ if(tooltipEl)tooltipEl.style.display='none'; });
354
+ u.over.addEventListener('mousemove',function(e){
355
+ var rect=u.over.getBoundingClientRect();
356
+ var idx=u.posToIdx(e.clientX-rect.left);
357
+ if(idx!=null&&idx>=0) syncCrosshair(u, idx, e.clientX, e.clientY);
358
+ });
359
+ u.over.addEventListener('mouseleave',function(){
360
+ if(tooltipEl)tooltipEl.style.display='none';
361
+ clearSyncCrosshair(u);
362
+ });
363
+ }
364
+ // ── Tag bubble list ───────────────────────────────────────────────────────────
365
+ function renderTagBubbles(tagsList) {
366
+ if (!tagsList) return;
367
+ // Remove only tag bubbles — preserve annotation (up-ann-item) rows
368
+ var existing = tagsList.querySelector('.up-tag-bubbles');
369
+ if (existing) existing.remove();
370
+ var existingLabel = tagsList.querySelector('.up-list-section-label.for-tags');
371
+ if (existingLabel) existingLabel.remove();
372
+
373
+ var bubbleWrap=document.createElement('div');
374
+ bubbleWrap.className='up-tag-bubbles';
375
+ UP_REGIONS.forEach(function(r){
376
+ if(!r._tagId) return;
377
+ var bubble=document.createElement('span');
378
+ bubble.className='up-tag-bubble';
379
+ bubble.style.background=hexAlpha(r.color,0.18);
380
+ bubble.style.borderColor=hexAlpha(r.color,0.5);
381
+ bubble.style.color=r.color;
382
+ var s0=UP_IS_TIMESERIES?r.x_start/1000:r.x_start;
383
+ var s1=UP_IS_TIMESERIES?r.x_end/1000:r.x_end;
384
+ bubble.title=formatTagDateTime(s0,UP_IS_TIMESERIES)+' → '+formatTagDateTime(s1,UP_IS_TIMESERIES);
385
+ var nameSpan=document.createElement('span');
386
+ nameSpan.className='up-tag-bubble-name';nameSpan.textContent=r.label;
387
+ bubble.appendChild(nameSpan);
388
+ bubble.addEventListener('click',function(ev){
389
+ if(ev.target.classList.contains('up-tag-bubble-del'))return;
390
+ var pad=(s1-s0)*0.15;
391
+ uplotInst.setScale('x',{min:s0-pad,max:s1+pad});
392
+ });
393
+ if(r._removable!==false){
394
+ var delBtn=document.createElement('button');
395
+ delBtn.className='up-tag-bubble-del';delBtn.title='Remove';delBtn.innerHTML='&times;';
396
+ delBtn.addEventListener('click',function(ev){
397
+ ev.stopPropagation();
398
+ var idx=UP_REGIONS.indexOf(r);
399
+ if(idx!==-1)UP_REGIONS.splice(idx,1);
400
+ if(uplotInst)uplotInst.redraw();
401
+ renderTagBubbles(tagsList);
402
+ });
403
+ bubble.appendChild(delBtn);
404
+ }
405
+ bubbleWrap.appendChild(bubble);
406
+ });
407
+ if(bubbleWrap.children.length>0){
408
+ // Insert tags section before any annotation items
409
+ var firstAnn = tagsList.querySelector('.up-ann-section');
410
+ if (firstAnn) {
411
+ tagsList.insertBefore(bubbleWrap, firstAnn);
412
+ } else {
413
+ tagsList.insertBefore(bubbleWrap, tagsList.firstChild);
414
+ }
415
+ }
416
+ }
417
+
418
+ // ── Annotation pins (Figma-style) ─────────────────────────────────────────────
419
+ function positionPinEl(u, pinEl, pin) {
420
+ var b = u.bbox;
421
+ var dpr = window.devicePixelRatio || 1;
422
+ var xSec = UP_IS_TIMESERIES ? pin.x / 1000 : pin.x;
423
+ // Hide pin when x is outside the visible scale range
424
+ var sc = u.scales.x;
425
+ if (sc && (xSec < sc.min || xSec > sc.max)) {
426
+ pinEl.style.display = 'none';
427
+ return;
428
+ }
429
+ pinEl.style.display = '';
430
+ var xPos = u.valToPos(xSec, 'x', true);
431
+ xPos = Math.max(b.left, Math.min(b.left + b.width, xPos));
432
+
433
+ var yPos;
434
+ if (pin.y != null) {
435
+ var sk = 'y';
436
+ if (pin.scale === 'left') sk = 'y';
437
+ else if (pin.scale === 'right') sk = 'y2';
438
+ else if (pin.scale && u.scales[pin.scale]) sk = pin.scale;
439
+ yPos = u.valToPos(pin.y, sk, true);
440
+ } else {
441
+ var frac = (pin.y_frac != null) ? pin.y_frac : 0.2;
442
+ yPos = b.top + frac * b.height;
443
+ }
444
+ yPos = Math.max(b.top, Math.min(b.top + b.height, yPos));
445
+
446
+ // pins layer is offset to (b.left/dpr, b.top/dpr) — subtract origin, convert to CSS px
447
+ pinEl.style.left = ((xPos - b.left) / dpr - 10) + 'px';
448
+ pinEl.style.top = ((yPos - b.top) / dpr - 10) + 'px';
449
+ }
450
+
451
+ var pinColors = [
452
+ '#f59e0b', // amber
453
+ '#10b981', // emerald
454
+ '#6366f1', // indigo
455
+ '#f43f5e', // rose
456
+ '#06b6d4', // cyan
457
+ '#8b5cf6', // violet
458
+ '#f97316', // orange
459
+ '#84cc16', // lime
460
+ '#ec4899', // pink
461
+ '#14b8a6', // teal
462
+ ];
463
+ if (typeof window._UP_PIN_COLOR_IDX === 'undefined') window._UP_PIN_COLOR_IDX = 0;
464
+ var pinColorIdx = 0; // local alias, reads/writes window._UP_PIN_COLOR_IDX
465
+
466
+ function buildPinEl(u, pin, pinsLayer, tagsList) {
467
+ // Assign a color if not set
468
+ if (!pin.color) {
469
+ pin.color = pinColors[window._UP_PIN_COLOR_IDX % pinColors.length];
470
+ window._UP_PIN_COLOR_IDX++;
471
+ }
472
+
473
+ // ── Marker on canvas ──────────────────────────────────────────────────────
474
+ var el = document.createElement('div');
475
+ el.className = 'up-pin';
476
+ el.id = 'pin-' + pin.id;
477
+
478
+ var icon = document.createElement('div');
479
+ icon.className = 'up-pin-icon';
480
+ icon.style.background = pin.color;
481
+ icon.textContent = pin.label ? pin.label[0].toUpperCase() : '✎';
482
+ el.appendChild(icon);
483
+
484
+ var popup = document.createElement('div');
485
+ popup.className = 'up-pin-popup';
486
+ popup.innerHTML = '<div class="up-pin-popup-label">'+escapeHtml(pin.label)+'</div>'
487
+ +'<div class="up-pin-popup-time">'+formatTagDateTime(UP_IS_TIMESERIES?pin.x/1000:pin.x, UP_IS_TIMESERIES)+'</div>';
488
+ el.appendChild(popup);
489
+
490
+ var delMarker = document.createElement('button');
491
+ delMarker.className = 'up-pin-del';
492
+ delMarker.innerHTML = '&times;';
493
+ delMarker.title = 'Delete annotation';
494
+ el.appendChild(delMarker);
495
+
496
+ positionPinEl(u, el, pin);
497
+ pinsLayer.appendChild(el);
498
+
499
+ // ── Entry in the notes list below ────────────────────────────────────────
500
+ var listItem = null;
501
+ if (tagsList) {
502
+ // Ensure annotations section container exists
503
+ var annSection = tagsList.querySelector('.up-ann-section');
504
+ if (!annSection) {
505
+ annSection = document.createElement('div');
506
+ annSection.className = 'up-ann-section';
507
+ var sectionLabel = document.createElement('div');
508
+ sectionLabel.className = 'up-list-section-label';
509
+ sectionLabel.textContent = 'Annotations';
510
+ annSection.appendChild(sectionLabel);
511
+ // Wrap for flex-row sticky notes layout
512
+ var notesWrap = document.createElement('div');
513
+ notesWrap.className = 'up-ann-notes-wrap';
514
+ annSection.appendChild(notesWrap);
515
+ tagsList.appendChild(annSection);
516
+ }
517
+ var notesWrap = annSection.querySelector('.up-ann-notes-wrap');
518
+
519
+ listItem = document.createElement('div');
520
+ listItem.className = 'up-ann-item';
521
+ listItem.id = 'ann-item-' + pin.id;
522
+
523
+ // Sticky note background + folded corner tinted with pin color
524
+ var cornerColor = hexAlpha(pin.color, 0.45);
525
+ listItem.style.backgroundImage = 'linear-gradient(135deg, '+cornerColor+' 0px, '+cornerColor+' 12px, transparent 12px)';
526
+ listItem.style.backgroundColor = hexAlpha(pin.color, 0.28);
527
+
528
+ // Slight alternating tilt for the "scattered notes" feel
529
+ var noteCount = notesWrap.children.length;
530
+ var tilt = (noteCount % 2 === 0) ? 'rotate(-1.5deg)' : 'rotate(1deg)';
531
+ listItem.style.transform = tilt;
532
+
533
+ // Text block
534
+ var body = document.createElement('div');
535
+ body.className = 'up-ann-item-body';
536
+
537
+ var labelEl = document.createElement('div');
538
+ labelEl.className = 'up-ann-item-label';
539
+ labelEl.textContent = pin.label;
540
+
541
+ var timeEl = document.createElement('div');
542
+ timeEl.className = 'up-ann-item-time';
543
+ timeEl.textContent = formatTagDateTime(UP_IS_TIMESERIES?pin.x/1000:pin.x, UP_IS_TIMESERIES);
544
+
545
+ body.appendChild(labelEl);
546
+ body.appendChild(timeEl);
547
+
548
+ var delList = document.createElement('button');
549
+ delList.className = 'up-tag-del';
550
+ delList.innerHTML = '&times;';
551
+ delList.title = 'Delete annotation';
552
+
553
+ // Invisible dot (kept for delete logic compat)
554
+ var dot = document.createElement('span');
555
+ dot.className = 'up-ann-item-dot';
556
+
557
+ listItem.appendChild(dot);
558
+ listItem.appendChild(body);
559
+ listItem.appendChild(delList);
560
+
561
+ // Navigate to pin on click
562
+ listItem.addEventListener('click', function(ev) {
563
+ if(ev.target === delList) return;
564
+ var pad = (u.scales.x.max - u.scales.x.min) * 0.05;
565
+ var xSec = UP_IS_TIMESERIES ? pin.x/1000 : pin.x;
566
+ u.setScale('x', {min: xSec - pad, max: xSec + pad});
567
+ });
568
+
569
+ notesWrap.appendChild(listItem);
570
+ }
571
+
572
+ // Shared delete logic
573
+ function doDelete(e) {
574
+ e.stopPropagation();
575
+ UP_PINS = UP_PINS.filter(function(p){return p.id !== pin.id;});
576
+ el.remove();
577
+ if(listItem) {
578
+ listItem.remove();
579
+ // Remove the annotations section if no more notes remain
580
+ if(tagsList) {
581
+ var sec = tagsList.querySelector('.up-ann-section');
582
+ var wrap = sec ? sec.querySelector('.up-ann-notes-wrap') : null;
583
+ if(wrap && wrap.children.length === 0) sec.remove();
584
+ }
585
+ }
586
+ }
587
+ delMarker.addEventListener('click', doDelete);
588
+ if(listItem) listItem.querySelector('.up-tag-del').addEventListener('click', doDelete);
589
+
590
+ return el;
591
+ }
592
+
593
+ function escapeHtml(s) {
594
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
595
+ }
596
+
597
+ // ── HTML Export ───────────────────────────────────────────────────────────────
598
+ function exportAsHtml() {
599
+ // Clone the entire document so we never touch the live DOM.
600
+ var docClone = document.documentElement.cloneNode(true);
601
+
602
+ // Clear uPlot-rendered DOM from chart containers in the CLONE only.
603
+ // (Leaving them populated causes double-chart when JS re-runs on open.)
604
+ docClone.querySelectorAll('[id^="up-chart-"]').forEach(function(el) {
605
+ el.innerHTML = '';
606
+ });
607
+ // Clear pins layer in clone (pins are re-created by JS on load)
608
+ docClone.querySelectorAll('.up-pins-layer').forEach(function(el) {
609
+ el.innerHTML = '';
610
+ });
611
+ // Clear tags list in clone (bubbles + sticky notes are re-created by JS on load)
612
+ docClone.querySelectorAll('.up-tags-list').forEach(function(el) {
613
+ el.innerHTML = '';
614
+ });
615
+ // Hide tooltip in clone (it may have stale position/content)
616
+ docClone.querySelectorAll('.up-tooltip').forEach(function(el) {
617
+ el.style.display = 'none';
618
+ });
619
+
620
+ var html = '<!DOCTYPE html>\n' + docClone.outerHTML;
621
+
622
+ // Persist runtime state: UP_REGIONS (tags) and interactive-only pins.
623
+ // Code pins (UP_CODE_PINS) are already in the JS source — don't duplicate them.
624
+ var codePinIds = {};
625
+ if (typeof UP_CODE_PINS !== 'undefined') {
626
+ UP_CODE_PINS.forEach(function(p) { codePinIds[p.id] = true; });
627
+ }
628
+ var interactivePins = UP_PINS.filter(function(p) { return !codePinIds[p.id]; });
629
+
630
+ var regionsPatch = '<script id="up-state-patch">'
631
+ + 'window._UP_REGIONS_PATCH=window._UP_REGIONS_PATCH||{};'
632
+ + 'window._UP_REGIONS_PATCH["' + UP_CONTAINER_ID + '"]='
633
+ + JSON.stringify(UP_REGIONS) + ';'
634
+ + 'window._UP_PINS_PATCH=window._UP_PINS_PATCH||{};'
635
+ + 'window._UP_PINS_PATCH["' + UP_CONTAINER_ID + '"]='
636
+ + JSON.stringify(interactivePins) + ';'
637
+ + '<\/script>';
638
+
639
+ // Fix up-page padding
640
+ html = html.replace(/\.up-page\{[^}]*\}/g, '.up-page{max-width:100%;padding:0}');
641
+
642
+ // Move any <style> tags inside <body> into <head>
643
+ var headEndIdx = html.indexOf('</head>');
644
+ if (headEndIdx !== -1) {
645
+ var head = html.slice(0, headEndIdx);
646
+ var rest = html.slice(headEndIdx);
647
+ var bodyStyles = [];
648
+ var cleanRest = rest.replace(/<style[\s\S]*?<\/style>/gi, function(s) {
649
+ bodyStyles.push(s);
650
+ return '';
651
+ });
652
+ html = head + '\n' + bodyStyles.join('\n') + '\n' + regionsPatch + '\n' + cleanRest;
653
+ }
654
+
655
+ var blob = new Blob([html], {type:'text/html'});
656
+ var a = document.createElement('a');
657
+ a.href = URL.createObjectURL(blob);
658
+ var now = new Date();
659
+ a.download = 'chart-' + now.toISOString().slice(0,19).replace(/[:.]/g,'-') + '.html';
660
+ a.click();
661
+ URL.revokeObjectURL(a.href);
662
+ }
663
+
664
+ // ── Show measure bar ──────────────────────────────────────────────────────────
665
+ function buildShowMeasureBar(mBar, mDetails) {
666
+ return function showMeasureBar(timeStr, yDeltas, mId) {
667
+ if(!mBar) return;
668
+ var html='<div class="up-m-stat">ΔX: <span class="up-m-val">'+timeStr+'</span></div>';
669
+ yDeltas.forEach(function(d){
670
+ var sign=d.diff>0?'+':'';
671
+ html+='<div class="up-m-stat"><span class="up-tooltip-dot" style="background:'+d.color+'"></span>'+d.label+': <span class="up-m-val" style="color:'+d.color+'">'+sign+formatVal(d.diff,d.fmt)+'</span></div>';
672
+ });
673
+ mDetails.innerHTML=html;
674
+ mBar.style.display='flex';
675
+ if(mId)mBar.dataset.mid=mId; else mBar.dataset.mid='';
676
+ };
677
+ }
678
+
679
+ // ── Build chart ───────────────────────────────────────────────────────────────
680
+ function buildChart(){
681
+ var container=document.getElementById(UP_CONTAINER_ID);
682
+
683
+ // Restore any state that was captured at export time
684
+ if (window._UP_REGIONS_PATCH && window._UP_REGIONS_PATCH[UP_CONTAINER_ID]) {
685
+ UP_REGIONS = window._UP_REGIONS_PATCH[UP_CONTAINER_ID];
686
+ delete window._UP_REGIONS_PATCH[UP_CONTAINER_ID];
687
+ }
688
+ if (window._UP_PINS_PATCH && window._UP_PINS_PATCH[UP_CONTAINER_ID]) {
689
+ UP_PINS = window._UP_PINS_PATCH[UP_CONTAINER_ID];
690
+ delete window._UP_PINS_PATCH[UP_CONTAINER_ID];
691
+ }
692
+
693
+ var card=container.closest('.up-card');
694
+ var W=container.offsetWidth||window.innerWidth;
695
+ var allData=[UP_X_DATA].concat(UP_Y_DATA);
696
+ var isScatterOnly = UP_HAS_SCATTER && UP_SERIES_CFG.filter(function(s,i){return i>0&&s.stroke!=='rgba(0,0,0,0)';}).length===0;
697
+
698
+ // Tool buttons
699
+ var toolBtns=card?card.querySelectorAll('.up-tool-btn'):[];
700
+ toolBtns.forEach(function(btn){
701
+ // Hide tag + measure for scatter charts
702
+ if (isScatterOnly && (btn.dataset.tool==='tag'||btn.dataset.tool==='measure'||btn.dataset.tool==='annotate')) {
703
+ btn.style.display='none'; return;
704
+ }
705
+ btn.addEventListener('click',function(e){
706
+ e.preventDefault();
707
+ toolBtns.forEach(function(b){b.classList.remove('active');});
708
+ btn.classList.add('active');
709
+ currentTool=btn.dataset.tool;
710
+ if(currentTool==='measure'){
711
+ container.classList.add('up-measure-mode');
712
+ } else {
713
+ container.classList.remove('up-measure-mode');
714
+ measureAnchorIdx=null;
715
+ var bar=document.getElementById('up-measure-bar-'+UP_CONTAINER_ID.replace('up-chart-',''));
716
+ if(bar)bar.style.display='none';
717
+ if(uplotInst)uplotInst.redraw();
718
+ }
719
+ });
720
+ });
721
+
722
+ // Prompt
723
+ var tagPrompt=card?card.querySelector('.up-tag-prompt'):null;
724
+ var tagInput=card?card.querySelector('.up-tag-input'):null;
725
+ var btnCancel=card?card.querySelector('[id^="up-tag-cancel-"]'):null;
726
+ var btnSave=card?card.querySelector('[id^="up-tag-save-"]'):null;
727
+ var tagsList=card?card.querySelector('.up-tags-list'):null;
728
+
729
+ // Pins layer (overlay on uPlot canvas area)
730
+ var pinsLayer=card?card.querySelector('.up-pins-layer'):null;
731
+
732
+ // Annotation prompt (for pins)
733
+ var annPrompt=card?card.querySelector('.up-ann-prompt'):null;
734
+ var annInput=card?card.querySelector('.up-ann-input'):null;
735
+ var annCancel=card?card.querySelector('[id^="up-ann-cancel-"]'):null;
736
+ var annSave=card?card.querySelector('[id^="up-ann-save-"]'):null;
737
+ var pendingPinPos=null;
738
+
739
+ if(tagPrompt && btnCancel && btnSave && tagInput){
740
+ btnCancel.addEventListener('click',function(e){e.preventDefault();tagPrompt.style.display='none';activeTagRegion=null;});
741
+ btnSave.addEventListener('click',function(e){
742
+ e.preventDefault();
743
+ var name=tagInput.value.trim()||'Untitled';
744
+ tagPrompt.style.display='none';
745
+ if(activeTagRegion){
746
+ var tagId='tag_'+Date.now(), color=tagColors[tagColorIdx%tagColors.length]; tagColorIdx++;
747
+ UP_REGIONS.push({x_start:activeTagRegion.min*(UP_IS_TIMESERIES?1000:1),x_end:activeTagRegion.max*(UP_IS_TIMESERIES?1000:1),color:color,opacity:0.05,label:name,_tagId:tagId,_removable:true});
748
+ if(uplotInst)uplotInst.redraw();
749
+ renderTagBubbles(tagsList);
750
+ activeTagRegion=null;
751
+ } else if(window._activeMeasureRegion){
752
+ var mId='m_'+Date.now();
753
+ var mn=Math.min(window._activeMeasureRegion.min,window._activeMeasureRegion.max);
754
+ var mx=Math.max(window._activeMeasureRegion.min,window._activeMeasureRegion.max);
755
+ UP_MEASUREMENTS.push({id:mId,startIdx:uplotInst.valToIdx(mn),endIdx:uplotInst.valToIdx(mx),startX:mn,endX:mx,label:name});
756
+ if(uplotInst)uplotInst.redraw();
757
+ window._activeMeasureRegion=null;
758
+ }
759
+ });
760
+ tagInput.addEventListener('keydown',function(e){if(e.key==='Enter'){e.preventDefault();btnSave.click();}else if(e.key==='Escape'){e.preventDefault();btnCancel.click();}});
761
+ }
762
+
763
+ if(annPrompt && annSave && annCancel && annInput) {
764
+ annCancel.addEventListener('click',function(e){e.preventDefault();annPrompt.style.display='none';pendingPinPos=null;});
765
+ annSave.addEventListener('click',function(e){
766
+ e.preventDefault();
767
+ var name=annInput.value.trim()||'Note';
768
+ annPrompt.style.display='none';
769
+ if(pendingPinPos && uplotInst){
770
+ var pin={id:'pin_'+Date.now(),x:pendingPinPos.x,y_frac:pendingPinPos.y_frac,y:pendingPinPos.y,label:name,color:null};
771
+ UP_PINS.push(pin);
772
+ buildPinEl(uplotInst, pin, pinsLayer, tagsList);
773
+ pendingPinPos=null;
774
+ }
775
+ });
776
+ annInput.addEventListener('keydown',function(e){if(e.key==='Enter'){e.preventDefault();annSave.click();}else if(e.key==='Escape'){e.preventDefault();annCancel.click();}});
777
+ }
778
+
779
+ var fillPlugin=makeFillPlugin(UP_SERIES_CFG.slice(1));
780
+ var threshPlugin=makeThresholdPlugin(UP_BANDS||[]);
781
+ var annPlugin=makeAnnotationPlugin(UP_VLINES,UP_HLINES,UP_REGIONS,UP_IS_TIMESERIES);
782
+ var scatterPlugin=UP_HAS_SCATTER?window._makeScatterPlugin(UP_SCATTER_CFG,UP_TOOLTIP_ID,UP_REGIONS,UP_IS_TIMESERIES):null;
783
+ var plugins=UP_HAS_SCATTER?[fillPlugin,threshPlugin,annPlugin,scatterPlugin]:[fillPlugin,threshPlugin,annPlugin];
784
+
785
+ var mBar=card?card.querySelector('.up-measure-bar'):null;
786
+ var mDetails=card?card.querySelector('.up-measure-details'):null;
787
+ var showMeasureBar=buildShowMeasureBar(mBar,mDetails);
788
+
789
+ // Y-drag zoom state
790
+ var yDragStart=null;
791
+
792
+ var opts={
793
+ width:W, height:UP_HEIGHT,
794
+ plugins:plugins,
795
+ select:{show:true},
796
+ cursor:{
797
+ drag:{x:true,y:false,uni:20}, // we handle Y manually
798
+ bind:{
799
+ dblclick:function(u,targ,handler){
800
+ return function(e){e.preventDefault();upResetZoom();return null;};
801
+ }
802
+ },
803
+ points:{
804
+ size:function(u,si){return 8;},
805
+ fill:function(u,si){return u.series[si].stroke||'#fff';},
806
+ stroke:function(u,si){return u.series[si].stroke||'#000';},
807
+ width:2,
808
+ },
809
+ },
810
+ legend:{show:false},
811
+ scales:UP_SCALES,
812
+ axes:UP_AXES,
813
+ series:UP_SERIES_CFG,
814
+ hooks:{
815
+ ready:[function(u){
816
+ initialScales={};
817
+ Object.keys(u.scales).forEach(function(k){
818
+ var sc=u.scales[k];
819
+ initialScales[k]={min:sc.min,max:sc.max};
820
+ });
821
+ registerForSync(u);
822
+ if(UP_HAS_SCATTER)attachMixedTooltip(u); else attachLineTooltip(u);
823
+ renderTagBubbles(tagsList);
824
+
825
+ // Position pins layer over plot
826
+ if(pinsLayer){
827
+ pinsLayer.style.position='absolute';
828
+ var b=u.bbox,dpr=window.devicePixelRatio||1;
829
+ pinsLayer.style.left=(b.left/dpr)+'px';
830
+ pinsLayer.style.top=(b.top/dpr)+'px';
831
+ pinsLayer.style.width=(b.width/dpr)+'px';
832
+ pinsLayer.style.height=(b.height/dpr)+'px';
833
+ }
834
+
835
+ // ── Mouse events ──────────────────────────────────────────────────
836
+ u.over.addEventListener('mousemove',function(e){
837
+ var rect=u.over.getBoundingClientRect();
838
+ var cx=e.clientX-rect.left, cy=e.clientY-rect.top;
839
+ var idx=u.posToIdx(cx);
840
+ if(idx==null||idx<0)return;
841
+
842
+ // Y-axis drag zoom: detect vertical drag (more Y movement than X)
843
+ if(yDragStart!==null&&currentTool==='zoom'){
844
+ var dx=Math.abs(cx-yDragStart.cx), dy=cy-yDragStart.cy, dyAbs=Math.abs(dy);
845
+ // Once committed to vertical drag, keep going; commit when dy>dx and dy>8px
846
+ if(yDragStart.dragging||(dyAbs>8&&dyAbs>dx*1.5)){
847
+ yDragStart.dragging=true;
848
+ if(dyAbs>1){
849
+ var sks=Object.keys(u.scales).filter(function(k){return k!=='x';});
850
+ sks.forEach(function(sk){
851
+ var sc=u.scales[sk]; if(!sc)return;
852
+ var range=sc.max-sc.min;
853
+ // drag down = zoom out (expand), drag up = zoom in (contract)
854
+ var factor=dy/300;
855
+ var newMin=sc.min+range*factor*0.5;
856
+ var newMax=sc.max-range*factor*0.5;
857
+ if(newMax-newMin>1e-9)u.setScale(sk,{min:newMin,max:newMax});
858
+ });
859
+ yDragStart.cy=cy;
860
+ }
861
+ return; // prevent uPlot's own x-drag selection while doing y-drag
862
+ }
863
+ }
864
+
865
+ if(currentTool==='measure'&&measureAnchorIdx!==null){
866
+ window._up_currentMeasureIdx=idx;
867
+ var yDeltas=[];
868
+ UP_SERIES_CFG.slice(1).forEach(function(cfg,i){
869
+ var si=i+1; if(!seriesVisible[si])return;
870
+ var y1=u.data[si][measureAnchorIdx],y2=u.data[si][idx];
871
+ if(y1!=null&&y2!=null)yDeltas.push({label:cfg.label||'Series '+si,color:cfg.stroke,diff:y2-y1,fmt:cfg._hoverFormat});
872
+ });
873
+ var t1=u.data[0][measureAnchorIdx],t2=u.data[0][idx],tDiff=Math.abs(t2-t1);
874
+ var tStr=UP_IS_TIMESERIES?(tDiff<60?round2(tDiff)+'s':tDiff<3600?Math.floor(tDiff/60)+'m '+round2(tDiff%60)+'s':Math.floor(tDiff/3600)+'h '+Math.floor((tDiff%3600)/60)+'m'):round2(tDiff);
875
+ showMeasureBar(tStr,yDeltas,null);
876
+ u.redraw();
877
+ } else {
878
+ window._up_currentMeasureIdx=null;
879
+ var tsVal=u.data[0][idx], hoveredId=null;
880
+ for(var j=0;j<UP_MEASUREMENTS.length;j++){
881
+ var m=UP_MEASUREMENTS[j];
882
+ var mMin=Math.min(m.startX,m.endX),mMax=Math.max(m.startX,m.endX);
883
+ if(tsVal>=mMin&&tsVal<=mMax){
884
+ hoveredId=m.id;
885
+ var yDeltas=[];
886
+ UP_SERIES_CFG.slice(1).forEach(function(cfg,i){
887
+ var si=i+1;if(!seriesVisible[si])return;
888
+ var y1=u.data[si][m.startIdx],y2=u.data[si][m.endIdx];
889
+ if(y1!=null&&y2!=null)yDeltas.push({label:cfg.label||'Series '+si,color:cfg.stroke,diff:y2-y1,fmt:cfg._hoverFormat});
890
+ });
891
+ var tDiff=Math.abs(m.endX-m.startX);
892
+ var tStr=UP_IS_TIMESERIES?(tDiff<60?round2(tDiff)+'s':tDiff<3600?Math.floor(tDiff/60)+'m '+round2(tDiff%60)+'s':Math.floor(tDiff/3600)+'h '+Math.floor((tDiff%3600)/60)+'m'):round2(tDiff);
893
+ showMeasureBar(tStr,yDeltas,m.id);
894
+ break;
895
+ }
896
+ }
897
+ if(!hoveredId){if(mBar)mBar.style.display='none';if(currentTool==='measure')u.redraw();}
898
+ }
899
+ });
900
+
901
+ u.over.addEventListener('mousedown',function(e){
902
+ var rect=u.over.getBoundingClientRect();
903
+ var cx=e.clientX-rect.left, cy=e.clientY-rect.top;
904
+ var idx=u.posToIdx(cx);
905
+ if(idx==null)return;
906
+
907
+ // Start Y-drag tracking on left-button (we detect vertical vs horizontal later)
908
+ if(e.button===0&&currentTool==='zoom'){
909
+ yDragStart={cx:cx, cy:cy, dragging:false};
910
+ }
911
+
912
+ // Bottom measure bar click-to-delete
913
+ if(cy>=u.bbox.height-18&&cy<=u.bbox.height){
914
+ var tsVal=u.data[0][idx];
915
+ for(var j=0;j<UP_MEASUREMENTS.length;j++){
916
+ var m=UP_MEASUREMENTS[j];
917
+ if(tsVal>=Math.min(m.startX,m.endX)&&tsVal<=Math.max(m.startX,m.endX)){
918
+ UP_MEASUREMENTS.splice(j,1);
919
+ if(mBar)mBar.style.display='none';
920
+ u.redraw();e.stopPropagation();return;
921
+ }
922
+ }
923
+ }
924
+
925
+ if(currentTool==='measure'){
926
+ if(e.button===2){
927
+ if(measureAnchorIdx!==null){
928
+ window._activeMeasureRegion={min:u.data[0][measureAnchorIdx],max:u.data[0][idx]};
929
+ if(tagPrompt){tagPrompt.style.display='flex';tagInput.value='';setTimeout(function(){tagInput.focus();},10);}
930
+ measureAnchorIdx=null;window._up_currentMeasureIdx=null;u.redraw();
931
+ }
932
+ } else if(e.button===0){
933
+ measureAnchorIdx=measureAnchorIdx===null?idx:null;
934
+ if(measureAnchorIdx===null){window._up_currentMeasureIdx=null;if(mBar)mBar.style.display='none';}
935
+ u.redraw();
936
+ }
937
+ }
938
+
939
+ // Annotate mode: left click drops a pin
940
+ if(currentTool==='annotate'&&e.button===0){
941
+ var b=u.bbox, dpr=window.devicePixelRatio||1;
942
+ var xVal=u.posToVal(cx,'x');
943
+ // cx/cy are CSS px from getBoundingClientRect.
944
+ // b.top is canvas (physical) px — divide by dpr to get CSS px offset.
945
+ var plotH = u.over.offsetHeight || b.height / (window.devicePixelRatio||1);
946
+ var y_frac = Math.max(0, Math.min(1, cy / plotH));
947
+ var y_val = u.posToVal(cy, 'y');
948
+ pendingPinPos={
949
+ x: UP_IS_TIMESERIES?xVal*1000:xVal,
950
+ y_frac: y_frac,
951
+ y: y_val
952
+ };
953
+ if(annPrompt && annInput){
954
+ annPrompt.style.display='flex';
955
+ annInput.value='';
956
+ setTimeout(function(){annInput.focus();},10);
957
+ }
958
+ }
959
+ });
960
+
961
+ u.over.addEventListener('mouseup',function(e){
962
+ yDragStart=null;
963
+ });
964
+
965
+ u.root.addEventListener('contextmenu',function(e){
966
+ if(currentTool==='measure')e.preventDefault();
967
+ });
968
+ u.over.addEventListener('mouseleave',function(){
969
+ if(measureAnchorIdx===null&&mBar)mBar.style.display='none';
970
+ yDragStart=null;
971
+ });
972
+ }],
973
+ setSelect:[function(u){
974
+ if(tooltipEl)tooltipEl.style.display='none';
975
+ // In annotate mode clicks should not trigger a selection
976
+ if(currentTool==='annotate'){u.setSelect({left:0,top:0,width:0,height:0},false);return;}
977
+ if(u.select.width>10){
978
+ var min=u.posToVal(u.select.left,'x');
979
+ var max=u.posToVal(u.select.left+u.select.width,'x');
980
+ if(currentTool==='zoom'){
981
+ u.setScale('x',{min:min,max:max});
982
+ u.setSelect({left:0,top:0,width:0,height:0},false);
983
+ u.redraw();
984
+ } else if(currentTool==='tag'){
985
+ activeTagRegion={min:min,max:max};
986
+ u.setSelect({left:0,top:0,width:0,height:0},false);
987
+ if(tagPrompt){tagPrompt.style.display='flex';tagInput.value='';setTimeout(function(){tagInput.focus();},10);}
988
+ } else {
989
+ u.setSelect({left:0,top:0,width:0,height:0},false);
990
+ }
991
+ }
992
+ }],
993
+ draw:[function(u){
994
+ // Re-position pins on every redraw (zoom/pan)
995
+ if(pinsLayer){
996
+ var b=u.bbox,dpr=window.devicePixelRatio||1;
997
+ pinsLayer.style.left=(b.left/dpr)+'px';
998
+ pinsLayer.style.top=(b.top/dpr)+'px';
999
+ pinsLayer.style.width=(b.width/dpr)+'px';
1000
+ pinsLayer.style.height=(b.height/dpr)+'px';
1001
+ UP_PINS.forEach(function(pin){
1002
+ var el=pinsLayer.querySelector('#pin-'+pin.id);
1003
+ if(el)positionPinEl(u,el,pin);
1004
+ });
1005
+ }
1006
+ }],
1007
+ }
1008
+ };
1009
+
1010
+ uplotInst=new uPlot(opts,allData,container);
1011
+
1012
+ // Track which pin IDs have been built — prevents double-build across patch + code pins
1013
+ var builtPinIds = {};
1014
+
1015
+ if (UP_PINS && UP_PINS.length > 0) {
1016
+ UP_PINS.forEach(function(pin) {
1017
+ builtPinIds[pin.id] = true;
1018
+ buildPinEl(uplotInst, pin, pinsLayer, tagsList);
1019
+ });
1020
+ }
1021
+
1022
+ // Build code-defined pins (from fig.pin() API) — skip any already built via patch
1023
+ if (typeof UP_CODE_PINS !== 'undefined' && UP_CODE_PINS.length > 0) {
1024
+ UP_CODE_PINS.forEach(function(pin) {
1025
+ if (builtPinIds[pin.id]) return; // already restored via patch — skip
1026
+ var pinCopy = {id: pin.id, x: pin.x, label: pin.label, y_frac: pin.y_frac, color: pin.color || null};
1027
+ UP_PINS.push(pinCopy);
1028
+ builtPinIds[pin.id] = true;
1029
+ buildPinEl(uplotInst, pinCopy, pinsLayer, tagsList);
1030
+ });
1031
+ }
1032
+ seriesVisible=[null];
1033
+ for(var i=1;i<UP_SERIES_CFG.length;i++)seriesVisible.push(true);
1034
+ legendEls=[null];
1035
+ var legContainer=container.closest('.up-card');
1036
+ var legEls=legContainer?legContainer.querySelectorAll('.up-leg-item[data-si]'):document.querySelectorAll('.up-leg-item[data-si]');
1037
+ legEls.forEach(function(el){
1038
+ var kind=el.dataset.kind,siRaw=el.dataset.si;
1039
+ if(kind==='line'){
1040
+ var si=parseInt(siRaw); if(isNaN(si))return;
1041
+ legendEls[si]=el;
1042
+ el.addEventListener('click',(function(e,s){return function(ev){ev.preventDefault();legendClick(e,s);};})(el,si));
1043
+ } else if(kind==='scatter'){
1044
+ var scIdx=parseInt(siRaw.replace('sc-','')); if(isNaN(scIdx))return;
1045
+ el.addEventListener('click',(function(e,idx){return function(ev){ev.preventDefault();scatterLegendClick(e,idx);};})(el,scIdx));
1046
+ }
1047
+ });
1048
+
1049
+ // Export button
1050
+ var exportBtn=card?card.querySelector('.up-export-btn'):null;
1051
+ if(exportBtn)exportBtn.addEventListener('click',function(){exportAsHtml();});
1052
+ }
1053
+
1054
+ // ── Reset zoom ────────────────────────────────────────────────────────────────
1055
+ function upResetZoom(){
1056
+ if(!uplotInst)return;
1057
+ var xRange=(UP_INITIAL_RANGES&&UP_INITIAL_RANGES.x)?UP_INITIAL_RANGES.x:(initialScales&&initialScales.x?[initialScales.x.min,initialScales.x.max]:null);
1058
+ if(xRange)uplotInst.setScale('x',{min:xRange[0],max:xRange[1]});
1059
+ // Also reset unlocked Y scales
1060
+ Object.keys(initialScales||{}).forEach(function(k){
1061
+ if(k!=='x'&&initialScales[k]){
1062
+ uplotInst.setScale(k,{min:initialScales[k].min,max:initialScales[k].max});
1063
+ }
1064
+ });
1065
+ }
1066
+ window[UP_RESET_FN]=upResetZoom;
1067
+
1068
+ window.addEventListener('load',function(){
1069
+ if (typeof uPlot === 'undefined') {
1070
+ var el = document.getElementById(UP_CONTAINER_ID);
1071
+ if (el) el.innerHTML = '<div style="padding:24px;color:#f43f5e;font-size:13px;">⚠ uPlot failed to load. Open this file via a local web server or use the bundled version.</div>';
1072
+ return;
1073
+ }
1074
+ buildChart();
1075
+ window.addEventListener('resize',function(){
1076
+ if(!uplotInst)return;
1077
+ uplotInst.setSize({width:document.getElementById(UP_CONTAINER_ID).offsetWidth,height:UP_HEIGHT});
1078
+ });
1079
+ });
1080
+ """