Edouard@3470: // widget_xygraph.ysl2 Edouard@3470: widget_desc("XYGraph") { Edouard@3470: longdesc Edouard@3470: || Edouard@3470: XYGraph draws a cartesian trend graph re-using styles given for axis, Edouard@3470: grid/marks, legends and curves. Edouard@3470: Edouard@3470: Elements labeled "x_axis" and "y_axis" are svg:groups containg: Edouard@3470: - "axis_label" svg:text gives style an alignment for axis labels. Edouard@3470: - "interval_major_mark" and "interval_minor_mark" are svg elements to be Edouard@3470: duplicated along axis line to form intervals marks. Edouard@3470: - "axis_line" svg:path is the axis line. Paths must be intersect and their Edouard@3470: bounding box is the chart wall. Edouard@3470: Edouard@3470: Elements labeled "curve_0", "curve_1", ... are paths whose styles are used Edouard@3470: to draw curves corresponding to data from variables passed as HMI tree paths. Edouard@3470: "curve_0" is mandatory. HMI variables outnumbering given curves are ignored. Edouard@3470: Edouard@3470: || Edouard@3470: Edouard@3470: shortdesc > Cartesian trend graph showing values of given variables over time Edouard@3470: Edouard@3470: path name="value" count="1+" accepts="HMI_INT,HMI_REAL" > value Edouard@3470: Edouard@3470: arg name="size" accepts="int" > buffer size Edouard@3470: arg name="xformat" count="optional" accepts="string" > format string for X label Edouard@3470: arg name="yformat" count="optional" accepts="string" > format string for Y label Edouard@3470: arg name="ymin" count="optional" accepts="int,real" > minimum value foe Y axis Edouard@3470: arg name="ymax" count="optional" accepts="int,real" > maximum value for Y axis Edouard@3470: } Edouard@3470: Edouard@3470: widget_class("XYGraph") { Edouard@3470: || Edouard@3470: frequency = 1; Edouard@3470: init() { Edouard@3470: [this.x_size, Edouard@3470: this.x_format, this.y_format] = this.args; Edouard@3470: Edouard@3470: // Min and Max given with paths are meant to describe visible range, Edouard@3470: // not to clip data. Edouard@3470: this.clip = false; Edouard@3470: Edouard@3470: let y_min = -Infinity, y_max = Infinity; Edouard@3470: Edouard@3470: // Compute visible Y range by merging fixed curves Y ranges Edouard@3470: for(let minmax of this.minmaxes){ Edouard@3470: if(minmax){ Edouard@3470: let [min,max] = minmax; Edouard@3470: if(min < y_min) Edouard@3470: y_min = min; Edouard@3470: if(max > y_max) Edouard@3470: y_max = max; Edouard@3470: } Edouard@3470: } Edouard@3470: Edouard@3470: if(y_min !== -Infinity && y_max !== Infinity){ Edouard@3470: this.fixed_y_range = true; Edouard@3470: } else { Edouard@3470: this.fixed_y_range = false; Edouard@3470: } Edouard@3470: Edouard@3470: this.ymin = y_min; Edouard@3470: this.ymax = y_max; Edouard@3470: Edouard@3470: this.curves = []; Edouard@3470: this.init_specific(); Edouard@3470: Edouard@3470: this.reference = new ReferenceFrame( Edouard@3470: [[this.x_interval_minor_mark, this.x_interval_major_mark], Edouard@3470: [this.y_interval_minor_mark, this.y_interval_major_mark]], Edouard@3470: [this.y_axis_label, this.x_axis_label], Edouard@3470: [this.x_axis_line, this.y_axis_line], Edouard@3470: [this.x_format, this.y_format]); Edouard@3470: Edouard@3470: // create path and attach it to widget Edouard@3470: clipPath = document.createElementNS(xmlns,"clipPath"); Edouard@3470: clipPathPath = document.createElementNS(xmlns,"path"); Edouard@3470: clipPathPathDattr = document.createAttribute("d"); Edouard@3470: clipPathPathDattr.value = this.reference.getClipPathPathDattr(); Edouard@3470: clipPathPath.setAttributeNode(clipPathPathDattr); Edouard@3470: clipPath.appendChild(clipPathPath); Edouard@3470: this.element.appendChild(clipPath); Edouard@3470: Edouard@3470: // assign created clipPath to clip-path property of curves Edouard@3470: for(let curve of this.curves){ Edouard@3470: curve.setAttribute("clip-path", "url(#" + clipPath.id + ")"); Edouard@3470: } Edouard@3470: Edouard@3470: this.curves_data = []; Edouard@3470: this.max_data_length = this.args[0]; Edouard@3470: } Edouard@3470: Edouard@3470: dispatch(value,oldval, index) { Edouard@3470: // naive local buffer impl. Edouard@3470: // data is updated only when graph is visible Edouard@3470: // TODO: replace with separate recording Edouard@3470: Edouard@3470: this.curves_data[index].push(value); Edouard@3470: let data_length = this.curves_data[index].length; Edouard@3470: let ymin_damaged = false; Edouard@3470: let ymax_damaged = false; Edouard@3470: let overflow; Edouard@3470: Edouard@3470: if(data_length > this.max_data_length){ Edouard@3470: // remove first item Edouard@3470: overflow = this.curves_data[index].shift(); Edouard@3470: data_length = data_length - 1; Edouard@3470: } Edouard@3470: Edouard@3470: if(!this.fixed_y_range){ Edouard@3470: ymin_damaged = overflow <= this.ymin; Edouard@3470: ymax_damaged = overflow >= this.ymax; Edouard@3470: if(value > this.ymax){ Edouard@3470: ymax_damaged = false; Edouard@3470: this.ymax = value; Edouard@3470: } Edouard@3470: if(value < this.ymin){ Edouard@3470: ymin_damaged = false; Edouard@3470: this.ymin = value; Edouard@3470: } Edouard@3470: } Edouard@3470: Edouard@3470: // recompute X range based on curent time ad buffer depth Edouard@3470: // TODO: get PLC time instead of browser time Edouard@3470: const d = new Date(); Edouard@3470: let time = d.getTime(); Edouard@3470: Edouard@3470: // FIXME: this becomes wrong when graph is not visible and updated all the time Edouard@3470: [this.xmin, this.xmax] = [time - data_length*1000/this.frequency, time]; Edouard@3470: let Xlength = this.xmax - this.xmin; Edouard@3470: Edouard@3470: Edouard@3470: // recompute curves "d" attribute Edouard@3470: // FIXME: use SVG getPathData and setPathData when available. Edouard@3470: // https://svgwg.org/specs/paths/#InterfaceSVGPathData Edouard@3470: // https://github.com/jarek-foksa/path-data-polyfill Edouard@3470: Edouard@3470: let [base_point, xvect, yvect] = this.reference.getBaseRef(); Edouard@3470: this.curves_d_attr = Edouard@3470: zip(this.curves_data, this.curves).map(function([data,curve]){ Edouard@3470: let new_d = data.map(function([y, i]){ Edouard@3470: // compute curve point from data, ranges, and base_ref Edouard@3470: let x = Xmin + i * Xlength / data_length; Edouard@3470: let xv = vectorscale(xvect, (x - Xmin) / Xlength); Edouard@3470: let yv = vectorscale(yvect, (y - Ymin) / Ylength); Edouard@3470: let px = base_point.x + xv.x + yv.x; Edouard@3470: let py = base_point.y + xv.y + yv.y; Edouard@3470: if(!this.fixed_y_range){ Edouard@3470: if(ymin_damaged && y > this.ymin) this.ymin = y; Edouard@3470: if(xmin_damaged && x > this.xmin) this.xmin = x; Edouard@3470: } Edouard@3470: Edouard@3470: return " " + px + "," + py; Edouard@3470: }); Edouard@3470: Edouard@3470: new_d.unshift("M "); Edouard@3470: new_d.push(" z"); Edouard@3470: Edouard@3470: return new_d.join(''); Edouard@3470: } Edouard@3470: Edouard@3470: // computed curves "d" attr is applied to svg curve during animate(); Edouard@3470: Edouard@3470: this.request_animate(); Edouard@3470: } Edouard@3470: Edouard@3470: animate(){ Edouard@3470: Edouard@3470: // move marks and update labels Edouard@3470: this.reference.applyRanges([this.XRange, this.YRange]); Edouard@3470: Edouard@3470: // apply computed curves "d" attributes Edouard@3470: for(let [curve, d_attr] of zip(this.curves, this.curves_d_attr)){ Edouard@3470: curve.setAttribute("d", d_attr); Edouard@3470: } Edouard@3470: } Edouard@3470: Edouard@3470: || Edouard@3470: } Edouard@3470: Edouard@3470: widget_defs("XYGraph") { Edouard@3474: labels("/x_interval_minor_mark /x_axis_line /x_interval_major_mark /x_axis_label") Edouard@3474: labels("/y_interval_minor_mark /y_axis_line /y_interval_major_mark /y_axis_label") Edouard@3470: Edouard@3470: | init_specific() { Edouard@3470: Edouard@3470: // collect all curve_n labelled children Edouard@3470: foreach "$hmi_element/*[regexp:test(@inkscape:label,'^curve_[0-9]+$')]" { Edouard@3470: const "label","@inkscape:label"; Edouard@3470: const "id","@id"; Edouard@3470: Edouard@3470: // detect non-unique names Edouard@3470: if "$hmi_element/*[not($id = @id) and @inkscape:label=$label]"{ Edouard@3470: error > XYGraph id="«$id»", label="«$label»" : elements with data_n label must be unique. Edouard@3470: } Edouard@3470: | this.curves[«substring(@inkscape:label, 7)»] = id("«@id»"); /* «@inkscape:label» */ Edouard@3470: } Edouard@3470: Edouard@3470: | } Edouard@3470: Edouard@3470: } Edouard@3470: Edouard@3470: emit "declarations:XYGraph" Edouard@3470: || Edouard@3470: function lineFromPath(path_elt) { Edouard@3470: let start = path_elt.getPointAtLength(0); Edouard@3470: let end = path_elt.getPointAtLength(path_elt.getTotalLength()); Edouard@3470: return [start, new DOMPoint(end.x - start.x , end.y - start.y)]; Edouard@3470: }; Edouard@3470: Edouard@3470: function vector(p1, p2) { Edouard@3470: return new DOMPoint(p2.x - p1.x , p2.y - p1.y); Edouard@3470: }; Edouard@3470: Edouard@3470: function vectorscale(p1, p2) { Edouard@3470: return new DOMPoint(p2 * p1.x , p2 * p1.y); Edouard@3470: }; Edouard@3470: Edouard@3470: function move_elements_to_group(elements) { Edouard@3470: let newgroup = document.createElementNS(xmlns,"g"); Edouard@3470: for(let element of elements){ Edouard@3470: element.parentElement().removeChild(element); Edouard@3470: newgroup.appendChild(element); Edouard@3470: } Edouard@3470: return newgroup; Edouard@3470: } Edouard@3470: function getLinesIntesection(l1, l2) { Edouard@3470: /* Edouard@3470: Compute intersection of two lines Edouard@3470: ================================= Edouard@3470: Edouard@3470: ^ l2vect Edouard@3470: / Edouard@3470: / Edouard@3470: / Edouard@3470: l1start ----------X--------------> l1vect Edouard@3470: / intersection Edouard@3470: / Edouard@3470: / Edouard@3470: l2start Edouard@3470: Edouard@3470: intersection = l1start + l1vect * a Edouard@3470: intersection = l2start + l2vect * b Edouard@3470: ==> solve : "l1start + l1vect * a = l2start + l2vect * b" to find a and b and then intersection Edouard@3470: Edouard@3470: (1) l1start.x + l1vect.x * a = l2start.x + l2vect.x * b Edouard@3470: (2) l1start.y + l1vect.y * a = l2start.y + l2vect.y * b Edouard@3470: Edouard@3470: // express a Edouard@3470: (1) a = (l2start.x + l2vect.x * b) / (l1start.x + l1vect.x) Edouard@3470: Edouard@3470: // substitute a to have only b Edouard@3470: (1+2) l1start.y + l1vect.y * (l2start.x + l2vect.x * b) / (l1start.x + l1vect.x) = l2start.y + l2vect.y * b Edouard@3470: Edouard@3470: // expand to isolate b Edouard@3470: (2) l1start.y + (l1vect.y * l2start.x) / (l1start.x + l1vect.x) + (l1vect.y * l2vect.x * b) / (l1start.x + l1vect.x) = l2start.y + l2vect.y * b Edouard@3470: (2) l1start.y + (l1vect.y * l2start.x) / (l1start.x + l1vect.x) - l2start.y = l2vect.y * b - (l1vect.y * l2vect.x * b) / (l1start.x + l1vect.x) Edouard@3470: Edouard@3470: // factorize b Edouard@3470: (2) l1start.y + (l1vect.y * l2start.x) / (l1start.x + l1vect.x) - l2start.y = b * (l2vect.y - (l1vect.y * l2vect.x ) / (l1start.x + l1vect.x)) Edouard@3470: Edouard@3470: // extract b Edouard@3470: (2) b = (l1start.y + (l1vect.y * l2start.x) / (l1start.x + l1vect.x) - l2start.y) / ((l2vect.y - (l1vect.y * l2vect.x ) / (l1start.x + l1vect.x))) Edouard@3470: */ Edouard@3470: Edouard@3470: let [l1start, l1vect] = l1; Edouard@3474: let [l2start, l2vect] = l2; Edouard@3470: Edouard@3470: let b = (l1start.y + (l1vect.y * l2start.x) / (l1start.x + l1vect.x) - l2start.y) / ((l2vect.y - (l1vect.y * l2vect.x ) / (l1start.x + l1vect.x))); Edouard@3470: Edouard@3470: return new DOMPoint(l2start.x + l2vect.x * b, l2start.y + l2vect.y * b); Edouard@3470: }; Edouard@3470: Edouard@3470: Edouard@3470: // From https://stackoverflow.com/a/48293566 Edouard@3470: function *zip (...iterables){ Edouard@3470: let iterators = iterables.map(i => i[Symbol.iterator]() ) Edouard@3470: while (true) { Edouard@3470: let results = iterators.map(iter => iter.next() ) Edouard@3470: if (results.some(res => res.done) ) return Edouard@3470: else yield results.map(res => res.value ) Edouard@3470: } Edouard@3470: } Edouard@3470: Edouard@3470: class ReferenceFrame { Edouard@3470: constructor( Edouard@3470: // [[Xminor,Xmajor], [Yminor,Ymajor]] Edouard@3470: marks, Edouard@3470: // [Xlabel, Ylabel] Edouard@3470: labels, Edouard@3470: // [Xline, Yline] Edouard@3470: lines, Edouard@3470: // [Xformat, Yformat] printf-like formating strings Edouard@3470: formats Edouard@3470: ){ Edouard@3470: this.axes = zip(labels,marks,lines,formats).map((...args) => new Axis(...args)); Edouard@3470: Edouard@3470: let [lx,ly] = this.axes.map(axis => axis.line); Edouard@3470: let [[xstart, xvect], [ystart, yvect]] = [lx,ly]; Edouard@3470: let base_point = this.getBasePoint(); Edouard@3470: Edouard@3470: // setup clipping for curves Edouard@3470: this.clipPathPathDattr = Edouard@3470: "m " + base_point.x + "," + base_point.y + " " Edouard@3470: + xvect.x + "," + xvect.y + " " Edouard@3470: + yvect.x + "," + yvect.y + " " Edouard@3470: + -xvect.x + "," + -xvect.y + " " Edouard@3470: + -yvect.x + "," + -yvect.y + " z"; Edouard@3470: Edouard@3470: this.base_ref = [base_point, xvect, yvect]; Edouard@3470: Edouard@3470: for(let axis of this.axes){ Edouard@3470: axis.setBasePoint(base_point); Edouard@3470: } Edouard@3470: } Edouard@3470: Edouard@3470: getBaseRef(){ Edouard@3470: return this.base_ref; Edouard@3470: } Edouard@3470: Edouard@3470: getClipPathPathDattr(){ Edouard@3470: return this.clipPathPathDattr; Edouard@3470: } Edouard@3470: Edouard@3470: applyRanges(ranges){ Edouard@3470: for(let [range,axis] of zip(ranges,this.axes)){ Edouard@3470: axis.applyRange(range); Edouard@3470: } Edouard@3470: } Edouard@3470: Edouard@3470: getBasePoint() { Edouard@3470: let [[xstart, xvect], [ystart, yvect]] = this.axes.map(axis => axis.line); Edouard@3470: Edouard@3470: /* Edouard@3470: Compute graph clipping region base point Edouard@3470: ======================================== Edouard@3470: Edouard@3470: Clipping region is a parallelogram containing axes lines, Edouard@3470: and whose sides are parallel to axes line respectively. Edouard@3470: Given axes lines are not starting at the same point, hereafter is Edouard@3470: calculus of parallelogram base point. Edouard@3470: Edouard@3470: ^ given Y axis Edouard@3470: / / Edouard@3470: / / Edouard@3470: / / Edouard@3470: xstart /---------/--------------> given X axis Edouard@3470: / / Edouard@3470: / / Edouard@3470: /---------/-------------- Edouard@3470: base_point ystart Edouard@3470: Edouard@3470: base_point = xstart + yvect * a Edouard@3470: base_point = ystart + xvect * b Edouard@3470: ==> solve : "xstart + yvect * a = ystart + xvect * b" to find a and b and then base_point Edouard@3470: Edouard@3470: (1) xstart.x + yvect.x * a = ystart.x + xvect.x * b Edouard@3470: (2) xstart.y + yvect.y * a = ystart.y + xvect.y * b Edouard@3470: Edouard@3470: // express a Edouard@3470: (1) a = (ystart.x + xvect.x * b) / (xstart.x + yvect.x) Edouard@3470: Edouard@3470: // substitute a to have only b Edouard@3470: (1+2) xstart.y + yvect.y * (ystart.x + xvect.x * b) / (xstart.x + yvect.x) = ystart.y + xvect.y * b Edouard@3470: Edouard@3470: // expand to isolate b Edouard@3470: (2) xstart.y + (yvect.y * ystart.x) / (xstart.x + yvect.x) + (yvect.y * xvect.x * b) / (xstart.x + yvect.x) = ystart.y + xvect.y * b Edouard@3470: (2) xstart.y + (yvect.y * ystart.x) / (xstart.x + yvect.x) - ystart.y = xvect.y * b - (yvect.y * xvect.x * b) / (xstart.x + yvect.x) Edouard@3470: Edouard@3470: // factorize b Edouard@3470: (2) xstart.y + (yvect.y * ystart.x) / (xstart.x + yvect.x) - ystart.y = b * (xvect.y - (yvect.y * xvect.x ) / (xstart.x + yvect.x)) Edouard@3470: Edouard@3470: // extract b Edouard@3470: (2) b = (xstart.y + (yvect.y * ystart.x) / (xstart.x + yvect.x) - ystart.y) / ((xvect.y - (yvect.y * xvect.x ) / (xstart.x + yvect.x))) Edouard@3470: */ Edouard@3470: Edouard@3470: let b = (xstart.y + (yvect.y * ystart.x) / (xstart.x + yvect.x) - ystart.y) / ((xvect.y - (yvect.y * xvect.x ) / (xstart.x + yvect.x))); Edouard@3470: let base_point = new DOMPoint(ystart.x + xvect.x * b, ystart.y + xvect.y * b); Edouard@3470: Edouard@3470: // // compute given origin Edouard@3470: // // from drawing : given_origin = xstart - xvect * b Edouard@3470: // let given_origin = new DOMPoint(xstart.x - xvect.x * b, xstart.y - xvect.y * b); Edouard@3470: Edouard@3470: return base_point; Edouard@3470: Edouard@3470: } Edouard@3470: Edouard@3470: } Edouard@3470: Edouard@3470: class Axis { Edouard@3470: constructor(label, marks, line, format){ Edouard@3470: this.lineElement = line; Edouard@3470: this.line = lineFromPath(line); Edouard@3470: this.format = format; Edouard@3470: Edouard@3470: this.label = label; Edouard@3470: this.marks = marks; Edouard@3470: Edouard@3470: Edouard@3470: // add transforms for elements sliding along the axis line Edouard@3470: for(let [elementname,element] of zip(["minor", "major", "label"],[...marks,label])){ Edouard@3470: for(let name of ["base","slide"]){ Edouard@3470: let transform = svg_root.createSVGTransform(); Edouard@3470: element.transform.baseVal.appendItem(transform); Edouard@3470: this[elementname+"_"+name+"_transform"]=transform; Edouard@3470: }; Edouard@3470: }; Edouard@3470: Edouard@3470: // group marks an labels together Edouard@3470: let parent = line.parentElement() Edouard@3470: marks_group = move_elements_to_group(marks); Edouard@3470: marks_and_label_group = move_elements_to_group([marks_group_use, label]); Edouard@3470: group = move_elements_to_group([marks_and_label_group,line]); Edouard@3470: parent.appendChild(group); Edouard@3470: Edouard@3470: // Add transforms to group Edouard@3470: for(let name of ["base","origin"]){ Edouard@3470: let transform = svg_root.createSVGTransform(); Edouard@3470: group.transform.baseVal.appendItem(transform); Edouard@3470: this[name+"_transform"]=transform; Edouard@3470: }; Edouard@3470: Edouard@3470: this.group = group; Edouard@3470: this.marks_group = marks_group; Edouard@3470: this.marks_and_label_group = marks_and_label_group; Edouard@3470: Edouard@3470: this.mlg_clones = []; Edouard@3470: this.last_mark_count = 0; Edouard@3470: } Edouard@3470: Edouard@3470: setBasePoint(base_point){ Edouard@3470: // move Axis to base point Edouard@3470: let [start, _vect] = this.lineElement; Edouard@3470: let v = vector(start, base_point); Edouard@3470: this.base_transform.setTranslate(v.x, v.y); Edouard@3470: Edouard@3470: // Move marks and label to base point. Edouard@3470: // _|_______ _|________ Edouard@3470: // | ' | ==> ' Edouard@3470: // | 0 0 Edouard@3470: // | | Edouard@3470: Edouard@3470: for(let [markname,mark] of zip(["minor", "major"],this.marks)){ Edouard@3470: let transform = this[markname+"_base_transform"]; Edouard@3470: let pos = vector( Edouard@3470: // Marks are expected to be paths Edouard@3470: // paths are expected to be lines Edouard@3470: // intersection with axis line is taken Edouard@3470: // as reference for mark position Edouard@3470: base_point, getLinesIntesection( Edouard@3470: this.line, lineFromPath(mark))); Edouard@3470: this[markname+"_base_transform"].setTranslate(-pos.x, -pos.x); Edouard@3470: if(markname == "major"){ // label follow major mark Edouard@3470: this.label_base_transform.setTranslate(-pos.x, -pos.x); Edouard@3470: } Edouard@3470: } Edouard@3470: } Edouard@3470: Edouard@3470: applyOriginAndUnitVector(offset, unit_vect){ Edouard@3470: // offset is a representing position of an Edouard@3470: // axis along the opposit axis line, expressed in major marks units Edouard@3470: // unit_vect is the translation in between to major marks Edouard@3470: Edouard@3470: // ^ Edouard@3470: // | unit_vect Edouard@3470: // |<---> Edouard@3470: // _________|__________> Edouard@3470: // ^ | ' | ' | ' Edouard@3470: // |yoffset | 1 Edouard@3470: // | | Edouard@3470: // v xoffset| Edouard@3470: // X<------>| Edouard@3470: // base_point Edouard@3470: Edouard@3470: // move major marks and label to first positive mark position Edouard@3470: let v = vectorscale(unit_vect, offset+1); Edouard@3470: this.label_slide_transform.setTranslate(v.x, v.x); Edouard@3470: this.major_slide_transform.setTranslate(v.x, v.x); Edouard@3470: // move minor mark to first half positive mark position Edouard@3470: let h = vectorscale(unit_vect, offset+0.5); Edouard@3470: this.minor_slide_transform.setTranslate(h.x, h.x); Edouard@3470: } Edouard@3470: Edouard@3470: applyRange(min, max){ Edouard@3470: let range = max - min; Edouard@3470: Edouard@3470: // compute how many units for a mark Edouard@3470: // Edouard@3470: // - Units are expected to be an order of magnitude smaller than range, Edouard@3470: // so that marks are not too dense and also not too sparse. Edouard@3470: // Order of magnitude of range is log10(range) Edouard@3470: // Edouard@3470: // - Units are necessarily power of ten, otherwise it is complicated to Edouard@3470: // fill the text in labels... Edouard@3470: // Unit is pow(10, integer_number ) Edouard@3470: // Edouard@3470: // - To transform order of magnitude to an integer, floor() is used. Edouard@3470: // This results in a count of mark fluctuating in between 10 and 100. Edouard@3470: // Edouard@3470: // - To spare resources result is better in between 5 and 50, Edouard@3470: // and log10(5) is substracted to order of magnitude to obtain this Edouard@3470: // log10(5) ~= 0.69897 Edouard@3470: Edouard@3470: Edouard@3470: let unit = Math.pow(10, Math.floor(Math.log10(range)-0.69897)); Edouard@3470: Edouard@3470: // TODO: for time values (ms), units may be : Edouard@3470: // 1 -> ms Edouard@3470: // 10 -> s/100 Edouard@3470: // 100 -> s/10 Edouard@3470: // 1000 -> s Edouard@3470: // 60000 -> min Edouard@3470: // 3600000 -> hour Edouard@3470: // ... Edouard@3470: // Edouard@3470: Edouard@3470: // Compute position of origin along axis [0...range] Edouard@3470: Edouard@3470: // min < 0, max > 0, offset = -min Edouard@3470: // _____________|________________ Edouard@3470: // ... -3 -2 -1 |0 1 2 3 4 ... Edouard@3470: // <--offset---> ^ Edouard@3470: // |_original Edouard@3470: Edouard@3470: // min > 0, max > 0, offset = 0 Edouard@3470: // |________________ Edouard@3470: // |6 7 8 9 10... Edouard@3470: // ^ Edouard@3470: // |_original Edouard@3470: Edouard@3470: // min < 0, max < 0, offset = max-min (range) Edouard@3470: // _____________|_ Edouard@3470: // ... -5 -4 -3 |-2 Edouard@3470: // <--offset---> ^ Edouard@3470: // |_original Edouard@3470: Edouard@3470: let offset = (max>=0 && min>=0) ? 0 : ( Edouard@3470: (max<0 && min<0) ? range : -min); Edouard@3470: Edouard@3470: // compute unit vector Edouard@3470: let [_start, vect] = this.line; Edouard@3470: let unit_vect = vectorscale(vect, unit/range); Edouard@3470: let [umin, umax, uoffset] = [min,max,offset].map(val => Math.round(val/unit)); Edouard@3470: let mark_count = umax-umin; Edouard@3470: Edouard@3470: // apply unit vector to marks and label Edouard@3470: this.label_and_marks.applyOriginAndUnitVector(offset, unit_vect); Edouard@3470: Edouard@3470: // duplicate marks and labels as needed Edouard@3470: let current_mark_count = this.mlg_clones.length; Edouard@3470: for(let i = current_mark_count; i <= mark_count; i++){ Edouard@3470: // cloneNode() label and add a svg:use of marks in a new group Edouard@3470: let newgroup = document.createElementNS(xmlns,"g"); Edouard@3470: let transform = svg_root.createSVGTransform(); Edouard@3470: let newlabel = cloneNode(this.label); Edouard@3470: let newuse = document.createElementNS(xmlns,"use"); Edouard@3470: let newuseAttr = document.createAttribute("xlink:href"); Edouard@3470: newuseAttr.value = "#"+this.marks_group.id; Edouard@3470: newuse.setAttributeNode(newuseAttr.value); Edouard@3470: newgroup.transform.baseVal.appendItem(transform); Edouard@3470: newgroup.appendChild(newlabel); Edouard@3470: newgroup.appendChild(newuse); Edouard@3470: this.mlg_clones.push([tranform,newgroup]); Edouard@3470: } Edouard@3470: Edouard@3470: // move marks and labels, set labels Edouard@3470: for(let u = 0; u <= mark_count; u++){ Edouard@3470: let i = 0; Edouard@3470: let val = (umin + u) * unit; Edouard@3470: let vec = vectorscale(unit_vect, offset + val); Edouard@3470: let text = this.format ? sprintf(this.format, val) : val.toString(); Edouard@3470: if(u == uoffset){ Edouard@3470: // apply offset to original marks and label groups Edouard@3470: this.origin_transform.setTranslate(vec.x, vec.x); Edouard@3470: Edouard@3470: // update original label text Edouard@3470: this.label_and_mark.label.textContent = text; Edouard@3470: } else { Edouard@3470: let [transform,element] = this.mlg_clones[i++]; Edouard@3470: Edouard@3470: // apply unit vector*N to marks and label groups Edouard@3470: transform.setTranslate(vec.x, vec.x); Edouard@3470: Edouard@3470: // update label text Edouard@3470: element.getElementsByTagName("tspan")[0].textContent = text; Edouard@3470: Edouard@3470: // Attach to group if not already Edouard@3470: if(i >= this.last_mark_count){ Edouard@3470: this.group.appendChild(element); Edouard@3470: } Edouard@3470: } Edouard@3470: } Edouard@3470: Edouard@3470: // dettach marks and label from group if not anymore visible Edouard@3470: for(let i = current_mark_count; i < this.last_mark_count; i++){ Edouard@3470: let [transform,element] = this.mlg_clones[i]; Edouard@3470: this.group.removeChild(element); Edouard@3470: } Edouard@3470: Edouard@3470: this.last_mark_count = current_mark_count; Edouard@3470: } Edouard@3470: } Edouard@3470: ||