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@3505: arg name="xrange" accepts="int,time" > X axis range expressed either in samples or duration. 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: } Edouard@3470: Edouard@3470: widget_class("XYGraph") { Edouard@3470: || Edouard@3470: frequency = 1; Edouard@3470: init() { Edouard@3505: let x_duration_s; Edouard@3505: [x_duration_s, Edouard@3470: this.x_format, this.y_format] = this.args; Edouard@3470: Edouard@3505: let timeunit = x_duration_s.slice(-1); Edouard@3505: let factor = { Edouard@3505: "s":1, Edouard@3505: "m":60, Edouard@3505: "h":3600, Edouard@3505: "d":86400}[timeunit]; Edouard@3505: if(factor == undefined){ Edouard@3505: this.max_data_length = Number(x_duration_s); Edouard@3505: this.x_duration = undefined; Edouard@3505: }else{ Edouard@3505: let duration = factor*Number(x_duration_s.slice(0,-1)); Edouard@3505: this.max_data_length = undefined; Edouard@3505: this.x_duration = duration*1000; Edouard@3505: } Edouard@3505: Edouard@3505: 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@3484: let y_min = Infinity, y_max = -Infinity; Edouard@3470: Edouard@3470: // Compute visible Y range by merging fixed curves Y ranges Edouard@3691: for(let varopts of this.variables_options){ Edouard@3691: let minmax = varopts.minmax Edouard@3691: if(minmax){ Edouard@3691: let [min,max] = minmax; Edouard@3691: if(min < y_min) Edouard@3691: y_min = min; Edouard@3691: if(max > y_max) Edouard@3691: y_max = max; Edouard@3691: } Edouard@3470: } Edouard@3470: Edouard@3484: if(y_min !== Infinity && y_max !== -Infinity){ Edouard@3691: this.fixed_y_range = true; Edouard@3470: } else { Edouard@3691: 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@3484: [[this.x_interval_minor_mark_elt, this.x_interval_major_mark_elt], Edouard@3484: [this.y_interval_minor_mark_elt, this.y_interval_major_mark_elt]], Edouard@3484: [this.x_axis_label_elt, this.y_axis_label_elt], Edouard@3484: [this.x_axis_line_elt, this.y_axis_line_elt], Edouard@3470: [this.x_format, this.y_format]); Edouard@3470: Edouard@3490: let max_stroke_width = 0; Edouard@3490: for(let curve of this.curves){ Edouard@3490: if(curve.style.strokeWidth > max_stroke_width){ Edouard@3490: max_stroke_width = curve.style.strokeWidth; Edouard@3490: } Edouard@3490: } Edouard@3490: Edouard@3490: this.Margins=this.reference.getLengths().map(length => max_stroke_width/length); Edouard@3490: Edouard@3470: // create path and attach it to widget Edouard@3484: let clipPath = document.createElementNS(xmlns,"clipPath"); Edouard@3484: let clipPathPath = document.createElementNS(xmlns,"path"); Edouard@3484: let clipPathPathDattr = document.createAttribute("d"); Edouard@3470: clipPathPathDattr.value = this.reference.getClipPathPathDattr(); Edouard@3470: clipPathPath.setAttributeNode(clipPathPathDattr); Edouard@3470: clipPath.appendChild(clipPathPath); Edouard@3490: clipPath.id = randomId(); 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@3509: this.curves_data = []; Edouard@3470: } Edouard@3470: Edouard@3470: dispatch(value,oldval, index) { Edouard@3488: // TODO: get PLC time instead of browser time Edouard@3488: let time = Date.now(); Edouard@3488: 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@3509: if(this.curves_data[index] === undefined){ Edouard@3509: this.curves_data[index] = []; Edouard@3509: } Edouard@3488: this.curves_data[index].push([time, 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@3505: if(this.max_data_length == undefined){ Edouard@3505: let peremption = time - this.x_duration; Edouard@3505: let oldest = this.curves_data[index][0][0] Edouard@3505: this.xmin = peremption; Edouard@3505: if(oldest < peremption){ Edouard@3505: // remove first item Edouard@3505: overflow = this.curves_data[index].shift()[1]; Edouard@3505: data_length = data_length - 1; Edouard@3505: } Edouard@3488: } else { Edouard@3505: if(data_length > this.max_data_length){ Edouard@3505: // remove first item Edouard@3505: [this.xmin, overflow] = this.curves_data[index].shift(); Edouard@3505: data_length = data_length - 1; Edouard@3505: } else { Edouard@3505: if(this.xmin == undefined){ Edouard@3505: this.xmin = time; Edouard@3505: } Edouard@3488: } Edouard@3488: } Edouard@3488: Edouard@3488: this.xmax = time; Edouard@3490: let Xrange = this.xmax - this.xmin; 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@3490: let Yrange = this.ymax - this.ymin; Edouard@3490: Edouard@3505: // apply margin by moving min and max to enlarge range Edouard@3490: let [xMargin,yMargin] = zip(this.Margins, [Xrange, Yrange]).map(([m,l]) => m*l); Edouard@3490: [[this.dxmin, this.dxmax],[this.dymin,this.dymax]] = Edouard@3490: [[this.xmin-xMargin, this.xmax+xMargin], Edouard@3490: [this.ymin-yMargin, this.ymax+yMargin]]; Edouard@3490: Xrange += 2*xMargin; Edouard@3490: Yrange += 2*yMargin; 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@3484: Edouard@3470: let [base_point, xvect, yvect] = this.reference.getBaseRef(); Edouard@3484: this.curves_d_attr = Edouard@3484: zip(this.curves_data, this.curves).map(([data,curve]) => { Edouard@3488: let new_d = data.map(([x,y], i) => { Edouard@3484: // compute curve point from data, ranges, and base_ref Edouard@3490: let xv = vectorscale(xvect, (x - this.dxmin) / Xrange); Edouard@3490: let yv = vectorscale(yvect, (y - this.dymin) / Yrange); Edouard@3484: let px = base_point.x + xv.x + yv.x; Edouard@3484: let py = base_point.y + xv.y + yv.y; Edouard@3484: if(!this.fixed_y_range){ Edouard@3505: // update min and max from curve data if needed Edouard@3484: if(ymin_damaged && y < this.ymin) this.ymin = y; Edouard@3484: if(ymax_damaged && y > this.ymax) this.ymax = y; Edouard@3484: } Edouard@3484: Edouard@3484: return " " + px + "," + py; Edouard@3484: }); Edouard@3484: Edouard@3484: new_d.unshift("M "); Edouard@3484: Edouard@3484: 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@3484: // move elements only if enough data Edouard@3484: if(this.curves_data.some(data => data.length > 1)){ Edouard@3484: Edouard@3484: // move marks and update labels Edouard@3490: this.reference.applyRanges([[this.dxmin, this.dxmax], Edouard@3490: [this.dymin, this.dymax]]); Edouard@3484: Edouard@3484: // apply computed curves "d" attributes Edouard@3484: for(let [curve, d_attr] of zip(this.curves, this.curves_d_attr)){ Edouard@3484: curve.setAttribute("d", d_attr); Edouard@3484: } Edouard@3470: } Edouard@3470: } Edouard@3470: Edouard@3470: || Edouard@3470: } Edouard@3470: Edouard@3509: def "func:check_curves_label_consistency" { Edouard@3509: param "curve_elts"; Edouard@3509: param "number_to_check"; Edouard@3509: const "res" choose { Edouard@3509: when "$curve_elts[@inkscape:label = concat('curve_', string($number_to_check))]"{ Edouard@3509: if "$number_to_check > 0"{ Edouard@3509: value "func:check_curves_label_consistency($curve_elts, $number_to_check - 1)"; Edouard@3509: } Edouard@3509: } Edouard@3509: otherwise { Edouard@3509: value "concat('missing curve_', string($number_to_check))"; Edouard@3509: } Edouard@3509: } Edouard@3509: result "$res"; Edouard@3509: } Edouard@3509: Edouard@3470: widget_defs("XYGraph") { Edouard@3484: labels("/x_interval_minor_mark /x_axis_line /x_interval_major_mark /x_axis_label"); Edouard@3484: 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@3509: const "curves","$hmi_element/*[regexp:test(@inkscape:label,'^curve_[0-9]+$')]"; Edouard@3509: const "curves_error", "func:check_curves_label_consistency($curves,count($curves)-1)"; Edouard@3509: if "string-length($curves_error)" Edouard@3509: error > XYGraph id="«@id»", label="«@inkscape:label»" : «$curves_error» Edouard@3509: foreach "$curves" { Edouard@3470: const "label","@inkscape:label"; Edouard@3509: const "_id","@id"; Edouard@3509: const "curve_num", "substring(@inkscape:label, 7)"; Edouard@3509: | this.curves[«$curve_num»] = 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@3490: function vectorLength(p1) { Edouard@3490: return Math.sqrt(p1.x*p1.x + p1.y*p1.y); Edouard@3490: }; Edouard@3490: Edouard@3490: function randomId(){ Edouard@3490: return Date.now().toString(36) + Math.random().toString(36).substr(2); Edouard@3490: } Edouard@3490: Edouard@3470: function move_elements_to_group(elements) { Edouard@3470: let newgroup = document.createElementNS(xmlns,"g"); Edouard@3490: newgroup.id = randomId(); Edouard@3488: Edouard@3470: for(let element of elements){ Edouard@3484: let parent = element.parentElement; Edouard@3484: if(parent !== null) Edouard@3484: parent.removeChild(element); Edouard@3470: newgroup.appendChild(element); Edouard@3470: } Edouard@3470: return newgroup; Edouard@3470: } Edouard@3470: function getLinesIntesection(l1, l2) { Edouard@3484: let [l1start, l1vect] = l1; Edouard@3484: let [l2start, l2vect] = l2; Edouard@3484: Edouard@3488: 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@3488: */ Edouard@3488: let [x1, y1, x3, y3] = [l1start.x, l1start.y, l2start.x, l2start.y]; Edouard@3488: let [x2, y2, x4, y4] = [x1+l1vect.x, y1+l1vect.y, x3+l2vect.x, y3+l2vect.y]; Edouard@3488: Edouard@3488: // line intercept math by Paul Bourke http://paulbourke.net/geometry/pointlineplane/ Edouard@3488: // Determine the intersection point of two line segments Edouard@3488: // Return FALSE if the lines don't intersect Edouard@3488: Edouard@3488: // Check if none of the lines are of length 0 Edouard@3488: if ((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) { Edouard@3488: return false Edouard@3488: } Edouard@3488: Edouard@3488: denominator = ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)) Edouard@3488: Edouard@3488: // Lines are parallel Edouard@3488: if (denominator === 0) { Edouard@3488: return false Edouard@3488: } Edouard@3488: Edouard@3488: let ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator Edouard@3488: let ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denominator Edouard@3488: Edouard@3488: // Return a object with the x and y coordinates of the intersection Edouard@3488: let x = x1 + ua * (x2 - x1) Edouard@3488: let y = y1 + ua * (y2 - y1) Edouard@3488: Edouard@3488: return new DOMPoint(x,y); 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@3484: 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@3490: this.lengths = [xvect,yvect].map(v => vectorLength(v)); Edouard@3490: Edouard@3470: for(let axis of this.axes){ Edouard@3470: axis.setBasePoint(base_point); Edouard@3470: } Edouard@3470: } Edouard@3470: Edouard@3490: getLengths(){ Edouard@3490: return this.lengths; Edouard@3490: } Edouard@3490: 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@3488: let origin_moves = zip(ranges,this.axes).map(([range,axis]) => axis.applyRange(...range)); Edouard@3488: zip(origin_moves.reverse(),this.axes).forEach(([vect,axis]) => axis.moveOrigin(vect)); 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@3484: ^ given Y axis (yvect) Edouard@3470: / / Edouard@3470: / / Edouard@3470: / / Edouard@3484: xstart *---------*--------------> given X axis (xvect) Edouard@3484: / /origin Edouard@3470: / / Edouard@3484: *---------*-------------- Edouard@3470: base_point ystart Edouard@3470: Edouard@3470: */ Edouard@3470: Edouard@3484: let base_point = getLinesIntesection([xstart,yvect],[ystart,xvect]); 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@3490: element.transform.baseVal.insertItemBefore(transform,0); Edouard@3470: this[elementname+"_"+name+"_transform"]=transform; Edouard@3470: }; Edouard@3470: }; Edouard@3470: Edouard@3470: // group marks an labels together Edouard@3484: let parent = line.parentElement; Edouard@3488: this.marks_group = move_elements_to_group(marks); Edouard@3488: this.marks_and_label_group = move_elements_to_group([this.marks_group, label]); Edouard@3488: this.group = move_elements_to_group([this.marks_and_label_group,line]); Edouard@3488: parent.appendChild(this.group); Edouard@3470: Edouard@3484: // Add transforms to group Edouard@3470: for(let name of ["base","origin"]){ Edouard@3470: let transform = svg_root.createSVGTransform(); Edouard@3488: this.group.transform.baseVal.appendItem(transform); Edouard@3470: this[name+"_transform"]=transform; Edouard@3470: }; Edouard@3470: Edouard@3488: this.marks_and_label_group_transform = svg_root.createSVGTransform(); Edouard@3488: this.marks_and_label_group.transform.baseVal.appendItem(this.marks_and_label_group_transform); Edouard@3488: Edouard@3488: this.duplicates = []; Edouard@3490: this.last_duplicate_index = 0; Edouard@3470: } Edouard@3470: Edouard@3470: setBasePoint(base_point){ Edouard@3484: // move Axis to base point Edouard@3484: let [start, _vect] = this.line; Edouard@3470: let v = vector(start, base_point); Edouard@3470: this.base_transform.setTranslate(v.x, v.y); Edouard@3470: Edouard@3484: // Move marks and label to base point. Edouard@3484: // _|_______ _|________ Edouard@3484: // | ' | ==> ' Edouard@3484: // | 0 0 Edouard@3484: // | | Edouard@3470: Edouard@3470: for(let [markname,mark] of zip(["minor", "major"],this.marks)){ 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@3488: getLinesIntesection( Edouard@3488: this.line, lineFromPath(mark)),base_point); Edouard@3488: this[markname+"_base_transform"].setTranslate(pos.x - v.x, pos.y - v.y); Edouard@3470: if(markname == "major"){ // label follow major mark Edouard@3488: this.label_base_transform.setTranslate(pos.x - v.x, pos.y - v.y); Edouard@3488: } Edouard@3488: } Edouard@3488: } Edouard@3488: Edouard@3488: moveOrigin(vect){ Edouard@3488: this.origin_transform.setTranslate(vect.x, vect.y); Edouard@3488: } 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@3490: // - To spare resources result is better in between 3 and 30, Edouard@3490: // and log10(3) is substracted to order of magnitude to obtain this Edouard@3490: let unit = Math.pow(10, Math.floor(Math.log10(range)-Math.log10(3))); 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@3488: let unit_vect = vectorscale(vect, 1/range); Edouard@3488: let [mark_min, mark_max, mark_offset] = [min,max,offset].map(val => Math.round(val/unit)); Edouard@3488: let mark_count = mark_max-mark_min; Edouard@3470: Edouard@3470: // apply unit vector to marks and label Edouard@3484: // offset is a representing position of an Edouard@3484: // axis along the opposit axis line, expressed in major marks units Edouard@3488: // unit_vect is unit vector Edouard@3484: Edouard@3484: // ^ Edouard@3484: // | unit_vect Edouard@3484: // |<---> Edouard@3484: // _________|__________> Edouard@3484: // ^ | ' | ' | ' Edouard@3484: // |yoffset | 1 Edouard@3484: // | | Edouard@3484: // v xoffset| Edouard@3484: // X<------>| Edouard@3484: // base_point Edouard@3484: Edouard@3484: // move major marks and label to first positive mark position Edouard@3490: // let v = vectorscale(unit_vect, unit); Edouard@3490: // this.label_slide_transform.setTranslate(v.x, v.y); Edouard@3490: // this.major_slide_transform.setTranslate(v.x, v.y); Edouard@3484: // move minor mark to first half positive mark position Edouard@3490: let v = vectorscale(unit_vect, unit/2); Edouard@3488: this.minor_slide_transform.setTranslate(v.x, v.y); Edouard@3470: Edouard@3470: // duplicate marks and labels as needed Edouard@3488: let current_mark_count = this.duplicates.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@3484: let newlabel = this.label.cloneNode(true); Edouard@3470: let newuse = document.createElementNS(xmlns,"use"); Edouard@3490: let newuseAttr = document.createAttribute("href"); Edouard@3470: newuseAttr.value = "#"+this.marks_group.id; Edouard@3484: newuse.setAttributeNode(newuseAttr); Edouard@3470: newgroup.transform.baseVal.appendItem(transform); Edouard@3470: newgroup.appendChild(newlabel); Edouard@3470: newgroup.appendChild(newuse); Edouard@3488: this.duplicates.push([transform,newgroup]); Edouard@3470: } Edouard@3470: Edouard@3470: // move marks and labels, set labels Edouard@3488: // Edouard@3488: // min > 0, max > 0, offset = 0 Edouard@3488: // ^ Edouard@3488: // |________> Edouard@3488: // '| | ' | Edouard@3488: // | 6 7 Edouard@3488: // X Edouard@3488: // base_point Edouard@3488: // Edouard@3488: // min < 0, max > 0, offset = -min Edouard@3488: // ^ Edouard@3488: // _________|__________> Edouard@3488: // ' | ' | ' | ' Edouard@3488: // -1 | 1 Edouard@3488: // offset | Edouard@3488: // X<------>| Edouard@3488: // base_point Edouard@3488: // Edouard@3488: // min < 0, max < 0, offset = range Edouard@3488: // ^ Edouard@3488: // ____________| Edouard@3488: // ' | ' | |' Edouard@3488: // -5 -4 | Edouard@3488: // offset | Edouard@3488: // X<--------->| Edouard@3488: // base_point Edouard@3488: Edouard@3490: let duplicate_index = 0; Edouard@3488: for(let mark_index = 0; mark_index <= mark_count; mark_index++){ Edouard@3488: let val = (mark_min + mark_index) * unit; Edouard@3490: let vec = vectorscale(unit_vect, val - min); Edouard@3470: let text = this.format ? sprintf(this.format, val) : val.toString(); Edouard@3488: if(mark_index == mark_offset){ Edouard@3470: // apply offset to original marks and label groups Edouard@3488: this.marks_and_label_group_transform.setTranslate(vec.x, vec.y); Edouard@3470: Edouard@3470: // update original label text Edouard@3484: this.label.getElementsByTagName("tspan")[0].textContent = text; Edouard@3470: } else { Edouard@3490: let [transform,element] = this.duplicates[duplicate_index++]; Edouard@3470: Edouard@3470: // apply unit vector*N to marks and label groups Edouard@3484: transform.setTranslate(vec.x, vec.y); 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@3490: if(element.parentElement == null){ Edouard@3470: this.group.appendChild(element); Edouard@3470: } Edouard@3470: } Edouard@3470: } Edouard@3470: Edouard@3490: let save_duplicate_index = duplicate_index; Edouard@3470: // dettach marks and label from group if not anymore visible Edouard@3490: for(;duplicate_index < this.last_duplicate_index; duplicate_index++){ Edouard@3490: let [transform,element] = this.duplicates[duplicate_index]; Edouard@3470: this.group.removeChild(element); Edouard@3470: } Edouard@3470: Edouard@3490: this.last_duplicate_index = save_duplicate_index; Edouard@3488: Edouard@3488: return vectorscale(unit_vect, offset); Edouard@3470: } Edouard@3470: } Edouard@3470: ||