89 this.curves_data = this.curves.map(_unused => []); |
89 this.curves_data = this.curves.map(_unused => []); |
90 this.max_data_length = this.args[0]; |
90 this.max_data_length = this.args[0]; |
91 } |
91 } |
92 |
92 |
93 dispatch(value,oldval, index) { |
93 dispatch(value,oldval, index) { |
|
94 // TODO: get PLC time instead of browser time |
|
95 let time = Date.now(); |
|
96 |
94 // naive local buffer impl. |
97 // naive local buffer impl. |
95 // data is updated only when graph is visible |
98 // data is updated only when graph is visible |
96 // TODO: replace with separate recording |
99 // TODO: replace with separate recording |
97 |
100 |
98 this.curves_data[index].push(value); |
101 this.curves_data[index].push([time, value]); |
99 let data_length = this.curves_data[index].length; |
102 let data_length = this.curves_data[index].length; |
100 let ymin_damaged = false; |
103 let ymin_damaged = false; |
101 let ymax_damaged = false; |
104 let ymax_damaged = false; |
102 let overflow; |
105 let overflow; |
103 |
106 |
104 if(data_length > this.max_data_length){ |
107 if(data_length > this.max_data_length){ |
105 // remove first item |
108 // remove first item |
106 overflow = this.curves_data[index].shift(); |
109 [this.xmin, overflow] = this.curves_data[index].shift(); |
107 data_length = data_length - 1; |
110 data_length = data_length - 1; |
108 } |
111 } else { |
|
112 if(this.xmin == undefined){ |
|
113 this.xmin = time; |
|
114 } |
|
115 } |
|
116 |
|
117 this.xmax = time; |
|
118 let Xlength = this.xmax - this.xmin; |
109 |
119 |
110 if(!this.fixed_y_range){ |
120 if(!this.fixed_y_range){ |
111 ymin_damaged = overflow <= this.ymin; |
121 ymin_damaged = overflow <= this.ymin; |
112 ymax_damaged = overflow >= this.ymax; |
122 ymax_damaged = overflow >= this.ymax; |
113 if(value > this.ymax){ |
123 if(value > this.ymax){ |
119 this.ymin = value; |
129 this.ymin = value; |
120 } |
130 } |
121 } |
131 } |
122 let Ylength = this.ymax - this.ymin; |
132 let Ylength = this.ymax - this.ymin; |
123 |
133 |
124 // recompute X range based on curent time ad buffer depth |
|
125 // TODO: get PLC time instead of browser time |
|
126 const d = new Date(); |
|
127 let time = d.getTime(); |
|
128 |
|
129 // FIXME: this becomes wrong when graph is not visible and updated all the time |
|
130 [this.xmin, this.xmax] = [time - data_length*1000/this.frequency, time]; |
|
131 let Xlength = this.xmax - this.xmin; |
|
132 |
|
133 |
|
134 // recompute curves "d" attribute |
134 // recompute curves "d" attribute |
135 // FIXME: use SVG getPathData and setPathData when available. |
135 // FIXME: use SVG getPathData and setPathData when available. |
136 // https://svgwg.org/specs/paths/#InterfaceSVGPathData |
136 // https://svgwg.org/specs/paths/#InterfaceSVGPathData |
137 // https://github.com/jarek-foksa/path-data-polyfill |
137 // https://github.com/jarek-foksa/path-data-polyfill |
138 |
138 |
139 let [base_point, xvect, yvect] = this.reference.getBaseRef(); |
139 let [base_point, xvect, yvect] = this.reference.getBaseRef(); |
140 this.curves_d_attr = |
140 this.curves_d_attr = |
141 zip(this.curves_data, this.curves).map(([data,curve]) => { |
141 zip(this.curves_data, this.curves).map(([data,curve]) => { |
142 let new_d = data.map((y, i) => { |
142 let new_d = data.map(([x,y], i) => { |
143 // compute curve point from data, ranges, and base_ref |
143 // compute curve point from data, ranges, and base_ref |
144 let x = this.xmin + i * Xlength / data_length; |
|
145 let xv = vectorscale(xvect, (x - this.xmin) / Xlength); |
144 let xv = vectorscale(xvect, (x - this.xmin) / Xlength); |
146 let yv = vectorscale(yvect, (y - this.ymin) / Ylength); |
145 let yv = vectorscale(yvect, (y - this.ymin) / Ylength); |
147 let px = base_point.x + xv.x + yv.x; |
146 let px = base_point.x + xv.x + yv.x; |
148 let py = base_point.y + xv.y + yv.y; |
147 let py = base_point.y + xv.y + yv.y; |
149 if(!this.fixed_y_range){ |
148 if(!this.fixed_y_range){ |
221 return new DOMPoint(p2 * p1.x , p2 * p1.y); |
220 return new DOMPoint(p2 * p1.x , p2 * p1.y); |
222 }; |
221 }; |
223 |
222 |
224 function move_elements_to_group(elements) { |
223 function move_elements_to_group(elements) { |
225 let newgroup = document.createElementNS(xmlns,"g"); |
224 let newgroup = document.createElementNS(xmlns,"g"); |
|
225 newgroup.id = Date.now().toString(36) + Math.random().toString(36).substr(2); |
|
226 |
226 for(let element of elements){ |
227 for(let element of elements){ |
227 let parent = element.parentElement; |
228 let parent = element.parentElement; |
228 if(parent !== null) |
229 if(parent !== null) |
229 parent.removeChild(element); |
230 parent.removeChild(element); |
230 newgroup.appendChild(element); |
231 newgroup.appendChild(element); |
233 } |
234 } |
234 function getLinesIntesection(l1, l2) { |
235 function getLinesIntesection(l1, l2) { |
235 let [l1start, l1vect] = l1; |
236 let [l1start, l1vect] = l1; |
236 let [l2start, l2vect] = l2; |
237 let [l2start, l2vect] = l2; |
237 |
238 |
238 let b; |
239 |
239 /* |
240 /* |
240 Compute intersection of two lines |
241 Compute intersection of two lines |
241 ================================= |
242 ================================= |
242 |
243 |
243 ^ l2vect |
244 ^ l2vect |
248 / intersection |
249 / intersection |
249 / |
250 / |
250 / |
251 / |
251 l2start |
252 l2start |
252 |
253 |
253 intersection = l1start + l1vect * a |
254 */ |
254 intersection = l2start + l2vect * b |
255 let [x1, y1, x3, y3] = [l1start.x, l1start.y, l2start.x, l2start.y]; |
255 ==> solve : "l1start + l1vect * a = l2start + l2vect * b" to find a and b and then intersection |
256 let [x2, y2, x4, y4] = [x1+l1vect.x, y1+l1vect.y, x3+l2vect.x, y3+l2vect.y]; |
256 |
257 |
257 (1) l1start.y + l1vect.y * a = l2start.y + l2vect.y * b |
258 // line intercept math by Paul Bourke http://paulbourke.net/geometry/pointlineplane/ |
258 (2) l1start.x + l1vect.x * a = l2start.x + l2vect.x * b |
259 // Determine the intersection point of two line segments |
259 |
260 // Return FALSE if the lines don't intersect |
260 // express a |
261 |
261 (1) a = (l2start.y + l2vect.y * b - l1start.y) / l1vect.y |
262 // Check if none of the lines are of length 0 |
262 |
263 if ((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) { |
263 // substitute a to have only b |
264 return false |
264 (1+2) l1start.x + l1vect.x * (l2start.y + l2vect.y * b - l1start.y) / l1vect.y = l2start.x + l2vect.x * b |
265 } |
265 |
266 |
266 // expand to isolate b |
267 denominator = ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)) |
267 l1start.x + l1vect.x * l2start.y / l1vect.y + l2vect.y / l1vect.y * b - l1start.y / l1vect.y = l2start.x + l2vect.x * b |
268 |
268 |
269 // Lines are parallel |
269 // factorize b |
270 if (denominator === 0) { |
270 l1start.x + l1vect.x * l2start.y / l1vect.y - l1start.y / l1vect.y - l2start.x = b * ( l2vect.x - l2vect.y / l1vect.y) |
271 return false |
271 |
272 } |
272 // extract b |
273 |
273 b = (l1start.x + l1vect.x * l2start.y / l1vect.y - l1start.y / l1vect.y - l2start.x) / ( l2vect.x - l2vect.y / l1vect.y) |
274 let ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator |
274 */ |
275 let ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denominator |
275 |
276 |
276 /* avoid divison by zero by swapping (1) and (2) */ |
277 // Return a object with the x and y coordinates of the intersection |
277 if(l1vect.y == 0 ){ |
278 let x = x1 + ua * (x2 - x1) |
278 b = (l1start.y + l1vect.y * l2start.x / l1vect.x - l1start.x / l1vect.x - l2start.y) / ( l2vect.y - l2vect.x / l1vect.x); |
279 let y = y1 + ua * (y2 - y1) |
279 }else{ |
280 |
280 b = (l1start.x + l1vect.x * l2start.y / l1vect.y - l1start.y / l1vect.y - l2start.x) / ( l2vect.x - l2vect.y / l1vect.y); |
281 return new DOMPoint(x,y); |
281 } |
|
282 |
|
283 return new DOMPoint(l2start.x + l2vect.x * b, l2start.y + l2vect.y * b); |
|
284 }; |
282 }; |
285 |
283 |
286 class ReferenceFrame { |
284 class ReferenceFrame { |
287 constructor( |
285 constructor( |
288 // [[Xminor,Xmajor], [Yminor,Ymajor]] |
286 // [[Xminor,Xmajor], [Yminor,Ymajor]] |
322 getClipPathPathDattr(){ |
320 getClipPathPathDattr(){ |
323 return this.clipPathPathDattr; |
321 return this.clipPathPathDattr; |
324 } |
322 } |
325 |
323 |
326 applyRanges(ranges){ |
324 applyRanges(ranges){ |
327 for(let [range,axis] of zip(ranges,this.axes)){ |
325 let origin_moves = zip(ranges,this.axes).map(([range,axis]) => axis.applyRange(...range)); |
328 axis.applyRange(...range); |
326 zip(origin_moves.reverse(),this.axes).forEach(([vect,axis]) => axis.moveOrigin(vect)); |
329 } |
|
330 } |
327 } |
331 |
328 |
332 getBasePoint() { |
329 getBasePoint() { |
333 let [[xstart, xvect], [ystart, yvect]] = this.axes.map(axis => axis.line); |
330 let [[xstart, xvect], [ystart, yvect]] = this.axes.map(axis => axis.line); |
334 |
331 |
380 }; |
377 }; |
381 }; |
378 }; |
382 |
379 |
383 // group marks an labels together |
380 // group marks an labels together |
384 let parent = line.parentElement; |
381 let parent = line.parentElement; |
385 let marks_group = move_elements_to_group(marks); |
382 this.marks_group = move_elements_to_group(marks); |
386 let marks_and_label_group = move_elements_to_group([marks_group, label]); |
383 this.marks_and_label_group = move_elements_to_group([this.marks_group, label]); |
387 let group = move_elements_to_group([marks_and_label_group,line]); |
384 this.group = move_elements_to_group([this.marks_and_label_group,line]); |
388 parent.appendChild(group); |
385 parent.appendChild(this.group); |
389 |
386 |
390 // Add transforms to group |
387 // Add transforms to group |
391 for(let name of ["base","origin"]){ |
388 for(let name of ["base","origin"]){ |
392 let transform = svg_root.createSVGTransform(); |
389 let transform = svg_root.createSVGTransform(); |
393 group.transform.baseVal.appendItem(transform); |
390 this.group.transform.baseVal.appendItem(transform); |
394 this[name+"_transform"]=transform; |
391 this[name+"_transform"]=transform; |
395 }; |
392 }; |
396 |
393 |
397 this.group = group; |
394 this.marks_and_label_group_transform = svg_root.createSVGTransform(); |
398 this.marks_group = marks_group; |
395 this.marks_and_label_group.transform.baseVal.appendItem(this.marks_and_label_group_transform); |
399 this.marks_and_label_group = marks_and_label_group; |
396 |
400 |
397 this.duplicates = []; |
401 this.mlg_clones = []; |
|
402 this.last_mark_count = 0; |
398 this.last_mark_count = 0; |
403 } |
399 } |
404 |
400 |
405 setBasePoint(base_point){ |
401 setBasePoint(base_point){ |
406 // move Axis to base point |
402 // move Axis to base point |
413 // | ' | ==> ' |
409 // | ' | ==> ' |
414 // | 0 0 |
410 // | 0 0 |
415 // | | |
411 // | | |
416 |
412 |
417 for(let [markname,mark] of zip(["minor", "major"],this.marks)){ |
413 for(let [markname,mark] of zip(["minor", "major"],this.marks)){ |
418 let transform = this[markname+"_base_transform"]; |
|
419 let pos = vector( |
414 let pos = vector( |
420 // Marks are expected to be paths |
415 // Marks are expected to be paths |
421 // paths are expected to be lines |
416 // paths are expected to be lines |
422 // intersection with axis line is taken |
417 // intersection with axis line is taken |
423 // as reference for mark position |
418 // as reference for mark position |
424 base_point, getLinesIntesection( |
419 getLinesIntesection( |
425 this.line, lineFromPath(mark))); |
420 this.line, lineFromPath(mark)),base_point); |
426 this[markname+"_base_transform"].setTranslate(-pos.x, -pos.x); |
421 this[markname+"_base_transform"].setTranslate(pos.x - v.x, pos.y - v.y); |
427 if(markname == "major"){ // label follow major mark |
422 if(markname == "major"){ // label follow major mark |
428 this.label_base_transform.setTranslate(-pos.x, -pos.x); |
423 this.label_base_transform.setTranslate(pos.x - v.x, pos.y - v.y); |
429 } |
424 } |
430 } |
425 } |
431 } |
426 } |
|
427 |
|
428 moveOrigin(vect){ |
|
429 this.origin_transform.setTranslate(vect.x, vect.y); |
|
430 } |
432 |
431 |
433 applyRange(min, max){ |
432 applyRange(min, max){ |
434 let range = max - min; |
433 let range = max - min; |
435 |
434 |
436 // compute how many units for a mark |
435 // compute how many units for a mark |
486 let offset = (max>=0 && min>=0) ? 0 : ( |
485 let offset = (max>=0 && min>=0) ? 0 : ( |
487 (max<0 && min<0) ? range : -min); |
486 (max<0 && min<0) ? range : -min); |
488 |
487 |
489 // compute unit vector |
488 // compute unit vector |
490 let [_start, vect] = this.line; |
489 let [_start, vect] = this.line; |
491 let unit_vect = vectorscale(vect, unit/range); |
490 let unit_vect = vectorscale(vect, 1/range); |
492 let [umin, umax, uoffset] = [min,max,offset].map(val => Math.round(val/unit)); |
491 let [mark_min, mark_max, mark_offset] = [min,max,offset].map(val => Math.round(val/unit)); |
493 let mark_count = umax-umin; |
492 let mark_count = mark_max-mark_min; |
494 |
493 |
495 // apply unit vector to marks and label |
494 // apply unit vector to marks and label |
496 // offset is a representing position of an |
495 // offset is a representing position of an |
497 // axis along the opposit axis line, expressed in major marks units |
496 // axis along the opposit axis line, expressed in major marks units |
498 // unit_vect is the translation in between to major marks |
497 // unit_vect is unit vector |
499 |
498 |
500 // ^ |
499 // ^ |
501 // | unit_vect |
500 // | unit_vect |
502 // |<---> |
501 // |<---> |
503 // _________|__________> |
502 // _________|__________> |
507 // v xoffset| |
506 // v xoffset| |
508 // X<------>| |
507 // X<------>| |
509 // base_point |
508 // base_point |
510 |
509 |
511 // move major marks and label to first positive mark position |
510 // move major marks and label to first positive mark position |
512 let v = vectorscale(unit_vect, offset+unit); |
511 let v = vectorscale(unit_vect, unit); |
513 this.label_slide_transform.setTranslate(v.x, v.y); |
512 this.label_slide_transform.setTranslate(v.x, v.y); |
514 this.major_slide_transform.setTranslate(v.x, v.y); |
513 this.major_slide_transform.setTranslate(v.x, v.y); |
515 // move minor mark to first half positive mark position |
514 // move minor mark to first half positive mark position |
516 let h = vectorscale(unit_vect, offset+(unit/2)); |
515 v = vectorscale(unit_vect, unit/2); |
517 this.minor_slide_transform.setTranslate(h.x, h.y); |
516 this.minor_slide_transform.setTranslate(v.x, v.y); |
518 |
517 |
519 // duplicate marks and labels as needed |
518 // duplicate marks and labels as needed |
520 let current_mark_count = this.mlg_clones.length; |
519 let current_mark_count = this.duplicates.length; |
521 for(let i = current_mark_count; i <= mark_count; i++){ |
520 for(let i = current_mark_count; i <= mark_count; i++){ |
522 // cloneNode() label and add a svg:use of marks in a new group |
521 // cloneNode() label and add a svg:use of marks in a new group |
523 let newgroup = document.createElementNS(xmlns,"g"); |
522 let newgroup = document.createElementNS(xmlns,"g"); |
524 let transform = svg_root.createSVGTransform(); |
523 let transform = svg_root.createSVGTransform(); |
525 let newlabel = this.label.cloneNode(true); |
524 let newlabel = this.label.cloneNode(true); |
528 newuseAttr.value = "#"+this.marks_group.id; |
527 newuseAttr.value = "#"+this.marks_group.id; |
529 newuse.setAttributeNode(newuseAttr); |
528 newuse.setAttributeNode(newuseAttr); |
530 newgroup.transform.baseVal.appendItem(transform); |
529 newgroup.transform.baseVal.appendItem(transform); |
531 newgroup.appendChild(newlabel); |
530 newgroup.appendChild(newlabel); |
532 newgroup.appendChild(newuse); |
531 newgroup.appendChild(newuse); |
533 this.mlg_clones.push([transform,newgroup]); |
532 this.duplicates.push([transform,newgroup]); |
534 } |
533 } |
535 |
534 |
536 // move marks and labels, set labels |
535 // move marks and labels, set labels |
537 for(let u = 0; u <= mark_count; u++){ |
536 // |
|
537 // min > 0, max > 0, offset = 0 |
|
538 // ^ |
|
539 // |________> |
|
540 // '| | ' | |
|
541 // | 6 7 |
|
542 // X |
|
543 // base_point |
|
544 // |
|
545 // min < 0, max > 0, offset = -min |
|
546 // ^ |
|
547 // _________|__________> |
|
548 // ' | ' | ' | ' |
|
549 // -1 | 1 |
|
550 // offset | |
|
551 // X<------>| |
|
552 // base_point |
|
553 // |
|
554 // min < 0, max < 0, offset = range |
|
555 // ^ |
|
556 // ____________| |
|
557 // ' | ' | |' |
|
558 // -5 -4 | |
|
559 // offset | |
|
560 // X<--------->| |
|
561 // base_point |
|
562 |
|
563 for(let mark_index = 0; mark_index <= mark_count; mark_index++){ |
538 let i = 0; |
564 let i = 0; |
539 let val = (umin + u) * unit; |
565 let val = (mark_min + mark_index) * unit; |
540 let vec = vectorscale(unit_vect, offset + val); |
566 let vec = vectorscale(unit_vect, offset + val - min); |
541 let text = this.format ? sprintf(this.format, val) : val.toString(); |
567 let text = this.format ? sprintf(this.format, val) : val.toString(); |
542 if(u == uoffset){ |
568 if(mark_index == mark_offset){ |
543 // apply offset to original marks and label groups |
569 // apply offset to original marks and label groups |
544 this.origin_transform.setTranslate(vec.x, vec.y); |
570 this.marks_and_label_group_transform.setTranslate(vec.x, vec.y); |
545 |
571 |
546 // update original label text |
572 // update original label text |
547 this.label.getElementsByTagName("tspan")[0].textContent = text; |
573 this.label.getElementsByTagName("tspan")[0].textContent = text; |
548 } else { |
574 } else { |
549 let [transform,element] = this.mlg_clones[i++]; |
575 let [transform,element] = this.duplicates[i++]; |
550 |
576 |
551 // apply unit vector*N to marks and label groups |
577 // apply unit vector*N to marks and label groups |
552 transform.setTranslate(vec.x, vec.y); |
578 transform.setTranslate(vec.x, vec.y); |
553 |
579 |
554 // update label text |
580 // update label text |
561 } |
587 } |
562 } |
588 } |
563 |
589 |
564 // dettach marks and label from group if not anymore visible |
590 // dettach marks and label from group if not anymore visible |
565 for(let i = current_mark_count; i < this.last_mark_count; i++){ |
591 for(let i = current_mark_count; i < this.last_mark_count; i++){ |
566 let [transform,element] = this.mlg_clones[i]; |
592 let [transform,element] = this.duplicates[i]; |
567 this.group.removeChild(element); |
593 this.group.removeChild(element); |
568 } |
594 } |
569 |
595 |
570 this.last_mark_count = current_mark_count; |
596 this.last_mark_count = current_mark_count; |
|
597 |
|
598 return vectorscale(unit_vect, offset); |
571 } |
599 } |
572 } |
600 } |
573 || |
601 || |