svghmi/widget_xygraph.ysl2
changeset 3470 b36754171535
child 3474 3ba74350237d
equal deleted inserted replaced
3455:2716cd8e498d 3470:b36754171535
       
     1 // widget_xygraph.ysl2
       
     2 widget_desc("XYGraph") {
       
     3     longdesc
       
     4     ||
       
     5     XYGraph draws a cartesian trend graph re-using styles given for axis,
       
     6     grid/marks, legends and curves.
       
     7 
       
     8     Elements labeled "x_axis" and "y_axis" are svg:groups containg:
       
     9      - "axis_label" svg:text gives style an alignment for axis labels.
       
    10      - "interval_major_mark" and "interval_minor_mark" are svg elements to be
       
    11        duplicated along axis line to form intervals marks.
       
    12      - "axis_line"  svg:path is the axis line. Paths must be intersect and their
       
    13        bounding box is the chart wall.
       
    14 
       
    15     Elements labeled "curve_0", "curve_1", ... are paths whose styles are used
       
    16     to draw curves corresponding to data from variables passed as HMI tree paths.
       
    17     "curve_0" is mandatory. HMI variables outnumbering given curves are ignored.
       
    18 
       
    19     ||
       
    20 
       
    21     shortdesc > Cartesian trend graph showing values of given variables over time
       
    22 
       
    23     path name="value" count="1+" accepts="HMI_INT,HMI_REAL" > value
       
    24 
       
    25     arg name="size" accepts="int" > buffer size
       
    26     arg name="xformat" count="optional" accepts="string" > format string for X label
       
    27     arg name="yformat" count="optional" accepts="string" > format string for Y label
       
    28     arg name="ymin" count="optional" accepts="int,real" > minimum value foe Y axis
       
    29     arg name="ymax" count="optional" accepts="int,real" > maximum value for Y axis
       
    30 }
       
    31 
       
    32 widget_class("XYGraph") {
       
    33     ||
       
    34         frequency = 1;
       
    35         init() {
       
    36             [this.x_size,
       
    37              this.x_format, this.y_format] = this.args;
       
    38 
       
    39             // Min and Max given with paths are meant to describe visible range,
       
    40             // not to clip data.
       
    41             this.clip = false;
       
    42 
       
    43             let y_min = -Infinity, y_max = Infinity;
       
    44 
       
    45             // Compute visible Y range by merging fixed curves Y ranges
       
    46             for(let minmax of this.minmaxes){
       
    47                if(minmax){
       
    48                    let [min,max] = minmax;
       
    49                    if(min < y_min)
       
    50                        y_min = min;
       
    51                    if(max > y_max) 
       
    52                        y_max = max;
       
    53                }
       
    54             }
       
    55 
       
    56             if(y_min !== -Infinity && y_max !== Infinity){
       
    57                this.fixed_y_range = true;
       
    58             } else {
       
    59                this.fixed_y_range = false;
       
    60             }
       
    61 
       
    62             this.ymin = y_min;
       
    63             this.ymax = y_max;
       
    64 
       
    65             this.curves = [];
       
    66             this.init_specific();
       
    67 
       
    68             this.reference = new ReferenceFrame(
       
    69                 [[this.x_interval_minor_mark, this.x_interval_major_mark],
       
    70                  [this.y_interval_minor_mark, this.y_interval_major_mark]],
       
    71                 [this.y_axis_label, this.x_axis_label],
       
    72                 [this.x_axis_line, this.y_axis_line],
       
    73                 [this.x_format, this.y_format]);
       
    74 
       
    75             // create <clipPath> path and attach it to widget
       
    76             clipPath = document.createElementNS(xmlns,"clipPath");
       
    77             clipPathPath = document.createElementNS(xmlns,"path");
       
    78             clipPathPathDattr = document.createAttribute("d");
       
    79             clipPathPathDattr.value = this.reference.getClipPathPathDattr();
       
    80             clipPathPath.setAttributeNode(clipPathPathDattr);
       
    81             clipPath.appendChild(clipPathPath);
       
    82             this.element.appendChild(clipPath);
       
    83 
       
    84             // assign created clipPath to clip-path property of curves
       
    85             for(let curve of this.curves){
       
    86                 curve.setAttribute("clip-path", "url(#" + clipPath.id + ")");
       
    87             }
       
    88 
       
    89             this.curves_data = [];
       
    90             this.max_data_length = this.args[0];
       
    91         }
       
    92 
       
    93         dispatch(value,oldval, index) {
       
    94             // naive local buffer impl. 
       
    95             // data is updated only when graph is visible
       
    96             // TODO: replace with separate recording
       
    97 
       
    98             this.curves_data[index].push(value);
       
    99             let data_length = this.curves_data[index].length;
       
   100             let ymin_damaged = false;
       
   101             let ymax_damaged = false;
       
   102             let overflow;
       
   103 
       
   104             if(data_length > this.max_data_length){
       
   105                 // remove first item
       
   106                 overflow = this.curves_data[index].shift();
       
   107                 data_length = data_length - 1;
       
   108             }
       
   109 
       
   110             if(!this.fixed_y_range){
       
   111                 ymin_damaged = overflow <= this.ymin;
       
   112                 ymax_damaged = overflow >= this.ymax;
       
   113                 if(value > this.ymax){
       
   114                     ymax_damaged = false;
       
   115                     this.ymax = value;
       
   116                 }
       
   117                 if(value < this.ymin){
       
   118                     ymin_damaged = false;
       
   119                     this.ymin = value;
       
   120                 }
       
   121             }
       
   122 
       
   123             // recompute X range based on curent time ad buffer depth
       
   124             // TODO: get PLC time instead of browser time
       
   125             const d = new Date();
       
   126             let time = d.getTime();
       
   127 
       
   128             // FIXME: this becomes wrong when graph is not visible and updated all the time
       
   129             [this.xmin, this.xmax] = [time - data_length*1000/this.frequency, time];
       
   130             let Xlength = this.xmax - this.xmin;
       
   131 
       
   132 
       
   133             // recompute curves "d" attribute
       
   134             // FIXME: use SVG getPathData and setPathData when available.
       
   135             //        https://svgwg.org/specs/paths/#InterfaceSVGPathData
       
   136             //        https://github.com/jarek-foksa/path-data-polyfill
       
   137            
       
   138             let [base_point, xvect, yvect] = this.reference.getBaseRef();
       
   139             this.curves_d_attr = 
       
   140                 zip(this.curves_data, this.curves).map(function([data,curve]){
       
   141                 let new_d = data.map(function([y, i]){
       
   142                     // compute curve point from data, ranges, and base_ref
       
   143                     let x = Xmin + i * Xlength / data_length;
       
   144                     let xv = vectorscale(xvect, (x - Xmin) / Xlength);
       
   145                     let yv = vectorscale(yvect, (y - Ymin) / Ylength);
       
   146                     let px = base_point.x + xv.x + yv.x;
       
   147                     let py = base_point.y + xv.y + yv.y;
       
   148                     if(!this.fixed_y_range){
       
   149                         if(ymin_damaged && y > this.ymin) this.ymin = y;
       
   150                         if(xmin_damaged && x > this.xmin) this.xmin = x;
       
   151                     }
       
   152 
       
   153                     return " " + px + "," + py;
       
   154                 });
       
   155 
       
   156                 new_d.unshift("M ");
       
   157                 new_d.push(" z");
       
   158 
       
   159                 return new_d.join('');
       
   160             }
       
   161 
       
   162             // computed curves "d" attr is applied to svg curve during animate();
       
   163 
       
   164             this.request_animate();
       
   165         }
       
   166 
       
   167         animate(){
       
   168 
       
   169             // move marks and update labels
       
   170             this.reference.applyRanges([this.XRange, this.YRange]);
       
   171 
       
   172             // apply computed curves "d" attributes
       
   173             for(let [curve, d_attr] of zip(this.curves, this.curves_d_attr)){
       
   174                 curve.setAttribute("d", d_attr);
       
   175             }
       
   176         }
       
   177 
       
   178     ||
       
   179 }
       
   180 
       
   181 widget_defs("XYGraph") {
       
   182     labels("""
       
   183         /x_interval_minor_mark
       
   184         /x_axis_line
       
   185         /x_interval_major_mark
       
   186         /x_axis_label
       
   187         /y_interval_minor_mark
       
   188         /y_axis_line
       
   189         /y_interval_major_mark
       
   190         /y_axis_label""")
       
   191 
       
   192     |     init_specific() {
       
   193 
       
   194     // collect all curve_n labelled children
       
   195     foreach "$hmi_element/*[regexp:test(@inkscape:label,'^curve_[0-9]+$')]" {
       
   196         const "label","@inkscape:label";
       
   197         const "id","@id";
       
   198 
       
   199         // detect non-unique names
       
   200         if "$hmi_element/*[not($id = @id) and @inkscape:label=$label]"{
       
   201             error > XYGraph id="«$id»", label="«$label»" : elements with data_n label must be unique.
       
   202         }
       
   203     |         this.curves[«substring(@inkscape:label, 7)»] = id("«@id»"); /* «@inkscape:label» */
       
   204     }
       
   205 
       
   206     |     }
       
   207 
       
   208 }
       
   209 
       
   210 emit "declarations:XYGraph"
       
   211 ||
       
   212 function lineFromPath(path_elt) {
       
   213     let start = path_elt.getPointAtLength(0);
       
   214     let end = path_elt.getPointAtLength(path_elt.getTotalLength());
       
   215     return [start, new DOMPoint(end.x - start.x , end.y - start.y)];
       
   216 };
       
   217 
       
   218 function vector(p1, p2) {
       
   219     return new DOMPoint(p2.x - p1.x , p2.y - p1.y);
       
   220 };
       
   221 
       
   222 function vectorscale(p1, p2) {
       
   223     return new DOMPoint(p2 * p1.x , p2 * p1.y);
       
   224 };
       
   225 
       
   226 function move_elements_to_group(elements) {
       
   227     let newgroup = document.createElementNS(xmlns,"g");
       
   228     for(let element of elements){
       
   229         element.parentElement().removeChild(element);
       
   230         newgroup.appendChild(element);
       
   231     }
       
   232     return newgroup;
       
   233 }
       
   234 function getLinesIntesection(l1, l2) {
       
   235     /*
       
   236     Compute intersection of two lines
       
   237     =================================
       
   238 
       
   239                           ^ l2vect
       
   240                          /
       
   241                         /
       
   242                        /
       
   243     l1start ----------X--------------> l1vect
       
   244                      / intersection
       
   245                     /
       
   246                    /
       
   247                    l2start
       
   248 
       
   249     intersection = l1start + l1vect * a
       
   250     intersection = l2start + l2vect * b
       
   251     ==> solve : "l1start + l1vect * a = l2start + l2vect * b" to find a and b and then intersection
       
   252 
       
   253     (1)   l1start.x + l1vect.x * a = l2start.x + l2vect.x * b
       
   254     (2)   l1start.y + l1vect.y * a = l2start.y + l2vect.y * b
       
   255 
       
   256     // express a
       
   257     (1)   a = (l2start.x + l2vect.x * b) / (l1start.x + l1vect.x)
       
   258 
       
   259     // substitute a to have only b
       
   260     (1+2) l1start.y + l1vect.y * (l2start.x + l2vect.x * b) / (l1start.x + l1vect.x) = l2start.y + l2vect.y * b
       
   261 
       
   262     // expand to isolate b
       
   263     (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
       
   264     (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)
       
   265 
       
   266     // factorize b
       
   267     (2) l1start.y + (l1vect.y * l2start.x) / (l1start.x + l1vect.x) - l2start.y = b * (l2vect.y - (l1vect.y * l2vect.x ) / (l1start.x + l1vect.x))
       
   268 
       
   269     // extract b
       
   270     (2) b = (l1start.y + (l1vect.y * l2start.x) / (l1start.x + l1vect.x) - l2start.y) / ((l2vect.y - (l1vect.y * l2vect.x ) / (l1start.x + l1vect.x)))
       
   271     */
       
   272 
       
   273     let [l1start, l1vect] = l1;
       
   274     let [l1start, l2vect] = l2;
       
   275 
       
   276     let b = (l1start.y + (l1vect.y * l2start.x) / (l1start.x + l1vect.x) - l2start.y) / ((l2vect.y - (l1vect.y * l2vect.x ) / (l1start.x + l1vect.x)));
       
   277 
       
   278     return new DOMPoint(l2start.x + l2vect.x * b, l2start.y + l2vect.y * b);
       
   279 };
       
   280 
       
   281 
       
   282 // From https://stackoverflow.com/a/48293566
       
   283 function *zip (...iterables){
       
   284     let iterators = iterables.map(i => i[Symbol.iterator]() )
       
   285     while (true) {
       
   286         let results = iterators.map(iter => iter.next() )
       
   287         if (results.some(res => res.done) ) return
       
   288         else yield results.map(res => res.value )
       
   289     }
       
   290 }
       
   291 
       
   292 class ReferenceFrame {
       
   293     constructor(
       
   294         // [[Xminor,Xmajor], [Yminor,Ymajor]]
       
   295         marks,
       
   296         // [Xlabel, Ylabel]
       
   297         labels,
       
   298         // [Xline, Yline]
       
   299         lines,
       
   300         // [Xformat, Yformat] printf-like formating strings
       
   301         formats
       
   302     ){
       
   303         this.axes = zip(labels,marks,lines,formats).map((...args) => new Axis(...args));
       
   304 
       
   305         let [lx,ly] = this.axes.map(axis => axis.line);
       
   306         let [[xstart, xvect], [ystart, yvect]] = [lx,ly];
       
   307         let base_point = this.getBasePoint();
       
   308 
       
   309         // setup clipping for curves
       
   310         this.clipPathPathDattr =
       
   311             "m " + base_point.x + "," + base_point.y + " "
       
   312                  + xvect.x + "," + xvect.y + " "
       
   313                  + yvect.x + "," + yvect.y + " "
       
   314                  + -xvect.x + "," + -xvect.y + " "
       
   315                  + -yvect.x + "," + -yvect.y + " z";
       
   316 
       
   317         this.base_ref = [base_point, xvect, yvect];
       
   318 
       
   319         for(let axis of this.axes){
       
   320             axis.setBasePoint(base_point);
       
   321         }
       
   322     }
       
   323 
       
   324 	getBaseRef(){
       
   325         return this.base_ref;
       
   326 	}
       
   327 
       
   328     getClipPathPathDattr(){
       
   329         return this.clipPathPathDattr;
       
   330     }
       
   331 
       
   332     applyRanges(ranges){
       
   333         for(let [range,axis] of zip(ranges,this.axes)){
       
   334             axis.applyRange(range);
       
   335         }
       
   336     }
       
   337 
       
   338     getBasePoint() {
       
   339         let [[xstart, xvect], [ystart, yvect]] = this.axes.map(axis => axis.line);
       
   340 
       
   341         /*
       
   342         Compute graph clipping region base point
       
   343         ========================================
       
   344 
       
   345         Clipping region is a parallelogram containing axes lines,
       
   346         and whose sides are parallel to axes line respectively.
       
   347         Given axes lines are not starting at the same point, hereafter is
       
   348         calculus of parallelogram base point.
       
   349 
       
   350                               ^ given Y axis
       
   351                    /         /
       
   352                   /         /
       
   353                  /         /
       
   354          xstart /---------/--------------> given X axis
       
   355                /         /
       
   356               /         /
       
   357              /---------/--------------
       
   358         base_point   ystart
       
   359 
       
   360         base_point = xstart + yvect * a
       
   361         base_point = ystart + xvect * b
       
   362         ==> solve : "xstart + yvect * a = ystart + xvect * b" to find a and b and then base_point
       
   363 
       
   364         (1)   xstart.x + yvect.x * a = ystart.x + xvect.x * b
       
   365         (2)   xstart.y + yvect.y * a = ystart.y + xvect.y * b
       
   366 
       
   367         // express a
       
   368         (1)   a = (ystart.x + xvect.x * b) / (xstart.x + yvect.x)
       
   369 
       
   370         // substitute a to have only b
       
   371         (1+2) xstart.y + yvect.y * (ystart.x + xvect.x * b) / (xstart.x + yvect.x) = ystart.y + xvect.y * b
       
   372 
       
   373         // expand to isolate b
       
   374         (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
       
   375         (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)
       
   376 
       
   377         // factorize b
       
   378         (2) xstart.y + (yvect.y * ystart.x) / (xstart.x + yvect.x) - ystart.y = b * (xvect.y - (yvect.y * xvect.x ) / (xstart.x + yvect.x))
       
   379 
       
   380         // extract b
       
   381         (2) b = (xstart.y + (yvect.y * ystart.x) / (xstart.x + yvect.x) - ystart.y) / ((xvect.y - (yvect.y * xvect.x ) / (xstart.x + yvect.x)))
       
   382         */
       
   383 
       
   384         let b = (xstart.y + (yvect.y * ystart.x) / (xstart.x + yvect.x) - ystart.y) / ((xvect.y - (yvect.y * xvect.x ) / (xstart.x + yvect.x)));
       
   385         let base_point = new DOMPoint(ystart.x + xvect.x * b, ystart.y + xvect.y * b);
       
   386 
       
   387         // // compute given origin
       
   388         // // from drawing : given_origin = xstart - xvect * b
       
   389         // let given_origin = new DOMPoint(xstart.x - xvect.x * b, xstart.y - xvect.y * b);
       
   390 
       
   391         return base_point;
       
   392 
       
   393     }
       
   394 
       
   395 }
       
   396 
       
   397 class Axis {
       
   398     constructor(label, marks, line, format){
       
   399         this.lineElement = line;
       
   400         this.line = lineFromPath(line);
       
   401         this.format = format;
       
   402 
       
   403         this.label = label;
       
   404         this.marks = marks;
       
   405 
       
   406 
       
   407         // add transforms for elements sliding along the axis line
       
   408         for(let [elementname,element] of zip(["minor", "major", "label"],[...marks,label])){
       
   409             for(let name of ["base","slide"]){
       
   410                 let transform = svg_root.createSVGTransform();
       
   411                 element.transform.baseVal.appendItem(transform);
       
   412                 this[elementname+"_"+name+"_transform"]=transform;
       
   413             };
       
   414         };
       
   415 
       
   416         // group marks an labels together
       
   417         let parent = line.parentElement()
       
   418         marks_group = move_elements_to_group(marks);
       
   419         marks_and_label_group = move_elements_to_group([marks_group_use, label]);
       
   420         group = move_elements_to_group([marks_and_label_group,line]);
       
   421         parent.appendChild(group);
       
   422 
       
   423         // Add transforms to group 
       
   424         for(let name of ["base","origin"]){
       
   425             let transform = svg_root.createSVGTransform();
       
   426             group.transform.baseVal.appendItem(transform);
       
   427             this[name+"_transform"]=transform;
       
   428         };
       
   429 
       
   430         this.group = group;
       
   431         this.marks_group = marks_group;
       
   432         this.marks_and_label_group = marks_and_label_group;
       
   433 
       
   434         this.mlg_clones = [];
       
   435         this.last_mark_count = 0;
       
   436     }
       
   437 
       
   438     setBasePoint(base_point){
       
   439         // move Axis to base point 
       
   440         let [start, _vect] = this.lineElement;
       
   441         let v = vector(start, base_point);
       
   442         this.base_transform.setTranslate(v.x, v.y);
       
   443 
       
   444         // Move marks and label to base point. 
       
   445         // _|_______         _|________
       
   446         //  |  '  |     ==>   ' 
       
   447         //  |     0           0
       
   448         //  |                 |
       
   449 
       
   450         for(let [markname,mark] of zip(["minor", "major"],this.marks)){
       
   451             let transform = this[markname+"_base_transform"];
       
   452             let pos = vector(
       
   453                 // Marks are expected to be paths
       
   454                 // paths are expected to be lines
       
   455                 // intersection with axis line is taken 
       
   456                 // as reference for mark position
       
   457                 base_point, getLinesIntesection(
       
   458                     this.line, lineFromPath(mark)));
       
   459             this[markname+"_base_transform"].setTranslate(-pos.x, -pos.x);
       
   460             if(markname == "major"){ // label follow major mark
       
   461                 this.label_base_transform.setTranslate(-pos.x, -pos.x);
       
   462             }
       
   463         }
       
   464     }
       
   465 
       
   466     applyOriginAndUnitVector(offset, unit_vect){
       
   467         // offset is a representing position of an 
       
   468         // axis along the opposit axis line, expressed in major marks units
       
   469         // unit_vect is the translation in between to major marks
       
   470 
       
   471         //              ^
       
   472         //              | unit_vect
       
   473         //              |<--->
       
   474         //     _________|__________>
       
   475         //     ^  |  '  |  '  |  '
       
   476         //     |yoffset |     1 
       
   477         //     |        |
       
   478         //     v xoffset|
       
   479         //     X<------>|
       
   480         // base_point
       
   481 
       
   482         // move major marks and label to first positive mark position
       
   483         let v = vectorscale(unit_vect, offset+1);
       
   484         this.label_slide_transform.setTranslate(v.x, v.x);
       
   485         this.major_slide_transform.setTranslate(v.x, v.x);
       
   486         // move minor mark to first half positive mark position
       
   487         let h = vectorscale(unit_vect, offset+0.5);
       
   488         this.minor_slide_transform.setTranslate(h.x, h.x);
       
   489     }
       
   490 
       
   491     applyRange(min, max){
       
   492         let range = max - min;
       
   493 
       
   494         // compute how many units for a mark
       
   495         //
       
   496         // - Units are expected to be an order of magnitude smaller than range,
       
   497         //   so that marks are not too dense and also not too sparse.
       
   498         //   Order of magnitude of range is log10(range)
       
   499         //
       
   500         // - Units are necessarily power of ten, otherwise it is complicated to
       
   501         //   fill the text in labels...
       
   502         //   Unit is pow(10, integer_number )
       
   503         //
       
   504         // - To transform order of magnitude to an integer, floor() is used.
       
   505         //   This results in a count of mark fluctuating in between 10 and 100.
       
   506         //
       
   507         // - To spare resources result is better in between 5 and 50,
       
   508         //   and log10(5) is substracted to order of magnitude to obtain this
       
   509         //   log10(5) ~= 0.69897
       
   510 
       
   511 
       
   512         let unit = Math.pow(10, Math.floor(Math.log10(range)-0.69897));
       
   513 
       
   514         // TODO: for time values (ms), units may be :
       
   515         //       1       -> ms
       
   516         //       10      -> s/100
       
   517         //       100     -> s/10
       
   518         //       1000    -> s
       
   519         //       60000   -> min
       
   520         //       3600000 -> hour
       
   521         //       ...
       
   522         //
       
   523 
       
   524         // Compute position of origin along axis [0...range]
       
   525 
       
   526         // min < 0, max > 0, offset = -min
       
   527         // _____________|________________
       
   528         // ... -3 -2 -1 |0  1  2  3  4 ...
       
   529         // <--offset---> ^
       
   530         //               |_original
       
   531 
       
   532         // min > 0, max > 0, offset = 0
       
   533         // |________________
       
   534         // |6  7  8  9  10...
       
   535         //  ^
       
   536         //  |_original
       
   537 
       
   538         // min < 0, max < 0, offset = max-min (range)
       
   539         // _____________|_
       
   540         // ... -5 -4 -3 |-2
       
   541         // <--offset---> ^
       
   542         //               |_original
       
   543 
       
   544         let offset = (max>=0 && min>=0) ? 0 : (
       
   545                      (max<0 && min<0)   ? range : -min);
       
   546 
       
   547         // compute unit vector
       
   548         let [_start, vect] = this.line;
       
   549         let unit_vect = vectorscale(vect, unit/range);
       
   550         let [umin, umax, uoffset] = [min,max,offset].map(val => Math.round(val/unit));
       
   551         let mark_count = umax-umin;
       
   552 
       
   553         // apply unit vector to marks and label
       
   554         this.label_and_marks.applyOriginAndUnitVector(offset, unit_vect);
       
   555 
       
   556         // duplicate marks and labels as needed
       
   557         let current_mark_count = this.mlg_clones.length;
       
   558         for(let i = current_mark_count; i <= mark_count; i++){
       
   559             // cloneNode() label and add a svg:use of marks in a new group
       
   560             let newgroup = document.createElementNS(xmlns,"g");
       
   561             let transform = svg_root.createSVGTransform();
       
   562             let newlabel = cloneNode(this.label);
       
   563             let newuse = document.createElementNS(xmlns,"use");
       
   564             let newuseAttr = document.createAttribute("xlink:href");
       
   565             newuseAttr.value = "#"+this.marks_group.id;
       
   566             newuse.setAttributeNode(newuseAttr.value);
       
   567             newgroup.transform.baseVal.appendItem(transform);
       
   568             newgroup.appendChild(newlabel);
       
   569             newgroup.appendChild(newuse);
       
   570             this.mlg_clones.push([tranform,newgroup]);
       
   571         }
       
   572 
       
   573         // move marks and labels, set labels
       
   574         for(let u = 0; u <= mark_count; u++){
       
   575             let i = 0;
       
   576             let val = (umin + u) * unit;
       
   577             let vec = vectorscale(unit_vect, offset + val);
       
   578             let text = this.format ? sprintf(this.format, val) : val.toString();
       
   579             if(u == uoffset){
       
   580                 // apply offset to original marks and label groups
       
   581                 this.origin_transform.setTranslate(vec.x, vec.x);
       
   582 
       
   583                 // update original label text
       
   584                 this.label_and_mark.label.textContent = text;
       
   585             } else {
       
   586                 let [transform,element] = this.mlg_clones[i++];
       
   587 
       
   588                 // apply unit vector*N to marks and label groups
       
   589                 transform.setTranslate(vec.x, vec.x);
       
   590 
       
   591                 // update label text
       
   592                 element.getElementsByTagName("tspan")[0].textContent = text;
       
   593 
       
   594                 // Attach to group if not already
       
   595                 if(i >= this.last_mark_count){
       
   596                     this.group.appendChild(element);
       
   597                 }
       
   598             }
       
   599         }
       
   600 
       
   601         // dettach marks and label from group if not anymore visible
       
   602         for(let i = current_mark_count; i < this.last_mark_count; i++){
       
   603             let [transform,element] = this.mlg_clones[i];
       
   604             this.group.removeChild(element);
       
   605         }
       
   606 
       
   607         this.last_mark_count = current_mark_count;
       
   608     }
       
   609 }
       
   610 ||