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/__init__.py +37 -0
- youplot/colors/__init__.py +0 -0
- youplot/colors/palette.py +106 -0
- youplot/examples/basic_line.py +123 -0
- youplot/figure.py +518 -0
- youplot/options/__init__.py +0 -0
- youplot/options/annotations.py +60 -0
- youplot/options/axes.py +22 -0
- youplot/render/css.py +578 -0
- youplot/render/html.py +320 -0
- youplot/render/js.py +1080 -0
- youplot/render/scatter_js.py +195 -0
- youplot/render/serializer.py +242 -0
- youplot/series/__init__.py +2 -0
- youplot/series/line.py +70 -0
- youplot/series/scatter.py +76 -0
- youplot/themes/__init__.py +0 -0
- youplot/themes/base.py +105 -0
- youplot/utils/__init__.py +0 -0
- youplot/utils/browser.py +22 -0
- youplot/utils/data.py +150 -0
- youplot/vendor/__init__.py +12 -0
- youplot/vendor/__pycache__/__init__.cpython-311.pyc +0 -0
- youplot/vendor/uplot.iife.min.js +2 -0
- youplot/vendor/uplot.min.css +1 -0
- youplot-1.0.0.dist-info/METADATA +224 -0
- youplot-1.0.0.dist-info/RECORD +30 -0
- youplot-1.0.0.dist-info/WHEEL +5 -0
- youplot-1.0.0.dist-info/licenses/LICENSE +21 -0
- youplot-1.0.0.dist-info/top_level.txt +1 -0
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='×';
|
|
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 = '×';
|
|
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 = '×';
|
|
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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
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&¤tTool==='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&¤tTool==='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
|
+
"""
|